JVM的一些知识
JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java 虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。常见的虚拟机:JVM、VMwave、Virtual Box。
JVM 和其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进
行了裁剪。
JVM 是一台被定制过的现实当中不存在的计算机。
JVM 类加载的过程
- 加载
Java程序的 .Java 文件, 通过 javac 编译成 .class 文件, 存储在硬盘上, 当运行 Java 进程的时候, jvm 需要读去 .class 文件里面的内容, - 验证
验证读到的 .class 文件的数据是否正确, 是否合法 (在 Java 的标准文档中, 明确定义了 .class 文件的格式是什么样的) - 准备
根据读取到的内容尾类的静态变量分配内存, 将其设置为初始值比如 boolean 就设置成 false, 对象引用就设置成 null, 不会进行赋值的操作
(创造一个内存空间, 全部设为初始值) - 解析
Java虚拟机将常量池内的符号引用替换成了直接引用 (符号引用相当于就是一个名字, 比如 String s = “hello” 符号引用类似 hello, 直接引用可以理解成内存地址比如 0x19) - 初始化
针对类对象做最后的初始化操作, 执行静态成员的赋值语句 (此时静态代码块以及父类也会在这一阶段被加载)
死亡对象的判断算法
1. 引用计数法
给对象增加一个计数器, 每当有一个地方引用这个对象, 计数器就 +1, 当引用失效的时候就 -1, 一旦对象的计数器变成了 0, 就代表失效
但是主流的 jvm 都没有使用引用计数法, 主要是无法解决循环引用的问题
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() { Test test1 = new Test(); Test test2 = new Test(); test1.instance = test2; test2.instance = test1; test1 = null;
test2 = null;
// 强制jvm进行垃圾回收
System.gc(); }
public static void main(String[] args) {
testGC(); }
}
比如以上的情况, 就会发生无法回收的情况, 但是其实也引入了回路检测的算法, 可以解决这种问题
2. 可达性分析算法
通过一系列成为 GC Root 的对象, 进行不断向下搜索
(类似 jvm 手上有一份名单, 然后所有的 root 进行向下搜索, 如果发现有无法到达的对象, 即可说明该对象是不可用的)
GC Root 有很多个比如
- 栈上的局部变量
- 元数据区的静态变量
- 常量池引用指向的对象
垃圾回收算法
1. 标记清除算法
先标记再清除, 根据上述可达性算法, 先从所有的 GC Root 遍历一遍, 标记为存活对象, 之后遍历清除没有被标记的对象, 从而回收内存
优点
- 实现简单
- 无需移动对象
缺点
清除后会产生内存碎片, 导致内存的利用率变低
2. 复制算法
内存区域直接划分成两块, 只使用其中一块, 单进行垃圾回收的时候, 将存活对象从当前区域, 直接移动到另一块区域, 对当前区域进行整体回收操作
优点
空间碎片减少
缺点
- 空间利用率较低
- 对象多的时候, 复制成本大
3. 标记整理算法
一样通过 GC Root 对所有对象进行可达性的判断, 标记一下对象是否存活, 对存活的对象进行整理, 连续排列, 清理出连续的空间
优点
解决了内存碎片的问题
缺点
移动整理对象会产生搬运的开销
4. 分代回收算法
解析
- 分成三个区, 分别是伊甸区、幸存区、老年区
- 开始 new 出来的对象, 都会先放在伊甸区 (伊甸区是比较大的) , 根据经验规律 90% 的对象都是活不过第一轮 GC, 所以剩下活下来的会放到其中一个幸存区, 然后清空另一个幸存者区和伊甸区, 下一轮对象加入伊甸区再次 GC 后, 将幸存的对象和存放上一轮 GC 的存活对象放入到另一个幸存者区, 然后回收伊甸区和另一个幸存区的空间
- 当经历过好几轮的 GC 之后, 就会把多轮存活的对象转移到老年代
- 老年代的 GC 频率相较伊甸区的 GC 频率要低很多
优点
- 提高了回收的效率
- 减少了 STW 的时间
缺点
堆内存进行分区管理, 较为复杂
双亲委派模型
输入: 类的全限定名 , 类似于 java.lang.String
目的
防止用户写的类, 把标准库的类给覆盖掉, 保证标准库的类优先级最高, 扩展库其次, 第三方库的优先级最低
JVM内存区域的划分
1. 程序计数器, 保存了下一条要执行的指令的地址 (下一条指令是 Java的字节码)
2. 堆, jvm 最大的空间, new 出来的对象都在堆上
3. 栈
1. Java 虚拟机栈, 运行 Java 代码的方法调用关系, 存储函数中的局部变量, 函数的形参, 函数之间的调用关系
2. 本地方法栈, jvm 中 c++ 代码的函数调用关系
4. 元数据区(方法区), 代码中涉及到的类信息, 以及类的 static 属性, 静态变量
堆(线程共享)
堆是所有线程共享的, 分为新生代和老生代, 在堆上的 GC 操作在上述的分代算法有介绍
方法区(元数据区) (线程共享)
用来存储被虚拟机加载的类信息、静态变量等数据
Java虚拟机栈(线程私有的)
每个方法在执行的时候都会创建栈帧存储局部变量, 方法出口等等, 常说的堆内存, 栈内存指的就是虚拟机栈
- 局部变量表: 存储方法参数和局部变量
- 操作栈: 每个方法会生成一个先进后出的操作栈
- 动态链接: 指向常量池的方法引用
- 方法返回地址: PC 寄存器的地址
本地方法栈(线程私有)
本地方法栈和虚拟机栈类似, 但是 Java 虚拟机栈是给 jvm 使用的, 本地方法栈是给本地方法使用的
程序计数器(线程私有)
用来记录当前线程执行的行号
当前线程如果执行的是一个 Java 的方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果执行的是一个 Native 方法, 计数器的值为空 (因为调用的是其他语言的代码, 计数器并没有意义)
!!! 计数器是唯一一个在 jvm 规范中没有规定任何 OOM 情况的区域 (也就是内存溢出)