Java基础学习笔记:JVM
JVM
1. 类加载过程
1.1 编译
作用:将java源码文件编译成class文件
操作内容:对源代码程序做语法分析、语义分析、注解处理(Lombok)、泛型擦除等
过程
- 词法分析:一个个字节地读取源代码,识别语言关键字,得到Token流
- 语法分析:检查Token流中关键字组合是否符合Java语法规范,形成抽象语法树(结构化的语法表达形式)
- 语义分析:将复杂语法转化为简单语法,如注解处理、泛型擦除、注释等,得到注解抽象语法树
- 字节码生成:根据注解抽象语法树生成符合Java虚拟机规范的字节码
1.2 加载
作用:将class文件加载到JVM中
步骤:
-
装载
-
时机:有需要时才进行装载(new或反射),节省内存开销
-
操作:通过类加载器装载,防止内存中出现多份同样的字节码,使用双亲委派机制
不会自己尝试加载这个类,而是把请求委托给父加载器,依次向上
-
规则:本地方法由根加载器(BootstrapLoader)装载,内部实现的扩展类由扩展加载器(ExtClassLoader)装载,程序中的类由系统加载器(AppClassLoader)装载
-
内存角度:查找并加载类的二进制数据,在堆中创建一个java.lang.Class对象,类相关信息存储在方法区中
-
-
连接:对Class对象信息进行验证、为类变量分配内存空间并赋默认值
-
步骤:
-
验证:验证类是否符合Java规范和JVM规范
-
准备:为静态变量分配内存,初始化为默认值
-
解析:将符号引用转为直接引用
符号引用:可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,与虚拟机布局无关
直接引用:与虚拟机布局相关,已经被加载到内存中,避免不同虚拟机对于相同符号引用翻译出的直接引用不同
-
-
-
初始化
-
为静态变量赋上正确的初始值
-
收集类中的静态变量、静态代码块、静态方法、构造方法,从上往下开始执行
-
1.3 解释
作用:将class字节码文件转换为操作系统识别的指令
字节码解释器
- 非热点代码:直接解释
- 热点代码:编译,交给即时编译器
- 热点探测:方法调用计数器、回边计数器。达到阈值后,触发JIT编译
即时编译器JIT
- 保存热点代码的指令码,下次执行就无需重复解释,直接执行缓存的机器语言
1.4 执行
操作系统执行指令码
1.5 方法执行顺序
- 父类静态代码块
- 子类静态代码块
- 父类代码块
- 父类空参构造器
- 子类代码块
- 子类调用构造器
总结:静态 > 继承关系 > 代码块 > 构造器
默认子类构造器都会调用父类空参构造,如果父类无空参,子类必须说明使用的构造器
代码块早于构造器:提取各个构造器中公用部分,减少冗余
2. 双亲委派机制
2.1 类加载器
BootstrapClassLoader(启动类加载器)
- 最顶层的加载类,由C++实现
- 负责加载
%JAVA_HOME%lib
目录下的jar包和类,或者被-Xbootclasspath
参数指定的路径中的所有类
ExtensionClassLoader(扩展类加载器)
- 由Java实现,继承自
java.lang.ClassLoader
- 负责加载
%JAVA_HOME%/lib/ext
目录下的jar包和类,或者被java.ext.dirs
系统变量所指定的路径下的jar包
AppClassLoader(应用程序类加载器)
- 由Java实现,继承自
java.lang.ClassLoader
- 面向用户的加载器,负责加载当前应用classpath下的所有jar包和类
2.2 双亲委派模型
- 在类加载时,系统首先判断当前类是否被加载过
- 已经被加载过的类会直接返回
- 否则尝试自己加载
- 加载时,首先将该请求委派给父类加载器的loadClass()处理,即所有请求最终都会传送到顶层的BootstrapClassLoader
- 当父类加载器无法处理时,才自己加载
- 当父类加载器为NULL时,使用BootstrapClassLoader作为父类加载器
3. 内存结构(运行时数据区)
3.1 线程共享
3.1.1 堆
目的:
- 存放对象实例,几乎所有对象实例和数组都在此分配内存(逃逸分析)
- 分代便于垃圾回收
新生代(占堆的1/3)
-
Eden区(占新生代8/10):存放新对象
-
Survivor区(占新生代2/10):存放垃圾回收对象
-
from(占新生代1/10)
-
to(占新生代1/10)
-
老年代(占堆的2/3)
(进入老年代条件)
对象太大
- 新创建就很大的对象
- Survivor区无法存下的对象
- 需要大量连续内存空间的对象:字符串、数组
对象太老
- 经过MinorGC15次的对象
- HotSpot:15
- CMS:6
- 动态年龄判断:Survivor区中某个年龄占用内存超过50%时,取这个年龄和原来晋升年龄的较小值
对象头信息
- 存储对象自身运行时数据:哈希码、GC分代年龄、锁状态标识位、线程持有的锁、偏向锁ID、偏向时间戳
- 类型指针:JVM通过指针来确定该对象是哪个类的实例
3.1.2 方法区(逻辑上属于堆)
存放信息
类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等
内存结构
- 类型信息、字段信息、方法表
- JIT代码缓存
- 运行时常量池:字面量、符号引用
- 字符串常量池
- 静态变量
历史演变
JDK版本 | 变化 |
---|---|
JDK6 | 都在方法区中 |
JDK7 | 静态变量、字符串常量池移到堆中 |
JDK8 | 方法区具体实现由永久代变为元空间 |
实现方式
JDK版本 | 内存位置 | 区别 |
---|---|---|
JDK8以前 | 永久代(虚拟机中) | 可能内存溢出,OOM |
JDK8以后 | 元空间(本地内存) | 不会内存溢出 |
为什么要把永久代替换为元空间?
实现方式 | 描述 | 调整参数 |
---|---|---|
永久代 | 使用JVM内存,虽然可以调整,但有最大上限,容易溢出 | -XX: PermSize=N -XX: MaxPermSize=N |
元空间 | 1. 使用机器直接内存,可以调整,更难溢出,可以加载更多类的信息 2. 合并HotSpot和JRockit代码时,JRockit没有永久代 |
-XX: MetaspaceSize=N -XX: MaxMetaspaceSize=N |
3.2 线程私有
3.2.1 虚拟机栈
栈帧(每次方法调用创建)
- 局部变量表
- 操作数栈
- 方法返回值
- 动态链接
3.2.2 本地方法区
本地方法:非Java方法,通常是C
3.2.3 程序计数器
记录各个线程执行的字节码的地址
分支、循环、跳转、异常、线程恢复
4. 垃圾回收机制
4.1 垃圾
垃圾定义
只要对象不再被使用了,就认为该对象是垃圾,对象所占的空间就可以被回收
常见垃圾
-
废弃常量:没有任何对象引用该常量
-
无用类
-
该类的所有实例对象都被回收
-
该类的类加载器已被回收
-
该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类
-
垃圾判断算法
-
引用计数法
- 对象被引用则加1,引用失败则减1
- 计数器为0时,说明对象不再被引用,可以被回收
- 缺点:无法回收循环依赖对象(A依赖B,B依赖A)
-
可达性分析法
- 从GC Roots开始向下搜索
- 若没有任何引用相连时,说明对象不可用,可以回收
- GC Roots是一组必须活跃的引用,从此处出发,程序通过直接或间接引用,能找到可能正在被使用的对象
-
GC Roots
-
虚拟机栈(栈帧的局部变量表)中引用的对象:若此栈帧为栈顶,证明活跃,对象也正被引用,即为GC Roots
-
本地方法栈中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
所有被同步锁持有的对象
-
4.2 垃圾回收算法
标记-清除算法
过程
- 标记没有被GC Roots引用的对象
- 直接清除没有被标记的对象
特点:最基础算法、存在问题
- 效率问题:每次需要对整个内存区间进行回收
- 空间问题:产生大量不连续的内存空间,内存碎片
标记-复制算法
过程
- 将内存分为大小相同的两块,每次使用其中一块
- 当这块内存使用完成后,就将还存活的对象复制到另一块去,然后再把这块使用的空间一次清理掉
特点
- 解决效率问题:每次对内存区间的一半进行回收
- 新的效率问题:对象存活率高时,需要进行较多复制操作,效率降低
- 如果不想浪费50%的内存空间,就需要提供额外空间进行分配担保
- 一般情况,复制算法将内存分为相等的两块,交替使用
- 为提高内存利用率,将内存空间按照8:1:1划分为三块,即堆中的Eden、Survivor区。每当需要垃圾回收,先将存活对象复制到保留的Survivor,然后将Eden和之前使用的Survivor一起清理
- 由于Survivor内存空间较小,极端情况可能不够保存存活下来的对象,因此需要提供一块额外空间进行分配担保
标记-整理算法
过程
- 标记没有被GC Roots引用的对象
- 不直接清除没有被标记的对象,而是将所有存活对象向一端移动,然后清理掉端边界以外的内存
分代收集算法
根据对象存活周期不同,将内存分为几块(新生代、老生代),选择合适的垃圾回收算法
- 新生代:每次收集都有大量对象死去,选择标记-复制算法
- 老年代:对象存活率较高,且没有其他内存进行分配担保,选择标记-整理算法
4.3 MinorGC
触发时间:Eden区空间不足时
扫描对象(HotSpot虚拟机):年轻代对象
-
从GCRoots出发,通过地址判断对象分代
-
CardTable避免全局扫描老年代对象
-
堆中一小块区域形成卡页
-
一个卡页中存在对象的跨代引用时,将这个页标记为脏页
-
4.4 垃圾回收器
Serial收集器
特点
- 单线程:只使用一条垃圾收集线程,且同时暂停其他所有工作线程,直到收集结束
- 简单高效:没有线程交互的开销
- 适用于运行在Client模式下的虚拟机
ParNew收集器
特点
- 多线程并行:使用多条垃圾收集线程,但仍暂停其他工作线程
- 适用于运行在Server模式下的虚拟机
Parallel Scavenge收集器
特点
- 多线程并行:与ParNew类似
- 高吞吐量:提供许多参数供用户找到最合适的停顿时间或最大吞吐量(CPU中运行程序时间与总耗时的比值)
- JDK1.8默认收集器
CMS收集器
特点
- 多线程并发:垃圾收集线程和用户线程(基本上)同时工作
- 低停顿,提高用户体验
步骤:标记-清除算法
- 初始标记:暂停所有其他工作线程,CMS线程记录下直接与GC Roots相连的对象。速度很快
- 并发标记:同时开启垃圾收集线程和用户线程,用闭包结构标记可达对象(由于用户线程可能会不断更新引用域,所以无法保证可达性分析的实时性。因此也会跟踪记录这些发生引用更新的地方)
- 重新标记:暂停所有其他工作线程,CMS线程修正并发标记期间引用更新对象的标记。停顿时间稍大于初始标记,远小于并发标记
- 并发清除:同时开启垃圾收集线程和用户线程,垃圾回收线程对未标记的区域做清扫
缺点
- 对CPU资源敏感
- 无法处理浮动垃圾
- 使用标记-清除算法,导致收集结束时会有大量空间碎片产生
G1收集器
特点
- 并行与并发:充分利用CPU、多核环境下的硬件优势,缩短StopTheWorld停顿时间
- 分代收集
- 空间整合:从整体上看,使用标记-整理算法;从局部上看,使用标记-复制算法
- 可预测的停顿:低停顿,且建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内
步骤
- 初始标记
- 并发标记
- 最终标记
- 筛选回收