JVM(三)-- 运行时数据区
目录
前言
一、PC寄存器(程序计数器)
1. PC寄存器概述
2. 两个问题
二、虚拟机栈
1. 虚拟机栈的常见异常
2. 栈的存储结构和运行原理
3. 栈帧的内部结构
3.1 局部变量表
3.1.1 关于Slot的理解
3.2 操作数栈
3.3 动态链接
3.4 方法的调用
3.5 方法返回地址
三、本地方法栈
1. 本地方法接口
2. 本地方法栈
四、堆
1. 内存细分
2. 年轻代和老年代
2.1 概述
2.2 对象分配的一般过程
2.3 Minor GC、Major GC、Full GC
2.4 总结
3. TLAB(线程私有缓存)
4. 堆空间中常用的参数
5. 逃逸分析
五、方法区
1. 栈、堆、方法区的交互关系
2. 设置方法区大小的参数
3. 方法区的内部结构
3.1 概述
3.2 运行时常量池
4. 方法区演进细节
5. 方法区的垃圾回收
六、总结
前言
运行时数据区的结构如下图:
一、PC寄存器(程序计数器)
1. PC寄存器概述
JVM的PC寄存器是对物理PC寄存器的一种抽象模拟。
2. 两个问题
为什么使用PC寄存器记录当前线程的执行地址呢?
PC寄存器为什么会被设定为线程私有?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法就是为每一个线程都分配一个PC寄存器。否则的话,会出现相互干扰的情况,一旦切换线程,就不清楚上一个线程执行到了什么地方了。
二、虚拟机栈
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应这一次次的Java方法调用。主管Java程序的运行,保存方法的局部变量、部分结果、并参与方法的调用和返回。
1. 虚拟机栈的常见异常
2. 栈的存储结构和运行原理
3. 栈帧的内部结构
3.1 局部变量表
基本数据类型会直接将对应的值存储到局部变量表当中,对于引用数据类型的话,则会将地址存储在局部变量表中。代码中有多少个局部变量,局部变量表的大小就是多少。
3.1.1 关于Slot的理解
3.2 操作数栈
3.3 动态链接
动态链接又被称为指向运行时常量池的方法引用。它主要作用是在运行时将方法调用中的符号引用转换为直接引用。
其工作过程可以进行简单理解:
Java代码进行编译之后,方法调用(例如animal.speak())在字节码中最初只是一个符号引用(就好比一个“联系方式清单”,记录了方法名称和所属类)。当JVM执行到该调用指令后,动态链接会:根据当前对象的实际类型(例如Dog),然后去运行时常量池查找对应的符号引用,然后将其解析为具体的直接引用(即方法在内存中的实际入口地址),最终完成方法调用。
如下图:
3.4 方法的调用
在JVM中,将符号引用转换为调用方法的直接引用方法的绑定机制相关。方法的绑定机制分为静态链接(早期绑定)和动态链接(晚期绑定)。
上图中前四种方法都是非虚方法,其他方法都称为虚方法。
例如父类Animal中有一个普通方法eat,子类Dog继承父类Animal,然后在子类中调用eat这个方法,因为此时编译器不能确定这个方法是父类的还是子类的,所以该方法为虚方法。但是,如果是在子类中调用super.eat,那么这个方法就是非虚方法,因为在编译期间就可以确定这个方法是父类的方法。
3.5 方法返回地址
假如A方法调用B方法,B方法执行结束之后会将下一条需要执行的指令的值返回给A方法。
三、本地方法栈
1. 本地方法接口
利用native关键字来修饰的一个Java方法,就被称为一个本地方法。
目前本地方法使用的越来越少了,除非是与硬件有关的应用。
2. 本地方法栈
Java虚拟机栈用于管理Java方法的调用,本地方法栈用于管理本地方法的调用。
四、堆
1. 内存细分
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
JDK 1.8之前:方法区主要通过永久代(PermGen) 实现,它是堆内存的一部分,大小固定。
JDK 1.8及以后:永久代被彻底移除,被元空间(Metaspace) 取代。元空间不再使用JVM的堆内存,而是使用本地内存(Native Memory)。这使得元空间的大小默认仅受本地内存限制,减少了内存溢出的风险,垃圾回收效率也更高
2. 年轻代和老年代
2.1 概述
Java堆区进一步细分的话,可以划分为年轻代和老年代。其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区和to区)。
新生代存储生命周期短的对象,老年代存储生命周期长的对象。
在新生代中Eden和另外两块区域的比例为8:1:1,但是在实际的使用中的比例却是6:1:1。通常这是因为JVM的自适应内存调整机制导致的。
2.2 对象分配的一般过程
Java中几乎所有的对象都是在Eden区被创建的,但是对象创建较多时,就会将Eden区占满。此时就会触发垃圾回收机制YGC(会把Eden区直接清理干净), 没有被引用指向的对象就会直接被回收,剩下的对象就会被放进Survivor0区,同时给每一个对象分配一个年龄计数器。
之后,Eden区又不断地创建对象,发现区域又满了。此时,回收垃圾对象,然后存活对象和s0中的对象移到空闲的s1区。以后的操作也是如此,s0和s1哪里空闲就将剩余的对象移动到哪里。
在许多次循环往复之后,有一些对象的存活时间到达了我们所设置的阈值。此时,这些对象就会被移动到老年代中。
总结:
下面的示意图表达了完整的对象分配的过程(包含一些特殊情况,例如超大对象等):
2.3 Minor GC、Major GC、Full GC
2.4 总结
3. TLAB(线程私有缓存)
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,将这种内存分配方式称为快速分配策略。
每个线程创建的对象都在各自的TLAB的空间中进行创建,避免了并发安全问题。
如下图所示:
4. 堆空间中常用的参数
5. 逃逸分析
没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。
看new的对象是否有可能在方法外被调用,如果在方法外会被调用,则发生了逃逸。
但是目前的Hotspot的虚拟机并没有在栈上创建对象,而是通过标量替换进行实现的(不可再分的数据就是标量,例如基本数据类型。标量替换就是将引用数据类型中的各个基本数据类型拆分开进行初始化,这样就可以不需要在堆上创建对象实例了)。
五、方法区
1. 栈、堆、方法区的交互关系
栈中存放定义的局部变量person,即指向对象实例的引用。堆中存放对象实例。方法区中存放对象的类型数据,即Person这个类。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,则会报OOM异常。
2. 设置方法区大小的参数
3. 方法区的内部结构
3.1 概述
方法区用于存储已被虚拟机加载的类型信息、常量、即时编译器编译后的代码缓存等。
3.2 运行时常量池
方法区内部包含了运行时常量池。字节码文件内部包含了常量池。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息就是常量池表,包括各种字面量和对类型、域和方法的符号引用。
运行时常量池主要存放的是从Class文件常量池表加载而来的字面量和符号引用,并在运行时解析这些符号引用为直接引用,同时支持动态添加常量(如 String.intern()
方法)
上图中带“#”号的数字,表示的就是需要引用常量池中编号为2以及编号为3的的数据。
常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
4. 方法区演进细节
永久代为什么要被元空间替换?
5. 方法区的垃圾回收
六、总结
运行时数据区可由下面这张图进行概括: