JVM组织架构:

  1. 类加载器,负责加载Class文件,将二进制数据读到内存(方法区)
  2. 运行时数据区,为Java程序的运行分配空间并处理数据
  3. 执行引擎,将字节码翻译为当前机器的机器指令(执行字节码)

加载过程:
类加载器读取 Class 文件的二进制数据后,会在内存中生成一个代表该类的java.lang.Class对象(这个对象存放在堆内存中),同时将类的元数据(方法、字段、常量池等)存储到方法区(逻辑概念)。

运行时数据区

给所有的对象分配内存的地方,分为新生代和老年代,新生代又有:Eden区From区, To区之分,大小比例8:1:1

新生代 -> 老年代
长期存活,大对象,动态年龄判断
动态年龄判断:当Survivor区的所有对象大小超过一定比例(一般是Survivor区的一半),那么没到达年龄阈值的对象也可能被提前晋升到老年代

随着JIT(Just In Time)的出现,“所有”不再这么绝对,会有逃逸分析来决定对象是否可以分配在栈上。

逃逸分析

1
2
3
4
5
6
7
private static void createObject() {
// 创建一个对象,但这个对象的引用没有逃逸出方法
User user = new User();
user.setName("Alice");
// 对象只在方法内部使用,没有返回也没有传递给其他方法
System.out.println(user.getName());
}

