java中变量的内存分配
java中的变量大体分为:类(静态)变量、成员变量、局部变量,在class文件被jvm的类加载器加载后,随后这些变量被分配至内存中。但是,它们何时被分配至内存的何处呢?
jvm把自己运行时管理的内存称为运行时数据区。主要分为栈、堆、方法区,java变量就存在这3个区中。
下表为栈、堆、方法区内存分配情况:
运行时数据区 | 内存分配时机 | 分配内容 | 备注 |
栈 | 线程执行方法时 |
• 当前线程中局部基本类型的变量(boolean、char、byte、short、int、long、float、double) |
|
堆 | new创建对象时 |
• 对象实例及其成员变量 |
• 可以认为Java中所有通过new创建的对象的内存都在此分配 |
方法区 | 类加载器加载class文件时 |
• 类的信息(名称、修饰符等) |
• 很难被回收,在一定的条件下它也会被GC |
注意:
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()/*堆*/; } }
根据上面示例代码,我画了一张图加以说明: