JVM 垃圾回收
目录
- 一、内存分配原则
- 1.对象优先在Eden区分配
- 2.需要大量连续内存空间的对象直接分配到Tenured区
- 3.长期存活的对象转移到Tenured区
- 4.内存分配过程
- 4.1 JDK9 G1
- 4.2 JDK8 CMS
- 二、内存回收原则
- 1.GC类型
- 2.死亡对象判断方法
- 2.1 引用计数法
- 2.2 可达性分析算法
- 3.垃圾收集算法
- 3.1 标记-清除算法
- 3.2 标记-复制算法
- 4.内存回收过程
- 4.1 三色标计法
- 4.2 CMS
- 4.3 G1
一、内存分配原则
1.对象优先在Eden区分配
对象首先在Eden区分配内存空间。当Eden区没有足够空间进行分配时,JVM触发Minor GC/Young GC:
- 将Eden区中所有存活对象的对象头部记录年龄的4bit位+1,并移动到
S1区。 - 将S0区的存活对象年龄+1,如果年龄达到
1111,将对象移动到Tenured区,否则移动到S1区。 - 如果S0区中相同年龄的所有对象大小的总和超过了S0空间的一半,那么年龄大于或等于该年龄的对象会移动到
Tenured区。 - 此时Eden和S0被清空,将S0和S1身份互换,下次GC移动到S0。
- 为新对象在
Eden区分配内存空间,初始化年龄为0000。 - 如果
S1区在Minor GC过程中空间不足,JVM会采用分配担保机制,将超出S1容量的对象直接移动到Tenured区。如果Tenured区空间也不足,就会触发Full GC。
这里可能会问为什么要有Eden、S0、S1,直接用Eden不行吗?
首先这是一种垃圾回收算法,分区机制是考虑到将存活的对象转移到新的连续内存空间中保证死亡对象回收后不会有内存碎片问题,导致空间不足。
其次这种转移方式是为了统一更新存活对象的年龄,在转移时更新避免遗漏。
2.需要大量连续内存空间的对象直接分配到Tenured区
大对象直接进入Tenured区是为了避免大对象在S0\S1区之间来回移动的成本、避免占用太大的S1区内存、大对象需要连续空间会导致S1区内存碎片,降低存储效率。
3.长期存活的对象转移到Tenured区
对象在S0\S1区每熬过一次Minor GC/Young GC,对象的年龄就增加1岁,当它的年龄增加到一定程度(默认为 15 岁),就会被移动到Tenured区中。
4.内存分配过程
4.1 JDK9 G1
- 类加载后,G1采用页式离散管理+可变分区连续管理,基于
-XX:G1HeapRegionSize将堆内存划分为大小相同的Page。JVM按照地址升序(页号,页内地址[TLAB号,内地址])维护一个全局空闲分区链表。 - 为了解决多线程并发的效率问题,JVM的思路是为每个线程预分配一块堆空间TLAB,线程实例化对象时直接在TLAB低地址划分一块连续内存空间给当前对象,当前线程的后续对象依次在该TLAB中分配,直到该TLAB满了。
- 当该线程的TLAB空间不足时,从最近(之前的肯定都分配满了)状态为Eden的Page中
CAS修改分配指针(每个Page页内地址从0x0000开始)取一块连续的TLAB给当前线程。 - 当Eden的Page空间不足时,使用首次适应算法(低地址开始分配)从空闲分区链表获取一个状态Free的Page,使用
CAS标记为Eden,所以各代由一组非连续的Page构成,且各代空间占用动态变化。 - 如果空闲分区链表为空,那么触发
Young GC。 - 对于内存占用超过
Page/2的大对象,直接分配到Tenured区,遍历空闲分区链表获取一个或多个连续的状态为Free的Page标记为Humongous,不同的是Page内即使有空闲空间也不会再被使用。
4.2 JDK8 CMS
- CMS的Eden区和Tenured区都是一片连续的存储空间,各代的空间占用固定
-XX:SurvivorRatio。 - Eden区维护一大片连续的存储空间,每个线程还是私有TLAB,当TLAB不足时在Eden区低地址部分
CAS修改分配指针分配连续空间。 - Tenured区维护空闲列表,对于
-XX:PretenureSizeThreshold的大对象,JVM使用首次适应/最佳适应算法找到一个足够大的、连续的内存块来存放对象。
二、内存回收原则
1.GC类型
Minor GC:Eden内存区空间不足时触发,只清理Eden和S0、S1中的对象。Major GC:当Tenured内存区空间不足时触发,只清理Tenured中的对象。Full GC:当方法区、Tenured内存区空间不足时触发,清理整个堆空间(Eden、S0、S1、方法区Metaspace)
2.死亡对象判断方法
2.1 引用计数法
给对象头中添加一个引用计数器,每当该对象被引用,计数器就加 1;当引用失效,计数器就减 1;计数器为 0 的对象默认为死亡。
这种方法的缺点是当多个对象相互引用,但是这些对象除了相互引用外没有被使用,此时GC就无法回收。
voidtest(){Nodea=newNode();// 对象1Nodeb=newNode();// 对象2a.next=b;b.next=a;// 方法结束,对象1和对象2就用不到了,但是由于他们相互引用,GC无法回收他们}2.2 可达性分析算法
将活跃栈帧本地变量表中的引用、方法区中引用类型的静态变量作为GC Roots,每个GC Roots可以引出一棵树结构,在JVM堆中(包括字符串常量池)不在任何一棵树下的对象被定义为死亡,等待下次GC,死亡对象的内存空间就会被释放。
Java 引用:强引用、软引用、弱引用
大白话就是只要方法中用到的堆对象就不会被回收(前提是强引用),方法执行完了实例化的对象就没用了,那就可以回收了。
voidtest(){Nodea=newNode();// 对象1Nodeb=newNode();// 对象2a.next=b;b.next=a;// 方法结束,栈帧被释放// 虽然对象1和对象2互相引用,但没有GC Roots指向它们// 结果:对象1和对象2都不可达,都会被回收}对于方法区中的类,判定为死亡需要满足三个条件:
- 该类所有在堆中的实例都已经被回收。
- 加载该类的ClassLoader已经被回收。
- 内存中没有使用反射访问该类。
3.垃圾收集算法
3.1 标记-清除算法
CMS垃圾回收器在老年代用的就是标记清除算法,老年代是一块固定的连续存储空间,垃圾回收后通过维护空闲列表来记录可用内存。标记清除算法首先会标记存活的对象,统一回收所有没有被标记的对象,缺点是内存碎片问题严重。
3.2 标记-复制算法
CMS垃圾回收器在青年代用的就是标记复制算法,Eden和Survivor是固定的连续存储空间,分别维护一个碰撞指针每次从低地址分配内存,标记复制算法将存活对象移动到Survivor低地址,便于维护碰撞指针。将内存分为大小相同的两块(Eden+Survivor[P0、P1]),只向Eden和P0中存数据,执行垃圾回收时将存活的对象全部转移到P1内存中,清空Eden+P0的,很好的解决了内存碎片问题。
G1垃圾回收器基于标记复制算法,将多个Page中所有存活的对象移动到新的空闲Page中并标记为Survivor,将原Page放回空闲链表。
4.内存回收过程
4.1 三色标计法
- STW在基于可达性分析标记垃圾时会阻塞应用程序的所有线程,保证标记完整(一边扫地一边嗑瓜子)。三色标记法实现了并发标记不影响应用线程的执行。
- 三色标计法在标记时会基于上次标记的结果树,只修改发生变化的子树,减少了重复的扫描。为每个对象涂色,每次标记只扫描灰色集合中的对象并扫描其子树:
白色:对象没有被标记过
灰色:对象已经被标记过了,但该对象下还有部分属性没被标记
黑色:对象已经被标记过了,且对象下的属性也被标记过了
因为标记是在并发条件下的,当本轮标记后-准备清除前死亡对象又被重新引用了,就会导致对象被错误的回收,但是CMS和G1已经解决了这个问题。
4.2 CMS
- 初始标记:单线程并Stop the World,标记GC Roots能直达的对象。
- 并发标记:无需中断,和用户线程同时运行,三色标记法从GC Roots直达对象开始遍历整个对象树。
- 重新标记:多线程并STW,标记并发标记阶段引用发生变化的对象(扫描三色标记法灰色集合)。
- 并发清除:和用户线程同时运行,清理标记为死亡的对象。
- 筛选回收:STW,老年代执行清除算法,年轻代执行复制算法。
4.3 G1
- 获取对应区域的Page集合
- 初始标记:单线程并Stop the World,标记GC Roots能直达的对象。
- 并发标记:无需中断,和用户线程同时运行,三色标记法从GC Roots直达对象开始遍历整个对象树。
- 重新标记:多线程并STW,标记并发标记阶段引用发生变化的对象(扫描三色标记法灰色集合),防止对象被。
- 并发清除:和用户线程同时运行,清理标记为死亡的对象。
- 筛选回收:STW,执行复制算法,选择多个Page构成回收集,执行复制算法把回收集中的对象复制到新的Page中标记为Survivor,将回收集的Page清空并放回空闲链表。
