java中变量的内存分配

java中的变量大体分为:类(静态)变量、成员变量、局部变量,在class文件被jvm的类加载器加载后,随后这些变量被分配至内存中。但是,它们何时被分配至内存的何处呢?

jvm把自己运行时管理的内存称为运行时数据区。主要分为栈、堆、方法区,java变量就存在这3个区中。

下表为栈、堆、方法区内存分配情况:

运行时数据区 内存分配时机 分配内容 备注
线程执行方法时

• 当前线程中局部基本类型的变量(boolean、char、byte、short、int、long、float、double)
• 对象引用(非基本类型的对象在JVM栈上仅存放一个指向堆上的地址)
• returnAddress......

 
new创建对象时

• 对象实例及其成员变量
• 数组值

• 可以认为Java中所有通过new创建的对象的内存都在此分配
• 是GC回收的主要区域

方法区 类加载器加载class文件时

• 类的信息(名称、修饰符等)
• 类中的静态变量(从jdk7开始移至堆中)
• 类中定义为final类型的常量(从jdk7开始移至堆中)
• 类中的Field信息
• 类中的方法信息

• 很难被回收,在一定的条件下它也会被GC
• jdk7及以前由永久代实现,因此方法区也被习惯称为永久代,jdk8由元数据区实现
• jdk7开始,静态变量、运行时常量池(final修饰的常量在运行时常量池中)被移至堆中。在“《深入理解Java虚拟机》第三版 2.4.3 方法区和运行时常量池溢出”一节中的实例测试也印证了运行时常量池在jdk7搬家到了堆中

注意:
1.对于引用类型的变量而言,由于引用类型的变量由两部分组成:引用及引用指向的对象。因此,对于引用类型的变量而言,搞清楚它在内存中的位置需要明白:其引用存储在哪里,其引用指向的对象存储在哪里。而对于基本类型的变量,由于它没有引用(变量名和引用是两码事,基本类型的变量虽然有变量名但没有引用),所以无需考虑基本类型变量的引用存储在哪里。
2.本文的难点是:搞清楚静态变量及常量在内存中的存储位置因为它们在jdk7时“搬过家”:从方法区的永久代搬家到了堆中(至于搬到了堆中的具体位置笔者也不是很清楚)。由于在《Java虚拟机规范》中我没有找到这两个家伙的搬家记录,下面仅从《深入理解Java虚拟机》中的相关记录进行说明。需要说明的是:由于常量在类加载器加载后被存储在运行时常量池中,因此确定了运行时常量池的存储位置自然就确定了常量的存储位置。
• 静态变量的搬家记录:

到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出——《深入理解Java虚拟机》第三版 2.2运行时数据区域

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8【笔者注:作者在后面说到JDK1.7的时候静态变量已从永久代移出,所以这里应该是印刷错误或笔误,正确的写法应该是“JDK7”】及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了,关于这部分内容,笔者已在4.3.1节介绍并且验证过。——《深入理解Java虚拟机》第三版 7.3类加载的过程——《深入理解Java虚拟机》第三版 7.3类加载的过程

• 常量的搬家记录:

在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,......出现这种变化,是因为自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中 ——《深入理解Java虚拟机》第三版 2.4.3 方法区和运行时常量池溢出

上文从堆、栈、方法区的角度说明了变量的内存分配,但是,通常情况下,我们很少从这个角度想问题,更多的是,我们会从变量的角度,去思考这个变量存在哪里。下文就从变量的角度再梳理一遍,虽然你可能认为没这个必要,因为下文的内容主要依据是来自于上文,本来在写本文时,笔者也考虑了将下文干脆删去使得本文更加简洁,但是最后还是保留了,或许这类似于古诗文中的“互文”吧。

一、局部变量

• 局部变量存在栈中。当线程执行某一个方法时,jvm会在栈空间创建一个栈帧,栈帧包含局部变量表、动态链接等信息。局部变量表存放了局部变量,包含基本类型变量(boolean、byte、char、short、int、float、long、double)及对象引用(reference类型,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或其他与此对象相关的位置)。
• 引用指向的对象实例存储在堆中。

二、成员变量

• 当new创建一个对象时,成员变量会随着对象被分配在堆中。如果成员变量是引用类型,引用会随着对象存储在一块堆空间中,引用指向的对象存储在另一块堆空间中。

三、类(静态)变量、常量

• 类(静态)变量
类加载器在加载class文件的准备阶段,即为类变量分配内存并设置初始值的阶段。类变量被分配在方法区中,在jdk7之前,HotSpot使用永久代实现方法区时,类变量被分配在永久代中,从jdk7开始,类变量被移至堆中。(静态常量和静态变量存储的位置是一致的)

• 常量
常量也属于成员变量,按照成员变量的内存分配原则,它也应该随着创建的对象一起被分配到堆中,然而并非如此,被final修饰的常量被编译成class文件后,位于常量池表中,在类加载后,常量池表中的内容存放在运行时常量池中,而运行时常量池是方法区的一部分。运行时常量池像类变量一样,在jdk7也搬家到了堆中。因此,在jdk7之前,常量存储在方法区中,从jdk7开始,常量存储在堆中。

总结:

在类中定义的三种变量在内存中的分配位置及分配时机如下图:

代码示例

package 内存分配原理;

public class Demo {
    /*
    1.成员变量
    分配时机:new创建对象时
    分配位置:堆
     */
    // 举例说明:在创建对象时(C1 c = new C1();)会给对象c的成员变量分配内存,其中基本类型数据i及引用s作为对象c的实例数据存储在堆中;引用s指向的对象new String("a")存储在另一块堆空间
    private int i = 1; // 堆
    private String s/*引用:堆*/ = new String("a")/*对象实例:另一块堆空间中*/;
    private String s1/*引用:堆*/ = "aa"/*字面量aa:字符串常量池*/; // 字符串常量池是运行时常量池的一部分,在jdk7之前位于方法区,jdk7开始移至堆中

    /*
    2.类(静态)变量&常量&静态常量
    分配时机:类加载时
    分配位置:方法区(jdk7之前) -> 堆(jdk7及以后)
     */
    // 静态变量
    private static int ii = 2; // 方法区(jdk7之前) -> 堆(jdk7及以后)
    private static String ss/*引用:方法区(jdk7之前) -> 堆(jdk7及以后)*/ = new String("b")/*对象实例:堆*/;
    // 常量
    private final int iii = 3;// 方法区(jdk7之前) -> 堆(jdk7及以后)
    private final String sss/*引用:方法区(jdk7之前) -> 堆(jdk7及以后)*/ = new String("c")/*对象实例:堆*/;
    // 静态常量
    private final static String ssss/*引用:方法区(jdk7之前) -> 堆(jdk7及以后)*/ = new String("d")/*对象实例:堆*/;

    /*
    3.局部变量
    分配时机:线程执行方法时
    分配位置:栈
    注:对于引用类型,其引用存在栈中,其引用所指向的对象实例存在堆中
     */
    void m() {
        int i4 = 4; // 栈
        Demo d/*引用:栈*/ = new Demo()/*堆*/;
    }
}

根据上面示例代码,我画了一张图加以说明:

 

posted @ 2021-05-30 23:42  Tom1997  阅读(1378)  评论(0编辑  收藏  举报