本小节主要讲了Java虚拟机的运行时数据区域:
- 将整个内存划分几个区域去管理
-
每个区域的用途、创建/销毁的时机各不相同:有的随虚拟机进程的启动而一直存在,有的则是依赖用户线程的启动和结束而建立和销毁。
image.png
一、程序计数器
- 当前线程所执行的字节码的行号指示器
- 字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 线程私有,线程之间计数器互不影响,独立存储
二、Java虚拟机栈
- 线程私有
- Java方法执行的线程内存模型:方法被执行的时候,虚拟机会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息
- 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
- 局部变量表存放编译期可知的基本数据类型、对象引用(reference类型,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄)和returnAddress类型(指向了一条字节码指令的地址)。
- 这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个
- 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小
三、本地方法栈
- 本地方法栈与虚拟机栈的作用是相似的,其区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
- HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。
- 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
四、Java堆
- Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
- 此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。
- Java堆是垃圾收集器管理的内存区域
- 从分配内存的角度看,Java堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率
- Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
- Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)
- 如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常
五、方法区
- 线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- JDK 6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划
- JDK7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出
- JDK8 HotSpot终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
- 这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载
- 方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常
1. class文件常量池
- Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用
2. 运行时常量池
- 属于方法区的一部分
- class文件常量池中的内容将在类加载后存放到方法区的运行时常量池中。
- 一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中
- 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性
- Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
3. 全局字符串常量池
- class文件常量池和运行时常量池是每个类都有的
- 全局字符串常量池在每个HotSpot VM实例中只有一份,所有类共享
- 全局字符串池里的内容是在类加载完成,经过验证、准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中
- string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的
- HotSpot VM里实现string pool功能的是一个StringTable类,它是一个哈希表,里面存放的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份
六、直接内存
- 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
- NIO使用Native函数库直接分配堆外内存,然后通过存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样可以显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
- 出现OutOfMemoryError异常
七、String.intern()
例一
之前一直对这个方法的背后机制不是很懂,这里再详细解释一下
首先由于JDK1.6和JDK1.7在字符串常量池的实现上发生了变化,导致有些代码的运行结果不一致:
String str1 = new String("123") + new String("456");
System.out.println(str1.intern() == str1);
System.out.println(str1 == "123456");
上面这段代码在JDK1.7及以后的版本中运行结果为
true
true
但是在JDK1.7以前,结果则是
false
false
这里以HotSpot为例,JDK1.7之前,字符串常量池是在永久代实现的,之后则移到了堆内存中;
JDK1.7之前
String str1 = new String("123")+ new String("456");
这行代码在堆内生成了一个String类型的对象,栈上的str1引用该对象,在字符串常量池中,生成了“123”和“456”字符串对象;
System.out.println(str1.intern() == str1);
str1.intern()
则在字符串常量池中查找是否与str1
相等(equals
)的字符串,即“123456”,因为没有,所以在常量池中创建一个“123456”的对象,并将其引用返回;由于str1引用的是堆上的对象内存,str1.intern()
引用的是常量池中的内存,所以两者不等。
System.out.println(str1 == "123456");
由前面一行代码中可知,str1.intern()
返回的即是常量池中"123456"字符串对象的引用,所以结果也是false。
JDK1.7及之后
String str1 = new String("123") + new String("456");
System.out.println(str1.intern() == str1);
System.out.println(str1 == "123456");
- 第一行代码,在字符串常量池中生成“123”和“456”对象,并在堆空间中生成
str1
引用指向的对象(内容为"123456")。注意此时常量池中是没有“123456”对象的。 - 第二行代码,
str1.intern()
将str1
中的“123456”字符串放入常量池中,此时常量池中不存在“123456”字符串,JDK1.6的做法是直接在常量池中生成一个"123456"的对象。 - 在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向
str1
引用的对象,也就是说str1.intern() == str1
会返回true。从而str1 == "123456"
也返回true。
例二
String str11 = new String("abc");
String str22 = str11.intern(); //
String str33 = "abc";
System.out.println(str11 == str22);//false
System.out.println(str33 == str22);//true
JDK1.8
- 第一行代码:栈上
str11
指向堆内存中的字符串对象(由new
产生),堆内的对象指向字符串常量池中的“abc”; - 第二行代码:此时由于字符串常量池中的“abc”已经存在,直接返回该字符串的引用给
str22
,而str11
指向的是堆内存中的字符串对象,所以str11 == str22
返回false; - 第三行代码:
str33
指向字符串常量池中的“abc”,所以str33 == str22
返回true
JDK1.6
结果与JDK1.8一致
总结
上面两个例子,例一的结果随着JDK的不同而不同,而例二的结果却是在不同的JDK下都一样,这是为什么呢?
例一中:
String str1 = new String("123") + new String("456");
例二中:
String str11 = new String("abc");
所以在各自对象调用intern()
方法时,条件是不一样的,前面一个此时常量池中还没有“123456”,而后者常量池中此时已经有了“abc”对象了。如果已经有了,那么虽然JDK不同,但是表现却都是一样,即返回已有字符串对象的引用;如果没有,那么不同的JDK表现却是不同:JDK1.7之前,直接在字符串常量池中创建字符串对象;JDK1.7及以后,则直接返回堆中的字符串引用;为什么会这样设计?
JDK1.7及以后,字符串常量池直接实现在堆内存中,而不是之前的永久代(HotSpot),此时new出来的对象和常量池都在堆中,所以字符串常量池中既可以存储字符串对象,也可以存储字符串对象的引用。
对于JDK1.7及以后:
String s = "111";
如果此时常量池中不存在“111”字符串对象,则在常量池中创建一个字符串对象;
String s = s1.intern();
如果此时常量池中不存在与s1
相等(equals
)的字符串对象,则在常量池中引入一个s1
对象的引用,并返回给s
;
从上述的例子代码可以看出,jdk7版本对intern()
操作和常量池都做了一定的修改。主要包括2点:
- 将String常量池从Perm区移动到了Java Heap区
- 调用String#intern()方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
八、参考
String.intern() 方法__jdk1.6与jdk1.7的不同
JVM——Java虚拟机架构
字符串常量池、class常量池和运行时常量池
深入解析String#intern