Java 类和对象在内存中的表现形式,栈、堆、方法区、常量池

Java内存分配与管理是Java的核心技术之一,不管学习任何一门语言,我们要知其然,知其所以然,本文主要分析下Java中类和对象在内存中的表现形式,方便我们对其有更深了解。一般Java在内存分配时会涉及到这几个区域:栈区(stack)、堆区(heap)、方法区(Method Area)、常量池。我们先对下面几个概念进行深刻了解后,再进行画图分析类和对象在内存中的变化及表现形式。

栈:存放基本类型的数据和对象的引用变量的数据,但对象本身不存放在栈中,而是存放在堆中(new 出来的对象)

堆:存放用new产生的对象数据,每个对象包含了一个与之对应的 class 类的信息。

方法区(又称为静态区):存放对象中用static定义的静态成员

常量池:通常用来存放常量数据、静态变量、类的加载信息等

一、栈区

在函数(方法)中定义的一些基本类型的变量或者对象的引用变量都在栈内存中分配。

当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。

每个方法(Method)执行时,都会创建一个方法栈区,用于存储局部变量表、操作数栈、动态链接、方法出口信息等

栈中所存储的变量和引用都是局部的(即:定义在方法体中的变量或者引用),局部变量和引用都在栈中(包括final的局部变量)

八种基本数据类型(byte、short、int、long、float、double、char、boolean)的局部变量(定义在方法体中的基本数据类型的变量)在栈中存储的是它们对应的值

每个线程包含一个栈区,栈中只保存基本数据类型的变量和引用数据类型的变量,每个栈中的数据(基本数据类型和对象的引用)都是私有的,其它栈是无法进行访问的。栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

栈中还存储局部的对象的引用(定义在方法体中的引用类型的变量)对象的引用并不是对象本身,而是对象在堆中的地址,换句话说,局部的对象的引用所指对象在堆中的地址在存储在了栈中。当然,如果对象的引用没有指向具体的对象,对象的引用则是null

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

栈有一个很重要的特殊性,就是存在栈中的数据可以共享。

二、堆区

堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

堆内存是被所有线程共享的一块内存区域,在虚拟机启动时创建。Java堆(Java Heap)唯一目的就是存放对象实例。所有的对象实例及数组都要在**Java堆(Java Heap)**上分配内存空间。

在堆中产生了一个数组或对象后,在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。

引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉),这也是Java比较占内存的原因。

实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!

Java的堆是一个运行时数据区,类的对象从中分配空间。对象一般通过new 来创建,例如new Date(),它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

三、方法区

方法区跟堆一样,又被称为静态区,通常存放常量数据。它存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等,它跟堆一样,被所有的线程共享。

3.1 存储的类信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  • 这个类型的完整有效名称(全名=包名.类名)

  • 这个类型直接父类的完整有效名称( java.lang.Object除外,其他类型若没有声明父类,默认父类是Object)

  • 这个类型的修饰符(public、abstract、final的某个子集)

  • 这个类型直接接口的一个有序列表

除此之外还方法区(Method Area)存储类信息还有

  • 类型的常量池( constant pool)

  • 域(Field)信息

  • 方法(Method)信息

  • 除了常量外的所有静态(static)变量

3.2 存储的常量

static final修饰的成员变量都存储于 方法区(Method Area)中

3.3 存储的静态变量

  • 静态变量又称为类变量,类中被static修饰的成员变量都是静态变量(类变量)

  • 静态变量之所以又称为类变量,是因为静态变量和类关联在一起,随着类的加载而存在于方法区(而不是堆中)

  • 八种基本数据类型(byte、short、int、long、float、double、char、boolean)的静态变量会在方法区开辟空间,并将对应的值存储在方法方法区,对于引用类型的静态变量如果未用new关键字为引用类型的静态变量分配对象(如:static Object obj;),那么对象的引用obj会存储在方法区中,并为其指定默认值null;若对于引用类型的静态变量如果用new关键字为引用类型的静态变量分配对象(如:static Cat cat = new Cat();),那么对象的引用cat会存储在方法区中,并且该对象在堆中的地址也会存储在方法区中(注意此时静态变量只存储了对象的堆地址,而对象本身仍在堆内存中);当然这个过程还涉及到静态变量初始化问题。

