目录
1 JVM内存模型简介
要说Java中的栈,堆,方法区和常量池就要提到HotSpot,HotSpot是Sun JDK 和 Open JDK中所带的虚拟机。 (Sun JDK 和 Open JDK除了注释不同,代码实现基本上是一样的)
因为内存模型众说纷纭,这是本人理解到总结的模型,以下内容可能与此图冲突,以上图为准。
以下说的内容都是围绕HotSpot。
- 堆——堆是所有线程共享的,主要用来存储对象。其中,堆可分为:年轻代和老年代两块区域。使用NewRatio参数来设定比例。对于年轻代,一个Eden区和两个Suvivor区,使用参数SuvivorRatio来设定大小;
- Java虚拟机栈/本地方法栈——线程私有的,主要存放局部变量表,操作数栈,动态链接和方法出口等;
- 程序计数器——同样是线程私有的,记录当前线程的行号指示器,为线程的切换提供保障;
- 方法区——线程共享的,主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。方法区理论上来说是堆的逻辑组成部分;
- 运行时常量池——是方法区的一部分,用于存放编译期生成的各种字面量和符号引用;
如果是一个类里面的静态成员变量和静态成员方法,它是存储在方法区的,静态成员变量是在方法区的静态域里面,而静态成员方法是在方法区的class二进制信息里面(.class文件和方法区里面的二进制信息不一样,读取.class文件按照虚拟机需要的格式存储在方法区。这种格式包括数据结构方面),静态成员和静态成员方法使用时不用创建对象,即类加载初始化后就可以使用,并且是线程共享的。
通过图中分析,很多问题也能够迎刃而解,比如不同线程调用方法为什么是线程安全的。局部变量存储在哪儿里(栈中),成员变量存储在哪儿里(静态成员变量存储在方法区,非静态成员变量存储在堆区),为什么局部变量不能够static修饰等(局部变量存储在栈区,在方法调用时不能够自动初始化必须由程序员手动初始化,否则会报错,归根结底是由于static变量和局部变量存储的位置不一样。)。
2 永久代和方法区的关系
涉及到内存模型时,往往会提到永久代,那么它和方法区又是什么关系呢?《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。其他的虚拟机实现并没有永久带这一说法。在1.7之前在(JDK1.2 ~ JDK6)的实现中,HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收,可以使用如下参数来调节方法区的大小:
-XX:PermSize
方法区初始大小
-XX:MaxPermSize
方法区最大大小
超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen
方法区是一个规范。存储的运行时的常量池,类加载后的类信息、final定义的常量、静态变量及即时编译器编译后的数据信息等等。
永久代(PermGen)是方法区的一个实现(相当于接口于实现类的关系),我们通常说的是HotSpot虚拟机,也就是说,它用来存放运行时的常量池,类加载后的元数据、final定义的常量、静态变量及即时编译器编译后的数据信息等等。
jdk1.6及以前,存在,并且字符串常量池StringPool也在方法区就是永久代中。
jdk1.7已经逐步移去,字符串常量池StringPool移到了堆中。
jdk1.8彻底移去,替换成了元空间,字符串常量池StringPool移到了元空间中。
3 元空间
对于Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它可永久代有什么不同的?存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
永久代和元空间的区别:
元空间是直接存在内存中的,并不在java虚拟机中,因此元空间依赖于内存大小。当然也可以自定义元空间大小。
永久代理论上是堆的一部分,和新生代老年代的地址是连续的。
元空间存储类的元信息,而将静态变量和常量池等并入堆中。相当于原来永久代的数据被分到了堆和元空间中。
jdk1.8,字符串常量池StringPool移到了元空间,StringPool维护的是字符串的引用,不是实例(在HotSpot虚拟机找那个,实现stringpool的是StringTable类,是一个哈希表来记录引用),实例在堆中开辟了一块空间,专门储存。
jdk1.8之后,元空间替换了永久代,如下图所示。
4 Class文件常量池
常量池,分为class常量池和运行时常量池,运行时的常量池是属于方法区的一部分,而Class常量池是Class文件中的。
Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 这里的字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;声明为final的常量值指的是成员变量,不包含本地变量,本地变量是属于方法的。这些都在常量池的 UTF-8 表中(逻辑上的划分);
- 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用;
还有一张图来表示方法区中class文件信息包括哪些内容:
即方法区里的class文件信息包括:魔数,版本号,常量池,类,父类, 接口数组,字段,方法等信息,其中类里又包括字段和方法的信息。
常量池里存储的内容:
5 运行时常量池
运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。
Class文件中的常量池:
当class文件被Java虚拟机加载进来后存在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量,符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引。
方法区中运行时常量池(动态性):
由于class文件中有一项信息是常量池,这部分内容在类加载后进入方法区的运行时常量池存放,在常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。
与Class文件一重大特征是具有动态性,Java并非要求常量一定只有编译期才能产生,即并不是先放入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,也就是String类的intern()方法。
运行时常量池是class文件常量池在运行时的表示。
运行时常量池里的内容除了是常量池里的内容外(符号引用的索引值是指向运行时常量池的),还将静态常量池里的符号引用转变为直接引用,而且动态常量池里的内容是能动态添加的。例如调用String的intern方法就能将String的值添加到String常量池中,这里String常量池是包含在运行时常量池里的,但在jdk1.8后,将String常量池放到了堆中。
6 字符串常量池
字符串常量池是全局的,JVM 中独此一份,因此也称为全局字符串常量池。运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。其实,“使用常量池”对应的字节码是一个 ldc
指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。String 类的 intern()
方法还可在运行期间把字符串放到字符串常量池中。JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,超出此范围仍然会去创建新的对象。其中:
- 在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;
- 在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;
- jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用。
7 常量池应用总结
几种常量池:
(1)静态常量池:即*.class文件中的常量池,在Class文件结构中,最头的4个字节存储魔数,用于确定一个文件是否能被JVM接受,接着4个字节用于存储版本号,前2个为次版本号,后2个主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。
这种常量池占用class文件绝大部分空间,主要用于存放两大类常量:字面量和符号引用量,字面量相当于Java语言层面常量的概念,如文本字符串、基础数据、声明为final的常值等;符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名、字段名称描述符、方法名称描述符。类的加载过程中的链接部分的解析步骤就是把符号引用替换为直接引用,即把那些描述符(名字)替换为能直接定位到字段、方法的引用或句柄(地址)。
(2)运行时常量池:虚拟机会将各个class文件中的常量池载入到运行时常量池中,即编译期间生成的字面量、符号引用,总之就是装载class文件。为什么它叫运行时常量池呢?因为这个常量池在运行时,里面的常量是可以增加的。如:“+”连接字符生成新字符后调用 intern()方法、生成基础数据的包装类型等等。
(3)字符串常量池 :字符串常量池可以理解为是分担了部分运行时常量池的工作。加载时,对于class文件的静态常量池,如果是字符串就会被装到字符串常量池中。
(4)整型常量池:Integer,类似字符串常量池。管理-128--127的常量。类似的还有Character、Long等常量池(基本数据类型没有,Double、Float也没有常量池)
总结就是:
class文件有常量池存放这个类的信息,占用了大多数空间。但是运行时所有加载进来的class文件的常量池的东西都要放到运行时常量池,这个运行时常量池还可以在运行时添加常量。字符串常量池、Integer等常量池则是分担了运行时常量池的工作,
在永久代移除后,字符串常量池也不再放在永久代了,但是也没有放到新的方法区---元空间里,而是留在了堆里(为了方便回收?)。运行时常量池当然是随着搬家到了元空间里,毕竟它是装类的重要信息的,有它的地方才称得上是方法区。
8 内存空间的使用常见问题
堆:堆主要存放Java在运行过程中new出来的对象,凡是通过new生成的对象都存放在堆中,对于堆中的对象生命周期的管理由Java虚拟机的垃圾回收机制GC进行回收和统一管理。类的非静态成员变量也放在堆区,其中基本数据类型是直接保存值,而复杂类型是保存指向对象的引用,非静态成员变量在类的实例化时开辟空间并且初始化。所以你要知道类的几个时机,加载-连接-初始化-实例化。
栈:栈主要存放在运行期间用到的一些局部变量(基本数据类型的变量)或者是指向其他对象的一些引用,因为方法执行时,被分配的内存就在栈中,所以当然存储的局部变量就在栈中咯。当一段代码或者一个方法调用完毕后,栈中为这段代码所提供的基本数据类型或者对象的引用立即被释放;
常量池:常量池是方法区的一部分内存。常量池在编译期间就将一部分数据存放于该区域,包含基本数据类型如int、long等以final声明的常量值,和String字符串、特别注意的是对于方法运行期位于栈中的局部变量String常量的值可以通过 String.intern()方法将该值置入到常量池中。
静态域:位于方法区的一块内存。存放类中以static声明的静态成员变量
方法区:是各个线程共享的内存区域,它用于存储class二进制文件,包含了虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。它有个名字叫做Non-Heap(非堆),目的是与Java堆区分开
需要特别注意的是:
方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待 !
最后总结起来就是:
栈:为即时调用的方法开辟空间,存储局部变量值(基本数据类型),局部变量引用。注意:局部变量必须手动初始化。
堆:存放引用类型的对象,即new出来的对象、数组值、类的非静态成员变量值(基本数据类型)、非静态成员变量引用。其中非静态成员变量在实例化时开辟空间初始化值。更具体点,个人感觉非静态成员变量是放在堆的对象中。
方法区:存放class二进制文件。包含类信息、静态变量,常量池(String字符串和final修饰的常量值等),类的版本号等基本信息。因为是共享的区域,所以如果静态成员变量的值或者常量值(String类型的值能够非修改,具体请查看博客)被修改了直接就会反应到其它类的对象中。
成员变量与局部变量总结:
一:在方法中声明的变,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因<1>在方法中生明的变量可以是基本类型的变量,也可以是引用类型的变量,(1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中(2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的》》》二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的,<1>同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量(1)当声明的是基本类型的变量其变量名及其只时放在堆类存中的,(2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象
如果是一个类里面的静态成员变量和静态成员方法,它是存储在方法区的,静态成员变量是在方法区的静态域里面,而静态成员方法是在方法区的class二进制信息里面(.class文件和方法区里面的二进制信息不一样,读取.class文件按照虚拟机需要的格式存储在方法区。这种格式包括数据结构方面),静态成员和静态成员方法使用时不用创建对象,即类加载初始化后就可以使用,并且是线程共享的。
通过图中分析,很多问题也能够迎刃而解,比如不同线程调用方法为什么是线程安全的。局部变量存储在哪儿里(栈中),成员变量存储在哪儿里(静态成员变量存储在方法区,非静态成员变量存储在堆区),为什么局部变量不能够static修饰等(局部变量存储在栈区,在方法调用时不能够自动初始化必须由程序员手动初始化,否则会报错,归根结底是由于static变量和局部变量存储的位置不一样。)。
9 实例分析
声明一个类:
public class A {
public final String tempString="world";//这里可以把final去掉,结果等同!!
public final char[] charArray="Hello".toCharArray();
public char[] getCharArray() {
return charArray;
}
public String getTempString() {
return tempString;
}
}
创建测试类:
public class TestA {
public static void main(String[] args) {
A a1=new A();
A a2=new A();
System.out.println(a1.charArray==a2.charArray);
System.out.println(a1.tempString==a2.tempString);
}
}
输出结果:
false
true
要想明白上面字符串对比为什么输出为true你必须知道:
该图片截自《深入理解Java虚拟机》
- 一个Class字节码文件的Class字节码文件对象只有一个常量池,常量池被所有线程共享。
- 在常量池中,字符串被存储为一个字符序列,每个字符序列都对应一个String对象,该对象保存在堆中。所以也就是说为什么
String temp="xxx";
能够当成一个对象使用!! - 如果多个线程去访问A类中的String字符串,每次都会到常量区中去找该字符序列的引用。
- 所以访问A类被创建的两个A类型对象的String字符串对比会输出true。
如果对字符串输出为true还是不懂,可以参考这篇博客:字符串被存储的原理过程
那么为什么final类型的字符数组就不为true了呢??
申明(不管是通过new还是通过直接写一个数组)一个数组其实在Java中就等同创建了一个对象,即每次创建类的对象都会自动创建一个新的数组空间。
其中要注意的是:常量池中存储字符数组只是存储的是每个字符或者字符串。
为了证明每次获取的final数组地址不一样,并且数组中的字符都会存储在常量池中,我们需要参考另外一个代码:
public class A {
public String tempString="world";
public final String tempStringArray[]={"Fire","Lang"};
public final char[] charArray={'h','e','l','l','o'};
public Character charx='l';
public char[] getCharArray() {
return charArray;
}
public String getTempString() {
return tempString;
}
public String[] getTempStringArray() {
return tempStringArray;
}
public Character getCharx() {
return charx;
}
}
测试代码:
public class TestA {
public static void main(String[] args) {
A a1=new A();
A a2=new A();
System.out.println(a1.tempString==a2.tempString);
System.out.println(a1.tempStringArray==a2.tempStringArray);//看这里
System.out.println("#####################");//看这里
System.out.println(a1.tempStringArray[0]==a2.tempStringArray[0]);
System.out.println(a1.tempStringArray[0]=="Fire");
System.out.println("#####################");
System.out.println(a1.charArray==a2.charArray);
System.out.println(a1.charx==a2.charx);
}
}
输出:
true
false
#####################
true
true
#####################
false
true
可以看到每次输出的final数组地址都不一样,最重要的是String类型的数组地址也都不一样!!但是String类型数组中的每个字符串都存储在常量池中。
所以可以肯定的是字符串和其它能够确定值的final字面量值是存储在常量池的!!并且在方法区内存中只有一份!!与所有线程共享访问!!
常量池存储的项目类型:
参考链接:
JDK8的JVM内存结构,元空间替代永久代成为方法区及常量池的变化