JVM
学习目标:
1、了解JVM的发展史;
2、了解JVM运行原理;
3、掌握JVM基本组成;
4、掌握JVM垃圾回收算法;
5、掌握类加载机制;
6、掌握jmm;
1. JVM简介
☁️ JVM 是 Java Virtual Machine 的简称;
☁️ JVM 虚拟机是指通过软件模拟实现的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统;
☁️ 常见的虚拟机有:JVM,VMwave,Virtual Box;
☁️ JVM和其他两个虚拟机的区别:
⚡️ VMwave 与 VirtualBox 是通过软件模拟物理 CPU 的指令集,物理系统中会有很多的寄存器;
⚡️ JVM 则是通过软件模拟 Java 字节码的指令集,JVM 中只是主要保留了 PC 寄存器,其他寄存器都进行裁剪;
🌴 什么是指令集?通俗的理解, 指令集就是CPU能认识的语言,指令集运行于一定的微架构上, 不同的微架构可以支持相同的指令集,比如Intel和AMD的CPU的微架构是不同的,但是同样支持X86指令集,这很容易理解,指令集只是一套指令集合,一套指令规范,具体的实现,仍然依赖于CPU的翻译和执行;
🌴 什么是寄存器? 寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成;
1.1 JVM发展史
Sun Classic VM
☁️ 在1996年 Java 1.0 版本的时候,Sun 公司发布了一款名为 Sun Classic vm 的 Java 虚拟机,它也是世界上第一款商业Java虚拟机,jdk 1.4 时被完全淘汰;
Exact VM
☁️ 为了解决上一个虚拟机问题,jdk 1.2 时,Sun 提供了此虚拟机;
☁️ Exact 具备现代高性能虚拟机的雏形,包含了以下功能:
⚡️ 热点探测(将热点代码编译为字节码加速程序执行);
当虚拟机发现某个方法或代码块运行的特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code),判断一段代码是不是热点代码的这种行为被称为“热点探测”(Hot Spot Detection)
⚡️ 编译器与解析器混合工作模式;
java的编译器先将其编译为class文件,也就是字节码,然后将字节码交由jvm(java虚拟机)解释执行,所以需要用到解析器
HotSpot VM
☁️ HotSpot 历史:
⚡️ 最初由一家名为 “Longview Technologies” 的小公司设计;
⚡️ 1997年,此公司被 Sun 收购;
⚡️ JDK 1.3 时,HotSpot VM成为默认虚拟机;
☁️ 不管是现在仍在广泛使用JDK6,还是使用比较多的JDK8中,默认的虚拟机都是HotSpot;
☁️ 名称中的HotSpot指的就是它的热点代码探测技术;它能通过计数器找到最具编译价值的代码,触发即 时编译(JIT)或栈上替换;通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡;
JRockit
☁️ JRockit 是专注于服务器端应用,目前在HotSpot的基础上,移植JRockit的优秀特性;
☁️ 它可以不太关注程序的启动速度,因此 JRockit 内部不包含解析器实现,全部代码都靠即时编译器编译后执行; 大量的行业基准测试显示,JRockit JVM 是世界上最快的 JVM ;
☁️ 使用 JRockit 产品,客户已经体验到了显著的性能提高(一些超过了 70% )和硬件成本的减少(达 50%);
☁️ JRockit 面向延迟敏感型应用的解决方案 JRockit Real Time 提供以毫秒或微秒级的 JVM 响应时间,适合财务、军事指挥、电信网络的需要;
J9 JVM
☁️ 全称:IBM Technology for Java Virtual Machine,简称 IT4J,内部代号:J9;
☁️ 市场定位于HotSpot接近,服务器端、桌面应用、嵌入式等多用途JVM,广泛用于IBM的各种Java产品;
Taobao JVM
☁️ 由 AliJVM 团队发布,覆盖云计算、金融、物流、电商等众多领域, 需要解决高并发、高可用、分布式的复合问题,有大量的开源产品;
☁️ 基于 OpenJDK 开发了自己的定制版本AlibabaJDK,简称AJDK;是整个阿里JAVA体系的基石;
☁️ 基于 OpenJDK HotSpot JVM 发布的国内第一个优化、深度定制且开源的高性能服务器版 Java 虚拟机,它具有以下特点:
⚡️ 创新的 GCIH(GC invisible heap) 技术实现了 off-heap ,即将生命周期较长的Java对象从heap中移到 heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收评率和提升GC的回收效率的目的;
⚡️ GCIH 中的对象还能够在多个 Java 虚拟机进程中实现共享;
⚡️ 使用 crc32 指令实现 JVM intrinsic 降低 JNI 的调用开销;
⚡️ PMU hardware 的 Java profiling tool 和诊断协助功能;
⚡️ 针对大数据场景的 ZenGC;
☁️ taobao JVM 应用在阿里产品上性能高,硬件严重依赖 intel 的 cpu ,损失了兼容性,但提高了性能,目前已经在淘宝、天猫上线,把 Oracle 官方 JVM 版本全部替换了;
JVM和《Java虚拟机规范》
☁️ 以上的各种 JVM 版本,比如 HotSpot 和 J9 JVM,都可以看做是不同厂商实现 JVM 产品的具体实现,而 它们(JVM)产品的实现必须要符合《Java虚拟机规范》,《Java虚拟机规范》是 Oracle 发布 Java 领域 最重要和最权威的著作,它完整且详细的描述了 JVM 的各个组成部分;
JVM运行流程
☁️ jvm 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 jvm 是如何执行的呢?
JVM执行流程
☁️ 程序在执行前要先把Java代码转换为字节码(class文件);
☁️ 接着 JVM 需要把字节码通过一定方式的 类加载器(ClassLoader)把文件加载到内存中的运行时数据区(Runtime Data Area);
☁️ 字节码文件并不能直接交给底层操作系统去执行,需要 特定的命令解析器 **执行引擎 (Execution Engine)**将字节码翻译成底层系统指令再交由CPU去执行;
☁️ 翻译过程需要调用其他语言的接口**本地库接口(Native Interface)**来实现整个程序的功能;
JVM 运行时数据区*
☁️ JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称 JMM)完全不同,属于完全不同的两个概念;
☁️ JVM 由五大部分组成:
堆(线程共享)
☁️ 堆的作用:程序中创建的所有对象都在保存在堆中;
☁️ 堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代;新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1) ;
☁️ 垃圾回收的时候会将 Endn 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清除掉;
Java虚拟机栈(线程私有)
☁️ Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法的执行;
☁️ 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数 栈、动态链接、方法出口等信息;
☁️ Java 虚拟机栈中包含了以下 4 部分:
⚡️ 局部变量表:存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用;局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小;简单来说就是存放方法参数和局部变量;
⚡️ 操作栈:每个方法会生成一个先进后出的操作栈;
⚡️ 动态链接:指向运行时常量池的方法引用;
⚡️ 方法返回地址:PC 寄存器的地址;
☁️ 什么是线程私有?
⚡️ 由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令;因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储;我们就把类似这类区域称之为 “线程私有” 的内存;
本地方法栈(线程私有)
☁️ 本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的;
☁️ 调用Java方法,就是创建栈帧,放在线程的Java虚拟机中;调用其他语言的函数就是使用本地方法栈;
程序计数器(线程私有)
☁️ 程序计数器的作用:用来记录当前线程执行的行号的;
1、哪些内存区域是线程共享的?哪些是线程线程私有的?
2、每个区域分别保存什么数据?
3、什么是OOM?哪些区域会发生OOM?
除了程序计数器,都有可能发生OOM;
4、哪些区域能发生垃圾回收?
方法区(线程共享)
☁️ 方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的;也叫永久代(jdk1.7)或元空间(jdk1.8);
⚡️ 常量和静态变量指引用;
☁️ 总结:
内存布局中的异常问题
Java堆溢出
☁️ Java 堆用于存储对象实例,只要不断的创建对象,并且保证 GC Roots 到对象之间有可达路径来避免来 GC 清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常;
☁️ 可以设置 JVM 参数 -Xms:设置堆的最小值
、-Xmx:设置堆最大值
;
☁️ 演示内存溢出:
/**
* JVM 参数为:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
* @author 38134
*
*/
public class Test {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list =
new ArrayList<>();
while(true) {
list.add(new OOMObject());
}
}
}
☁️ 设置参数:
☁️ 运行下列代码:
☁️ 内存溢出:
☁️ 内存溢出和内存泄漏有什么区别?
⚡️ 内存泄漏:线程生命周期太长,导致始终引用一些不使用的数据,这些数据无法回收,导致可用空间越来越少;简单地说就是创建了一块内存却无法将这块内存释放;
⚡️ 内存溢出:所要存储的数据容量大于内存可用容量;
虚拟机栈和本地方法栈溢出
☁️ 由于 HotSpot 虚拟机将虚拟机栈与本地方法栈合二为一,因此对于HotSpot来说,栈容量只需要由-Xss参数来设置;
☁️ 关于虚拟机栈会产生的两种异常
⚡️ 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出 StackOverflow 异常;
⚡️ 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常;
☁️ 观察 StackOverflow 异常(单线程环境下):
/**
* JVM参数为:-Xss128k
* @author 38134
*
*/
public class Test {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
Test test = new Test();
try {
test.stackLeak();
} catch (Throwable e) {
System.out.println("Stack Length: "+test.stackLength);
throw e;
}
}
}
☁️ 出现StackOverflowError异常时有错误堆栈可以阅读,比较好找到问题所在。如果使用虚拟机默认参数,栈深度在多数情况下达到1000-2000完全没问题,对于正常的方法调用(包括递归),完全够用;
☁️ 如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的方式来换取更多线程;
/**
* JVM参数为:-Xss2M
* @author 38134
*
*/
public class Test {
private void dontStop() {
while(true) {
}
}
public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
Test test = new Test();
test.stackLeakByThread();
}
}
☁️ 以上代码运行需谨慎。先记得保存手头所有工作;