3.4 存储的方法(Method)

程序运行时会加载类编译生成的字节码,这个过程中静态变量(类变量)和静态方法及普通方法对应的字节码加载到方法区。

方法区中没有实例变量,这是因为,类加载先于对应类对象的产生,而实例变量是和对象关联在一起的,没有对象就不存在实例变量,类加载时没有对象,所以方法区中没有实例变量。

静态变量(类变量)和静态方法及普通方法在方法区(Method Area)存储方式是有区别的

四、常量池

常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。

除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:类和接口的全限定名;字段的名称和描述符;方法和名称和描述符。

虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。在程序执行的时候,常量池会储存在方法区(Method Area),而不是堆中。

五、画图分析类实例化及操作时在内存中的变化及表现形式

package com.joshua317;

public class Main {

    public static void main(String[] args) {
        //实例化一个Cat对象
        Cat cat = new Cat();
        //给成员变量赋值
        cat.name = "招财";
        cat.age = 2;
        cat.weight = 2.02;
        //打印
        System.out.println("小猫的名字:"+cat.name + " 小猫的年龄:"+cat.age);
        //调用成员方法
        cat.say();
    }
}

class Cat {
    /**
     * 成员变量 name
     */
    String name;
    /**
     * 成员变量 age
     */
    int age;
    /**
     * 成员变量 weight
     */
    double weight;

    public void say()
    {
        System.out.println("喵喵~~");
    }
}

上面这段代码首先有个主程序的类Main,这个我们不过多说明。我们主要分析main函数体里面的这段代码。

我们需要知道在Cat类中,定义了三个成员属性:name、age、weight;定义了一个成员方法:say();

//实例化一个Cat对象
Cat cat = new Cat();
//给成员变量赋值
cat.name = "招财";
cat.age = 2;
cat.weight = 2.02;
//打印
System.out.println("小猫的名字:"+cat.name + " 小猫的年龄:"+cat.age);
//调用成员方法
cat.say();

在main() 函数里实例化对象 cat, 内存中在堆区内会给实例化对象 cat 分配一个内存地址,然后我们给对象 cat进行了赋值并且打印了一些信息,最后调用了成员方法 say() ,程序执行完毕。

1.在程序的执行过程中,首先Main类中的成员属性和成员方法会加载到方法区

2.程序执行类Main的main() 方法时,main()函数方法体会进入栈区,这一过程叫做进栈(压栈)。

3.程序执行到 Cat cat = new Cat(); 时,首先会把Cat类的成员属性和成员方法加载到方法区,此时方法的内存空间地址为1x000000,同时在在堆内存开辟一块内存空间74a14482,用于存放 Cat 实例对象,并给成员属性及成员方法分配对应的地址空间,比如下图的0x000001~0x000004即为对象分配的堆内存地址,但此时成员属性都是默认值,比如int类型默认值为0,String类型默认值为null,成员方法地址值为方法区对应成员方法体的内存地址值;然后在栈内存中会给变量cat分配一个栈地址34b23231,用来存放Cat实例对象的引用地址的值74a14482

4.接下来对 cat 对象进行赋值

//给成员变量赋值
cat.name = "招财";
cat.age = 2;
cat.weight = 2.02;

先在栈区找到引用变量cat,然后根据地址值找到 new Cat() 对象的内存地址,并对里面的属性进行赋值操作。由于成员属性name的类型为String,为引用数据类型,所以此时会在常量池开辟一块地址空间2x00000000,存放招财这个值,而age的类型为int,weight的类型为double,都为基本数据类型,所以值直接存放堆中。

5.当程序执行到 cat.say() ;方法时,会先到栈区找到cat这个引用变量(这个变量存的是对象的引用地址),然后根据该地址值在堆内存中找到 new Cat() 对象里面的say()方法进行调用,在调用say()方法时,会在栈区开辟一块空间进行运行。

6.在方法体void say()被调用完成后,就会立刻马上从栈内弹出(出站 ),最后,在main()函数完成后,main()函数也会出栈

joshua317博客
请先登录后发表评论
  • latest comments
  • 总共1条评论
joshua317博客

一叶知秋 :写的真是太棒啦,简单易懂,太厉害啦!!

2023-04-06 10:56:57 回复