JVM学习-运行时数据区
Java虚拟机运行时数据区
1.程序计数器
线程私有,没有规定OutOfMemoryError。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任意时刻,一条Java虚拟机线程只会执行一个方法的代码,正在被线程执行的方法被称为Current Method。如果方法是Java方法,计数器记录的是正在执行的虚拟机字节码地址;如果方法是Native的,计数器为Undefined。
2.Java虚拟机栈
线程私有,生命周期与线程相同,用于存储栈帧(从实现角度看,也可以在堆中分配),可以是固定大小或者动态扩展和收缩。如果前程请求分配的栈容量超过Java虚拟机栈允许的最大容量,会抛出StackOverflowError;内存不够或者扩展时申请不到OOM。
虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
2.1.栈帧
栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态链接、方法返回值和异常分派,随着方法调用而创建,随着方法结束而销毁,由创建他的线程分配存储空间,线程私有。
2.1.1局部变量表(空间在编译期间完成分配,运行期不会改变)存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。虚拟机不要求double和long类型数据采用64位对齐的方式连续存储在局部变量表里,可以由两个局部变量来存储一个double或long类型的值。
6.2操作数栈 LIFO,栈帧刚创建时,操作数栈是空的。Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或者变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据以及把操作结果重新入栈。操作数栈有一个确定的栈深,long和double占2个单位的栈深。
6.3动态链接 每个栈帧内部都包含一个指向当前方法所在的运行时常量池的引用,以便堆当前方法的代码实现动态链接。
3.本地方法栈
线程私有,StackOverflowError和OOM
虚拟机可以自由的实现本地方法栈,hotspot把本地方法栈和虚拟机栈合并了。
与虚拟机栈的区别是虚拟机栈为Java方法服务,而本地方法栈为虚拟机使用到的native方法服务。
4.Java堆(GC堆)
线程共享,OOM,虚拟机启动时创建,Java虚拟机管理的内存中最大的一块,存放对象实例和数组(随着JIT和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有对象不一定都分配在堆上)。
Java堆可以处于物理上不连续的内存空间中,只要逻辑连续即可。
Java堆被分为新生代(Eden和两个Survivor)和老年代。
1. 为什么需要Survivor空间?如果没有Survivor的话,Eden里的存活对象是不是要放到老年代里,那么这样老年代会容易发生Full GC,通过Survivor的话,因为只有一定年龄(熬过一次Minor GC 年龄加一,可以通过-XX:MaxTenuringThreshold=?来设置进入老年代的年龄)的对象才会从新生代到老年代,所以有效的减少了去老年代的对象。
2. 为什么需要两个Survivor呢?两个Survivor可以减少内存碎片,你需要先了解新生代使用的垃圾回收算法:复制算法。
垃圾回收:https://www.cnblogs.com/KuroNJQ/p/11198814.html
为什么需要两个Survivor空间:https://blog.csdn.net/qq_35181209/article/details/78033329
5.方法区
线程共享,OOM,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据,方法去在虚拟机启动时创建,java虚拟机规范把方法区表述成堆的逻辑部分,SE8版中也不限定实现方法区的内存位置和编译代码的管理策略,大小可以是固定或者动态,物理内存可以不连续。
SE8之前,hotspot用永久代实现方法区(方法去是Java虚拟机规范的,而永久代是具体实现),是的垃圾收集器可以向管理Java堆一样管理方法区。对于其他的虚拟机(BEA JRofkit、IBM J9)是不存在永久代的概念的,永久代更易出现内存溢出问题。其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap(字符串常量池)或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除。在JDK1.8中完全移除了永久代,多了元空间。
以下来自:https://www.cnblogs.com/paddix/p/5309550.html
我们可以通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:
package com.paddx.test.memory; import java.util.ArrayList; import java.util.List; public class StringOomMock { static String base = "string"; public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i=0;i< Integer.MAX_VALUE;i++){ String str = base + base; base = str; list.add(str.intern()); } } }
从上述结果可以看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。现在我们看看元空间到底是一个什么东西?
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
现在我们在 JDK 8下重新运行一下代码段 4,不过这次不再指定 PermSize 和 MaxPermSize。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize的大小。输出结果如下:
5.1运行时常量池
运行时常量池是方法区的一部分,所以会有OOM。Class文件除了类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。
运行时常量池相对于Class文件常量池具备动态性,运行期也可能将新的常量放入常量池,比如String的intern()方法。
解析String.intern()的:https://www.cnblogs.com/Kidezyq/p/8040338.html
6.直接内存
直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,不受Java堆大小的限制,但是受本机总内存(RAM+SWAP区+分页文件),使用频率高,会有OOM。
参考:
《Java虚拟机规范 SE8》
《深入理解Java虚拟机》