因为所有的对象都在这里分配,所以也是[[#垃圾回收GC]]的主要场所。
堆和栈的区别
几乎所有的对象分配在堆中,是线程共享的,被调用之后不会被清除,直到对象不再被引用为止,由GC垃圾回收器做处理。
栈是线程独立的,每个方法的执行会生成栈帧,存放局部变量表,操作数栈,动态链接等信息,会随着方法的结束而被释放。

栈帧结构

new对象的过程

对象的内存布局(Object的底层结构)

主要由对象头,实例数据,对齐填充组成

对齐填充:JVM内存模型要求对象起始地址是8字节对齐(64位)
why:因为 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓存行加载,CPU 的访问效率就会降低。

强软弱虚四大引用

强:内存不足也不会回收,GC也不会回收
软:内存不足就回收
弱:下一次GC回收,不论内存是否充足,ThreadLocal。
虚:跟踪对象被垃圾回收的过程,一般用不到

堆中内存是怎么分配的?

指针碰撞:加入堆中连续的内存,JVM维护一个指针,指向下一个可用内存的地址。每次分配内存时,将指针向后移动一段距离即可,如果没有发生碰撞则分配内存给对象
空闲列表:JVM维护一个空闲列表,存储内存的大小及位置信息。分配内存时,遍历列表选择足够大的内存分配给对象;分配后,如果内存没完全利用则剩余的空间作为新的内存加入到空闲列表中

指针碰撞适合碎片化少的区域——年轻代
空闲列表适合内存碎片化严重或对象内存差异大的场景——老年代

方法区

JVM定义的一种逻辑规范,JDK 7之前的实现形式为永久代,8之后被元空间取代(Metaspace)
用于存放类加载的信息,常量,静态变量,即时编译器编译后的代码缓存等

  1. 元空间与永久代的核心区别:
    • 永久代位于 JVM 堆内存中,大小受 JVM 参数限制
    • 元空间使用本地内存,默认大小只受系统可用内存限制
    • 元空间仍然存储类元数据、常量池等信息,但管理方式不同
  2. 这一变化带来的好处:
    • 减少了 OutOfMemoryError: PermGen space 错误
    • 类元数据可以动态扩展
    • 垃圾回收效率提升

变量存在堆栈的什么位置
局部变量存储在栈中的局部变量表中,静态变量存在方法区中。
实例变量:声明在类中但非 static 的基本类型变量,作为对象的一部分,存储在中(跟随对象一起分配)。

关于this引用,this 引用指向当前实例对象,在实例方法调用时被隐式传入。当调用对象的实例方法时,JVM 会自动向该方法传递一个 this 引用,指向调用该方法的对象本身。因此在实例方法中可以直接使用 this 访问当前对象的成员(字段和方法)。

  1. 虚拟机栈:执行方法会创建栈帧,被压入虚拟机栈
  2. 本地方法栈:为Java程序提供调用本地方法服务
    常见native方法:hashcode(), System.currentTimeMillis()

类加载机制

类加载器
类加载过程
双亲委派机制
要求类加载器在加载类的时候,先委托父类进行加载,只有父类无法加载的时候,自加载器才会加载。

类的生命周期

加载 - 验证 - 准备 - 解析 - 初始化 - 使用 - 卸载
载入:将类的二进制字节码加载到内存中。
链接可以细分为三个小的阶段:

  • 验证:检查类文件格式是否符合 JVM 规范
  • 准备:为类的静态变量分配内存并设置默认值。
  • 解析:将符号引用替换为直接引用。
    初始化:执行静态代码块和静态变量初始化。

垃圾回收GC

谁是垃圾?

  • 通过可达性分析
  • 引用计数法 —— 无法解决循环引用的问题

垃圾回收算法

  1. 标记清除算法
    • 会造成内存碎片空间
  2. 复制算法
    • 移动对象较多,性能较低,适合在新生代中执行(存活的对象比较少)
    • 要将内存空间折半分,将垃圾清除之后剩下的对象复制到另一块内存中,内存浪费大
  3. 标记整理算法
    • 不会造成内存空间碎片,使内存更加紧凑
标记-清除 标记-整理 复制
速度 中等 最慢 最快
空间开销 少(有碎片) 少(无碎片) 最多
移动对象

从GC Roots开始递归扫描标记
GC Roots:

  • 虚拟机栈中的引用(方法的参数、局部变量等)
  • 本地方法栈中 JNI 的引用
  • 类静态变量
  • 运行时常量池中的常量(String 或 Class 类型)

    [!NOTE] 关于mark
    对于mark的标记的是可达对象还是不可达对象,网上这两种说法均有。官方文档虽然没有详细指出,但出现过类似的说明

Oracle官方文档—GC

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问题总结

  1. 并发标记与并发清理过程中的问题
    • 如果在并发标记、并发清理过程中,由于用户线程同时在执行,如果有新对象要进入老年代,但是空间又不够,那么就会导致“concurrent mode failure”。
    • 此时就会利用 Serial Old 来做一次垃圾收集,就会做一次全局的 STW。
  2. 浮动垃圾
    • 在并发清理过程中,可能产生新的垃圾,这些就是“浮动垃圾”,只能等到下一次 GC 时来清理。
  3. 内存碎片与解决方案
    • 由于采用的是标记-清除,所以会产生内存碎片。
    • 可以通过参数 -XX:+UseCMSCompactAtFullCollection 可以让 JVM 在执行完标记-清除后再做一次整理。
    • 也可以通过 -XX:CMSFullGCsBeforeCompaction 来指定多少次 GC 后来做整理,默认是 0,表示每次 GC 后都整理。
G1 回收器

其他

内存泄露和内存溢出

泄露:占着茅坑不拉屎
溢出:爆缸了

手写内存溢出的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class HeapSpaceErrorGenerator {
public static void main(String[] args) {
// 第一步,创建一个大的容器
List<byte[]> bigObjects = new ArrayList<>();
try {
// 第二步,循环写入数据
while (true) {
// 第三步,创建一个大对象,一个大约 10M 的数组
byte[] bigObject = new byte[10 * 1024 * 1024];
// 第四步,将大对象添加到容器中
bigObjects.add(bigObject);
}
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError 发生在 " + bigObjects.size() + " 对象后");
throw e;
}
}
}

空间分配担保

在进行MinorGC前,JVM确保老年代有足够的空间存放从新生代晋升的对象。如果老年代空间不足则可能会触发Full GC

new对象时,堆会发生抢占内存
解决:JVM给每一个线程分配了一小块内存即TLAB,但TLAB满了才会在堆中创建对象