1. JVM 是什么
JVM 是 Java 虚拟机的简称,我们在下载 Java 运行环境时(jre),就已经包含了 JVM 了,JVM 是 Java实现跨平台的最核心的部分,所有的 Java 程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行
2. JVM 的位置
JVM 是 Java 虚拟机的简称,其运行在操作系统之上
3. JVM 的体系结构
JVM 由类加载器、运行时数据区、执行引擎和本地方法接口组成。它们之间的关系如下图所示:
4. 类加载器
通过下列代码测试类加载器,输出结果如下,也证明了 JVM 共含三种类加载器 ,分别是: 应用类加载器 AppClassLoader
、扩展类加载器 ExtClassLoader
、根加载器 BootStrap
(因为底层是用 C 写的,所以访问不到)
/**
* @Author: WanqingLiu
* @Date: 2023/02/06/9:49
* 测试 Car 的类加载器
*/
public class Car {
public static void main(String[] args) {
Car car = new Car();
// 通过反射得到 Class 类
Class Car = car.getClass();
System.out.println(Car.getClassLoader()); // ” sun.misc.Launcher$AppClassLoader@18b4aac2 “
System.out.println(Car.getClassLoader().getParent()); // ”sun.misc.Launcher$ExtClassLoader@4554617c“
System.out.println(Car.getClassLoader().getParent().getParent()); // ”null“ —— ? 为什么输出是null : 因为底层是用 C 写的, Java 代码访问不到 ?
}
}
5. 双亲委派机制
双亲委派机制是类加载器在工作时(加载类时)所要遵循的工作模型。双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它会把请求委托给父加载器去完成,直至类加载请求被传递到顶层的启动类加载器中。只有当父加载器在它的搜索范围中没有找到所需的类时,子加载器才会尝试自己去加载该类。
通过双亲委派机制 JVM 类加载器工作过程如下:
- 当 AppClassLoader 加载一个类时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给它的父类加载器 ExtClassLoader 去完成
- 当 ExtClassLoader 加载一个类时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给 BootStrap ClassLoader 去完成。
- 如果BootStrap ClassLoader加载失败,会使用 ExtClassLoader 来尝试加载;
- 若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败,则会报出异常 ClassNotFoundException。
验证双亲委派机制,下面的代码不能正常允许:
package java.lang;
/**
* @Author: WanqingLiu
* @Date: 2023/02/06/10:19
*/
public final class String {
public static void main(String[] args) {
System.out.println("我是自定义的 String 类");
}
}
6. 沙箱机制
沙箱机制是 Java 安全模型的核心,沙箱就是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在 JVM 特定运行范围中,并且严格限制代码对本地资源的访问。( 沙箱机制的出现主要是因为 Java 当时想和其他语言竞争,所以得有比其他语言好的地方,因此就提出了沙箱机制)
经过 Java 多代的升级,沙箱机制演化为下图所示的 Demain 域的概念:
沙箱机制可以保证安全的原因:
- 字节码校验器: 系统类防止内存中出现多份同样的字节码
- 类装载器:采用双亲委派机制使得外层同名恶意类不得使用
- 存储控制器:控制核心 API 对系统的操作权限,用户可自己指定
- 安全管理器:核心 API 和操作系统间的主要接口,实现权限控制
- 安全软件包:java.security 包下的类,允许用户自行定义安全特性
7. native 关键字
native 关键字修饰的方法表示本地方法,带了 native 关键字,Java 的作用范围就达不到了,表示需要调用底层 C 语言写的库
调用方式为 JVM 运行时数据区的本地方法栈 调用 JVM 提供的本地方法接口 JNI (Java Native Interface )
那从上面我们也可以看出,JNI 的作用为 扩展 Java 语言的使用,融合不同的编程语言为 Java 所用。 现在在企业应用中很少见 JNI 的使用,但是驱动硬件的时候很多见(比如: Java 驱动打印机)
以 Thread 类中的 start0 方法为例,理解 native 关键字 :
8. PC 寄存器
每个线程都有一个私有的程序计数器,其指向方法区中的方法字节码(即指向即将要执行的指令的执行代码),其是一个非常小的内存空间,几乎可以忽略不计
9. 方法区
方法区是 JVM 内存中的一种,其被所有线程所共享,方法区中保存静态变量、常量和类信息(如构造函数、接口代码)。
10. 栈
栈内存主管程序的运行,其生命周期和线程同步,线程结束,栈内存释放。因此,对于栈来说,不存在垃圾回收问题。
栈内存中存放 8大基本数据类型 、对象引用 和 实例的方法 等
用栈解释为什么 main 方法最后执行
如下图所示:因为 main 方法最先入栈,因此最后执行
栈运行实例的方法原理:通过 栈桢,如下图所示
栈溢出 StackOverError :递归时常出现的问题,因为方法一直入栈,不能出栈执行,造成了栈溢出
11. 三种 JVM
- HotSpot —— 我们都是相对 HotSpot 说
- JRockit
- J9VM
12. 堆
堆 Heap,一个 JVM 只有一个堆内存,所有线程共享这一个堆内存。堆中存放所有对象的实例、类、方法、常量、变量等并保存所有引用类型的真实变量。
堆中详细区域分为如下:
- 新生区: 包括伊甸园区、幸存者 0 区 和 幸存者 1 区
- 养老区
- 永久区 —— JDK 1.8 后改名为 元空间,其中含有 方法区,因此方法区也是堆的一部分
堆中内存分配如下图所示:
元空间(永久代)逻辑上存在,但是物理上不存在 —— 它属于堆,但是真实计算物理区域,只有新生代和老年代
通过如下代码进行测试:
/**
* @Author: WanqingLiu
* @Date: 2023/02/05/18:49
* Xms 初始化内存分配大小
* xmx 最大分配内存
* -XX:+HeapDumpOnOutOfMemoryError
* -XX:+PrintGCDetails
*/
public class Demo01 {
public static void main(String[] args) {
// jvm 试图使用的最大内存 —— 默认的总内存是电脑内存的 1/4
long max = Runtime.getRuntime().maxMemory();
// jvm 的总内寸 —— 是最大内存的 1/64
long total = Runtime.getRuntime().totalMemory();
System.out.println("max = " + max + "字节\t" + max/(double)1024/1024 + "MB");
System.out.println("total = " + total + "字节\t" + total/(double)1024/1024 + "MB");
}
}
通过上述代码可得, 元空间只是逻辑上存在
13. 新生区、老年区
新生区 : 类诞生和成长的地方,甚至死亡
- 伊甸园区:类诞生的地方,所有对象都是在伊甸园区 new 出来的
- 幸存者区:分为 from 区 和 to 区,两者可交换,用于垃圾回收
14. 永久区
永久区是常驻内存的,存放 JDK 自带的 Class 对象,Interface 元数据 —— 即 java 运行时的一些环境或者类信息,这个区域不存在垃圾回收,关闭虚拟机时释放该区域内存
- JDK 6 : 永久代,常量池在方法区中
- JDK 7 :永久代,但是在退化,常量池在堆中
- JDK 8 :无永久代,变为元空间,常量池在元空间
15. 堆内存调优
堆内存可能会出现 OOM 故障(即 Out Of Memory 错误,表示堆内存满了),这时我们就需要进行堆内存调优,快速定位出现错误的位置
当一个启动类加载大量第三方 Jar 或者 Tomcat 部署太多应用, 产生大量反射类,就可能会出现 OOM
解决:
- 调整 堆 内存空间,看结果
使用 IDEA 调整 堆内存大小方法如下:
加入 VM option 选项,填入 -Xms1024m -Xmx1024m -XX:+PrintGCDetails
,其中 -XX:+PrintGCDetails
表示打印垃圾回收过程
- 分析内存,看一下那些地方出现了问题
在项目中突然出现了 OOM 故障 , 调错方式:
- 使用内存快照分析工具 —— MAT、JProfiler
使用内存快照分析工具可以让我们直接看到代码第几行出错 ——
IDEA 整合 JProfiler 方式为:
- IDEA 下载 JProfiler 插件 —— 下载好要重启
- 本机下载 -Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError 客户端:https://www.ej-technologies.com/products/jprofiler/overview.html
- VM Options 加入如下选项
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
, 表示拍摄内存快照 - 使用 JProfiler 客户端打开拍摄的快照,进行分析
- Dedug , 一行行分析代码
16. GC —— 分代收集算法
垃圾回收在堆中,即 GC 的作用区域在堆中
JVM 进行 GC 的堆内存位置:
- 新生代
- 幸存区 (from to)—— from to 是交换的
- 老年区
按照 GC 程度划分的 GC 两种类型
- 轻 GC :即普通 GC
- 重 GC:即全局 GC
16.1 引用计数法
16.2 复制算法
将 幸存者区,分为 from 区 和 to 区,遵循谁空谁是 to 区的原则,两者间可进行交换,复制算法执行过程为:
- 每次 GC 将 Eden 区存活的对象移动到幸存区中,一旦伊甸园区被 GC , 伊甸园区为空的
- 复制算法保证 to 区永远是空的 (复制方式为:每次将 GC 后存活的对象复制到 to 区,然后 to 区变为 from 区,将原 from 区清空,变为 to 区)
- 当一个对象经历了15次(可以调整)GC,就进入养老区
好处:没有内存碎片
缺点:浪费内存空间(多了一半 to 区永远是 null)
16.3 标记清除算法
缺点:两次扫描,严重浪费时间,而且会产生内存碎片(非连续的空间)
优点:不使用额外空间
16.4 标记压缩算法
在标记清除算法基础上再加一次扫描,向一端移动存活的对象,从而清除内存碎片
—— 优化:清除 n 次,压缩 1 次
总结 GC 算法:
内存效率 : 复制 > 标记清除 > 标志压缩
内存整齐度:复制算法 = 标记压缩 > 标记清除
内存利用率: 标记压缩 = 标记清除 > 复制算法
没有最好的算法,但是有针对与每个常见场景的最合适的算法
年轻代:存活率低, 使用复制算法
老年代:存活率高,区域大,使用标记清除加标记压缩混合实现 —— JVM 调优
17. JMM
Java Memory Medol
- 什么是 JMM: JMM 是一个抽象的概念,一个缓存一致性协议,其定义了读写的规则,
JMM 定义了线程工作内存和主内存中的抽象关系,解决共享对象的可见性问题(volatile) - JMM 的功能: 官方 博客 视频
- JMM 如何学习
最后:JVM 面试题
- JVM 的内存模型和分区 - 详细到每个区放什么
- 堆里面的分区有那些, Eden、 from 、to 、老年区的特点
- GC 算法 用法和原理
- 轻 GC 和 重 GC 分别在什么情况下发生
- 谈谈你对 JVM 的理解 ? Java 8 虚拟机和之前的变化
- 什么是 OOM,什么是栈溢出,怎么分析
- JVM 的常用调优参数
- 内存快照如何抓取,怎么分析 Dump 文件
- 堆 JVM 类加载器的认识