一、Java进阶-JVM
1.1 JVM运行时数据区
- 程序计数器:指向下一行我们需要执行的代码。任何时候,每个Java虚拟机都在执行单个方法的代码,即该线程的当前方法。如果该方法不是Native方法,即PC寄存器会记录当前正在执行的java虚拟机指令的地址,如果线程当前执行的方法是本地的,那么java虚拟机的PC寄存器的值就是Undefined。程序计数器是唯一一个不会出现OOM的区域。
- 堆:堆是所有线程共享的,主要是存放对象实例和数组。处于物理上不连续的内存空间,只要逻辑连续即可。
- 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 虚拟机栈:虚拟机栈是线程独有的空间,每个线程都有一个与线程同时创建的私有的虚拟机栈。虚拟机栈中存储栈帧,每个被线程调用的方法都会产生一个栈帧,栈帧中保存一个方法的状态信息,例如局部变量、操作数帧、方法出口等。调用一个方法就是执行一个栈帧的过程,一个方法调用完成,对应的栈帧就会出栈。
- 本地方法栈:本地方法栈类似虚拟机栈,区别就是本地方法栈存储的是Native方法。
1.2 对象内存模型
public class HeapMemory {
private Object obj1 = new Object();
public static void main(String[] args) {
Object obj2 = new Object();
}
}
obj1是类的属性,引用在方法区中,obj2是局部变量,引用存放在虚拟机栈中的栈帧里的局部变量表。所以obj1是方法区指向堆,obj2是经典的虚拟机栈指向堆。
对象内存中分为三块:对象头(Header),实例数据(Instance Data)和对齐填充。
对象访问方式:
- 句柄访问:使用句柄访问,Java虚拟机会在堆内划分出一块内存来存储句柄池,那么对象引用当中存储的就是句柄地址,句柄池中才会存储对象实例数据和对象类型的数据地址。
- 直接指针访问:直接指针访问,对象引用直接指向的就是对象实例数据。
对象生命周期:
- -Xms 1/64 | -Xmx 1/4
- -XX:NewRatio=2
- -XX:SurvivorRatio=8
1.3 类加载机制
加载:加载指的是通过一个完整的类或接口名称来获得其二进制流的形式,并将其按照Java虚拟机规范将数据存储到运行时数据区域,类加载主要做三件事:
- 通过一个类的全限定名获得定义此类的二进制字节流。
- 将这个二进制字节流所代表的的静态存储转化为方法区运行时数据结构。
- 在Java堆中生成一个代表此类的java.lang.Class对象,作为方法区中这些数据的访问入口。
双亲委派模型:
自定义类加载器->应用程序类加载器->扩展类加载器->启动类加载器
- BootStrap ClassLoader:rt.jar
- Extention ClassLoader: 加载扩展的jar包
- App ClassLoader:指定的classpath下面的jar包
- Custom ClassLoader:自定义的类加载器
连接:获取类或接口的二进制形式并将其结合到java虚拟机的运行时状态以便执行的过程。连接包括三个步骤:验证、准备和解析。
- 验证:1)文件格式验证 2)元数据验证 3)字节码验证
- 准备:为static变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)
- 解析:将常量池中符号引用替换为直接引用的过程。在使用符号引用之前,它必须经过解析,解析过程中会对符号引用的正确性进行检查。
初始化:初始化其实就是一个赋值的操作,它会执行一个类构造器的
卸载:GC将无用对象从内存中卸载。
1.4 GC
确定对象是否可回收:
- 引用计数法:无法解决相互引用的问题。
- 可达性分析法:可达性分析法就是选择一些称为GC Roots的对象作为起始点,然后从GC Roots开始向下搜索,搜索路径称为引用链,当一个对象不在任何一条引用链上时,就说明此对象是无效对象,可以被回收。
GC Roots:
- Java虚拟机栈内栈帧中的局部变量表中的变量
- 方法区中类静态属性
- 方法区中的常量
- 本地方法栈JNI(即Native方法)中的变量
垃圾回收算法:
- 标记-清除算法
- 标记-整理算法
- 复制算法
- 分代收集算法:新生代采用复制算法,老年代采用标记-整理或标记-清除算法。
垃圾收集器:
- Serial收集器是最基本、最早的收集器,Serial收集器是单一线程,就是在GC的时候STW(Stop The World),暂停所有用户线程,如果GC时间过长,用户可以感到卡顿。Serial Old也是单线程,作用于老年代。
- ParNew是Serial的多线程版本,实现了并行收集,原理跟Serial一致(并行指的是多个GC线程并行,但是用户线程还是暂停,并发指的是用户线程和GC线程同时执行)。ParNew默认开启和CPU个数相同的线程数进行回收。
- Parallel Scavange:新生代收集器,也是复制算法,和ParNew一样并行的多线程收集器,更关注系统的吞吐量(吞吐量=(运行用户代码的时间)/(运行用户代码的时间+GC时间))
- Parallel Old:是Parallel Scavange的老年代版本,因为Parallel Scavange无法和CMS搭配使用,所以只能和Serial Old。自从Parallel Old出现,就有了Parallel Scavange+Parallel Old的组合,这是JDK1.8使用的,注重吞吐量的一组收集器。
- CMS:这是优化GC停顿时间为目标的收集器,并发回收(仍然需要STW,但是时间很短)。通过-XX:+UseMarkSweepGC启用。CMS基于标记-清除算法实现。
- G1:面向服务器的一款垃圾收集器,主要针对于多核处理器的大内存(一般8G以上)机器,可以满足gc的停顿时间且保证吞吐量。
- ZGC:在对吞吐量影响不大的情况下,对任意大小堆收集停顿时间都控制在10ms以内的低延迟。(Jdk11及以上)
https://blog.csdn.net/qq_41931364/article/details/107040928
https://blog.csdn.net/weixin_37335761/article/details/110245500
1.5 运行时常量池
方法区的一部分,用于存储编译生成的字面量(基本数据类型或被final修饰的常量或字符串)和符号引用,类或接口的运行时常量池是在java虚拟机创建类或接口时创建的。在jdk1.6以及之前的版本,Java中的字符串是放在方法区中的运行时常量池内,但是在jdk1.7以后将字符串常量池拿出来放在了堆中。
public class GcDemo {
public static void main(String [] args) {
String str = new String("lonely")+new String("wolf");
System.out.println(str == str.intern());
}
}
- jdk1.6:调用String.intern()方法,会先去检查常量池中是否存在该字符串,如果不存在,则会在方法区中创建一个字符串,而new String()创建的字符串在堆中,两个字符串的地址当然不相等。
- jdk1.7:字符串常量池从方法区的运行时常量池移到了堆中,调用String.intern()方法,首先会检查常量池是否存在,如果不存在,那么就会创建一个常量,并将引用指向堆,也就是说不会再重新创建一个字符串对象了,两者都会指向堆中的对象,所以返回true。
public static void main(String [] args) {
String str = new String("lonely");
System.out.println(str == str.intern());
}
只有一个new String(),在jdk1.7和jdk1.8也会返回false,我们假设一开始字符串常量池没有任何字符串,执行一个new String("lonely")会产生两个对象,一个在堆,一个在字符串常量池。String.intern()先检查字符串常量池,发现存在"lonely"的字符串,所以直接返回,这时候两个地址不一样,所以返回false。
new String("lonely")+new String("wolf")会产生5个对象,2个在字符串常量池,3个在堆。这时候执行String.intern()如果在1.7和1.8中会检查字符串常量池,发现没有lonelywolf的字符串,所以会在字符串常量池创建一个,指向堆中的字符串。但是在jdk1.6中不会指向堆,会重新创建一个lonelywolf的字符串放到字符串常量池,所以才会产生不同的结果。
jdk1.7和jdk1.8实现方法区的区别:
- jdk1.7之前方法区使用永久代实现,方法区大小可以通过参数-XX:PermSize和-XX:MaxPermSize来控制方法区的大小和所能允许的最大值。
- jdk1.8移除了永久代,采用元空间实现,所以在jdk1.8中永久代的参数改成-XX:MetaspaceSize和-XX:MaxMetaspaceSize。元空间和永久代的一个很大的区别就是元空间已经不在jvm内了,直接存储到了本地内存。
1.6 JVM字节码
1.6.1 class文件结构
1.6.2 基于栈的执行引擎
1.6.3 字节码指令
在编译时时能确定目标方法叫做静态绑定,相反地,需要在运行时根据调用者的类型动态识别的叫动态绑定。
invokedynamic其实是一种调用方法的新方式,它用来告诉JVM可以延迟确认最终要调用的哪个方法。一开始invokedynamic并不知道要调用什么目标方法。第一次调用时引导方法(Bootstrap Method)会被调用,由这个引导方法决定哪个目标方法进行调用。
1.7 参考链接
https://juejin.cn/post/6844903892774289421#heading-13
https://juejin.cn/post/6941242430737874974#heading-48
https://juejin.cn/post/6844904048013869064#heading-36
https://juejin.cn/post/6844903636829487112#heading-18