一、简介
简介:虚拟机主要用于自动内存管理
- JVM是指JAVA平台的虚拟机,如:HotSpot、JRockit
- DVM和ART都是Android平台的虚拟机,在Android 5.0以后使用ART替代DVM,
为什么DVM会被ART替代?
- ART采用了AOT静态编译,提高了启动速度和运行效率;
- ART对垃圾回收机制进行了优化,采用了并发设计,减少了垃圾回收过程中的停顿时间;
- ART支持64位处理器,而DVM主要针对32位处理器设计;
二、编译过程
2.1 通过javac命令编译成.class字节码文件
- Android中会再次使用ProGuard混淆优化.class文件
- 可以使用010editor工具查看
- 通过javap命令打印堆信息,并通过JVM命令手册查看指令集含义
2.2 通过dx命令编译成.dex字节码文件
- D8命令用于取代dx命令
- R8命令用于取代ProGuard+D8
三、类装载过程
加载->验证->准备->解析->初始化->使用->卸载
3.1 加载
类加载器:将dex字节码文件加载到JVM中
- 启动类加载器: 由C++编写,负责加载lib目录下Java核心库,直接嵌入到JVM中
- 扩展类加载器: 负责加载ext扩展目录
- 应用类加载器: 负责加载应用程序的类路径(
classpath
)上的类 - 自定义类加载器: 负责加载继承ClassLoader的自定义类
双亲委派模型: 当一个类加载器收到类加载请求时,它首先会将请求委托给其父加载器去完成,如果父加载器无法完成加载任务,子加载器才会尝试自己加载。这种模型确保了Java核心库的安全性和类的唯一性,防止了核心类被重复加载和篡改。 沙箱安全
3.2 验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
3.3 准备
- 为类变量分配内存和设置初始值
- 静态变量如果是final的基本类型,则在准备阶段赋值
- 静态变量如果是final的引用类型,则在初始化阶段赋值
3.4 解析
执行引擎:把类中的符号引用转换为直接引用
- JIT动态编译:基于栈的架构
- AOT静态编译:基于寄存器的架构,Android 7.0后,第一次启动使用JIT,空闲时对应用AOT编译
3.5 初始化
- 对静态代码块和静态变量赋值
- 先初始化父类
3.6 使用
从main方法开始执行
3.7 卸载
执行完毕后,开始消耗Class对象
四、虚拟机组成
也称为运行时数据区/内存模型
4.1 堆
- 年轻代
特点:生命周期较短,主要存储对象和数组 分三个区:Eden(80%)+2个Survivor(S0、S1)
晋级老年代方式:
年龄达到阈值:gc>=15不被回收
担保机制:担保机制的作用是保证即使在极端情况下,例如老年代空间不足,仍然能够确保新生代中的对象能够顺利晋升到老年代,从而避免因为老年代空间不足而导致的垃圾回收失败或者频繁触发 Full GC
动态年龄判断:在survivor区中,所有年龄的对象的所占空间的累加和大于survivor空间的一半,大于或等于该年龄的对象,都可以进入老年代。
大对象humongous:由-XX:PretenureSizeThreshold参数决定
- 老年代:生命周期较长
old分区
4.2 虚拟机栈
- 每个线程运行时所需要的内存称为虚拟机栈,先进后出
- 默认1024k内存,栈帧内存并非越大越好,因为内存是有限的,过大会导致线程数变少
- 避免递归调用导致爆栈,利用尾调用优化
- 主要存储局部变量和方法
- 栈帧
- 由多个栈帧组成,每个栈帧对应每次方法调用时所需要的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
- 默认是线程安全的,如果有参数或者返回值则不是安全的
- 栈帧组成
- 局部变量
- 操作数栈
- 动态链接/参数
- 方法出口/返回地址
4.3 本地方法栈
- 本地方法栈用于执行本地方法,即那些使用Java本地接口(JNI)或其他技术编写的非Java方法,如C或C++
- 每个线程都有自己的本地方法栈,用于支持本地方法的调用和执行
4.3 程序计数器
- 线程私有的,每个线程有一个程序计数器
- 记录执行到的字节码行数,用于挂起后恢复继续执行
4.4 方法区/元空间/永久代
永久代:Java8之前方法区是在堆内存永久代中,被废弃,因为可能导致OOM
-
元空间
存储:静态变量、常量、类结构信息
-
运行时常量池
类加载时,它的常量信息就会放入运行时常量池,并把里面的符号地址变为 真实地址
4.5 直接内存/操作系统内存
- 直接内存是Java NIO(New Input/Output)中引入的一种内存类型,它允许Java程序直接在Java堆之外的本地内存中分配缓冲区。
- 直接内存的分配和回收不需要JVM垃圾回收器的干预,因为它不被视为GC堆的一部分。
- 直接内存可以通过ByteBuffer.allocateDirect()方法进行分配
- IO有2个缓存区(系统缓冲区和Java缓冲区),NIO是共享缓冲区,回收成本高
五、垃圾回收
指的是堆内存回收,当栈帧弹栈以后,栈内存会自动释放
5.1 标记算法
使用可达性分析(GC Roots)算法来标记
- 强引用: 没有引用时,GC才能回收
- 软应用SoftReference: 内存不足时,GC才回收
- 弱引用WeakReference: 只要GC就回收
- 虚引用PhantomReference: 配合ReferenceQueue引用队列使用,主要作用是跟踪对象何时被垃圾回收器标记为“可回收”的状态
5.2 回收算法
- 标记清除: 标记所有活跃对象,然后清除所有未标记的对象
- 标记整理: 重新排列存活对象,消除内存碎片
- 标记复制: 当“from”区中的对象被认为是垃圾时,垃圾回收器会将所有存活的对象复制到“to”区
分代收集算法:
- 年轻代:复制算法
- 老年代:标记清除或标记整理
5.3 垃圾收集器
-
Serial 串行垃圾收集器
整个过程在单线程中执行,会暂停应用线程(Stop-The-World,STW)。
- 标记:遍历对象图,标记所有从GC Roots可达的对象。
- 清除:清除所有未标记的对象。
- Parallel 并行垃圾收集器
适用于多核处理器,减少STW时间
- 标记:与Serial类似,但在会分成多个区域多线程中执行。
- 清除:清除所有未标记的对象。
- CMS(Concurrent Mark Sweep) 并发标记清除垃圾收集器
旨在减少垃圾回收过程中的停顿时间,特别是减少老年代的回收停顿。
- 初始标记:快速标记从GC Roots直接可达的对象。
- 并发标记:并发地遍历对象图,标记所有存活对象。
- 最终标记:修正并发标记阶段可能遗漏的对象。
- 并发清理:回收标记为死亡的对象,并尝试减少内存碎片。
- G1(Garbage-First) 垃圾优先收集器:采用标记复制算法,分区回收
旨在提供可预测的停顿时间,同时优化大堆内存的性能。
- 初始标记:快速标记从GC Roots直接可达的存活对象。
- 并发标记:并发地遍历堆中的对象图,标记存活对象。
- 最终标记:修正并发标记阶段可能遗漏的对象。
- 筛选回收:根据回收价值和成本,选择一部分区域进行回收,以满足停顿时间的目标。
- 清理:回收选定区域中标记为死亡的对象。
- ZGC低延迟垃圾收集器
目标是实现低延迟回收,适用于大堆内存。
- 并发标记:使用染色指针技术并发地标记存活对象。
- 并发清理:并发地回收标记为死亡的对象。
- 压缩:必要时,压缩堆以减少内存碎片。
- Shenandoah 低延迟垃圾收集器
六、实战
6.1 内存泄漏检测
利用弱引用实现