JVM知识点
JVM组织架构:
- 类加载器,负责加载Class文件,将二进制数据读到内存(方法区)
- 运行时数据区,为Java程序的运行分配空间并处理数据
- 执行引擎,将字节码翻译为当前机器的机器指令(执行字节码)
加载过程:
类加载器读取 Class 文件的二进制数据后,会在内存中生成一个代表该类的java.lang.Class对象(这个对象存放在堆内存中),同时将类的元数据(方法、字段、常量池等)存储到方法区(逻辑概念)。
运行时数据区
堆
给所有的对象分配内存的地方,分为新生代和老年代,新生代又有:Eden区,From区, To区之分,大小比例8:1:1
新生代 -> 老年代
长期存活,大对象,动态年龄判断
动态年龄判断:当Survivor区的所有对象大小超过一定比例(一般是Survivor区的一半),那么没到达年龄阈值的对象也可能被提前晋升到老年代
随着JIT(Just In Time)的出现,“所有”不再这么绝对,会有逃逸分析来决定对象是否可以分配在栈上。
逃逸分析
1 | private static void createObject() { |
因为所有的对象都在这里分配,所以也是[[#垃圾回收GC]]的主要场所。
堆和栈的区别:
几乎所有的对象分配在堆中,是线程共享的,被调用之后不会被清除,直到对象不再被引用为止,由GC垃圾回收器做处理。
栈是线程独立的,每个方法的执行会生成栈帧,存放局部变量表,操作数栈,动态链接等信息,会随着方法的结束而被释放。
栈帧结构
new对象的过程

对象的内存布局(Object的底层结构)
主要由对象头,实例数据,对齐填充组成
对齐填充:JVM内存模型要求对象起始地址是8字节对齐(64位)
why:因为 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓存行加载,CPU 的访问效率就会降低。
强软弱虚四大引用
强:内存不足也不会回收,GC也不会回收
软:内存不足就回收
弱:下一次GC回收,不论内存是否充足,ThreadLocal。
虚:跟踪对象被垃圾回收的过程,一般用不到
堆中内存是怎么分配的?
指针碰撞:加入堆中连续的内存,JVM维护一个指针,指向下一个可用内存的地址。每次分配内存时,将指针向后移动一段距离即可,如果没有发生碰撞则分配内存给对象
空闲列表:JVM维护一个空闲列表,存储内存的大小及位置信息。分配内存时,遍历列表选择足够大的内存分配给对象;分配后,如果内存没完全利用则剩余的空间作为新的内存加入到空闲列表中
指针碰撞适合碎片化少的区域——年轻代
空闲列表适合内存碎片化严重或对象内存差异大的场景——老年代
方法区
JVM定义的一种逻辑规范,JDK 7之前的实现形式为永久代,8之后被元空间取代(Metaspace)
用于存放类加载的信息,常量,静态变量,即时编译器编译后的代码缓存等
- 元空间与永久代的核心区别:
- 永久代位于 JVM 堆内存中,大小受 JVM 参数限制
- 元空间使用本地内存,默认大小只受系统可用内存限制
- 元空间仍然存储类元数据、常量池等信息,但管理方式不同
- 这一变化带来的好处:
- 减少了 OutOfMemoryError: PermGen space 错误
- 类元数据可以动态扩展
- 垃圾回收效率提升

变量存在堆栈的什么位置?
局部变量存储在栈中的局部变量表中,静态变量存在方法区中。
实例变量:声明在类中但非 static 的基本类型变量,作为对象的一部分,存储在堆中(跟随对象一起分配)。
关于this引用,this 引用指向当前实例对象,在实例方法调用时被隐式传入。当调用对象的实例方法时,JVM 会自动向该方法传递一个 this 引用,指向调用该方法的对象本身。因此在实例方法中可以直接使用 this 访问当前对象的成员(字段和方法)。
栈
- 虚拟机栈:执行方法会创建栈帧,被压入虚拟机栈
- 本地方法栈:为Java程序提供调用本地方法服务
常见native方法:hashcode(), System.currentTimeMillis()
类加载机制
类加载器
类加载过程
双亲委派机制
要求类加载器在加载类的时候,先委托父类进行加载,只有父类无法加载的时候,自加载器才会加载。
类的生命周期
加载 - 验证 - 准备 - 解析 - 初始化 - 使用 - 卸载
载入:将类的二进制字节码加载到内存中。
链接可以细分为三个小的阶段:
- 验证:检查类文件格式是否符合 JVM 规范
- 准备:为类的静态变量分配内存并设置默认值。
- 解析:将符号引用替换为直接引用。
初始化:执行静态代码块和静态变量初始化。
垃圾回收GC
谁是垃圾?
- 通过可达性分析
- 引用计数法 —— 无法解决循环引用的问题
垃圾回收算法
- 标记清除算法
- 会造成内存碎片空间
- 复制算法
- 移动对象较多,性能较低,适合在新生代中执行(存活的对象比较少)
- 要将内存空间折半分,将垃圾清除之后剩下的对象复制到另一块内存中,内存浪费大
- 标记整理算法
- 不会造成内存空间碎片,使内存更加紧凑
| 标记-清除 | 标记-整理 | 复制 | |
|---|---|---|---|
| 速度 | 中等 | 最慢 | 最快 |
| 空间开销 | 少(有碎片) | 少(无碎片) | 最多 |
| 移动对象 | 否 | 是 | 是 |
从GC Roots开始递归扫描标记
GC Roots:
- 虚拟机栈中的引用(方法的参数、局部变量等)
- 本地方法栈中 JNI 的引用
- 类静态变量
- 运行时常量池中的常量(String 或 Class 类型)
[!NOTE] 关于mark
对于mark的标记的是可达对象还是不可达对象,网上这两种说法均有。官方文档虽然没有详细指出,但出现过类似的说明
Concurrent Start : This type of collection starts the marking process in addition to performing a Normal young collection. Concurrent marking determines all currently reachable (live) objects in the old generation regions to be kept for the following space-reclamation phase. While collection marking hasn’t completely finished, Normal young collections may occur. Marking finishes with two special stop-the-world pauses: Remark and Cleanup.
并发启动:这种收集类型在执行正常年轻收集的同时启动标记过程。并发标记确定要保留在旧代区域中所有当前可到达(存活)的对象,以供后续的空间回收阶段使用。在收集标记尚未完全完成时,可能会发生正常的年轻收集。标记过程以两个特殊的全停暂停结束:标记和清理。
STW:Stop The World
在垃圾回收中会涉及到对象的移动,为了保证在对象移动的过程中不被修改,需要暂停所有线程。
- HOW:
- JVM发出安全信号
- 线程执行到安全点停止
- 垃圾回收器工作
- 线程恢复执行
安全点是 JVM 的一种机制,常用于垃圾回收的 STW 操作,用于让线程在执行到某些特定位置时,可以被安全地暂停。
垃圾回收器
CMS回收器
CMS 在JDK9被废弃
初始标记:
- STW,暂停所有工作线程
- 然后标记出 GC Roots 能直接可达的对象
- 一旦标记完,就恢复工作线程继续执行
- 这个阶段比较短
并发标记:
- 从上一个阶段标记出的对象,开始遍历整个老年代,标记出所有的可达对象
- 耗时会比较长
- 但是不需要 STW,用户线程与垃圾收集线程一起执行
重新标记:
- 上个阶段标记的对象,可能有误差,需要进行修正
- 需要 STW,但是时间也不是很长
- 增量更新
并发清除:
- 删除垃圾对象
- 由于不需要移动对象,这个阶段也可以和用户线程一起执行,不需要 STW
CMS问题总结:
- 并发标记与并发清理过程中的问题
- 如果在并发标记、并发清理过程中,由于用户线程同时在执行,如果有新对象要进入老年代,但是空间又不够,那么就会导致“concurrent mode failure”。
- 此时就会利用 Serial Old 来做一次垃圾收集,就会做一次全局的 STW。
- 浮动垃圾
- 在并发清理过程中,可能产生新的垃圾,这些就是“浮动垃圾”,只能等到下一次 GC 时来清理。
- 内存碎片与解决方案
- 由于采用的是标记-清除,所以会产生内存碎片。
- 可以通过参数
-XX:+UseCMSCompactAtFullCollection可以让 JVM 在执行完标记-清除后再做一次整理。 - 也可以通过
-XX:CMSFullGCsBeforeCompaction来指定多少次 GC 后来做整理,默认是 0,表示每次 GC 后都整理。
G1 回收器
其他
内存泄露和内存溢出
泄露:占着茅坑不拉屎
溢出:爆缸了
手写内存溢出的例子
1 | class HeapSpaceErrorGenerator { |
空间分配担保
在进行MinorGC前,JVM确保老年代有足够的空间存放从新生代晋升的对象。如果老年代空间不足则可能会触发Full GC
new对象时,堆会发生抢占内存
解决:JVM给每一个线程分配了一小块内存即TLAB,但TLAB满了才会在堆中创建对象