文章目录
什么是垃圾
一般来说没有引用指向的就是垃圾,但是有引用指向的也有可能是垃圾。
如何找到垃圾
引用计数(Reference Count)
对每个对象的被引用个数进行计数。若有三个引用指向它,则它的引用计数为3。
若为0,则说明它是垃圾。
缺点:无法处理循环引用的垃圾。如下图所示
根可达算法(Root Searching)
通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
如何清理垃圾
标记清除 (Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
缺点:内存碎片化严重
复制 (Copying)
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉
优点:实现简单,内存效率高,不易产生碎片
缺点:可用内存被压缩到了原本的一半。如果存活对象增多,Copying算法的效率会大大降低
标记压缩 (Mark-Compact)
标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象。
唯一问题是效率稍微低一些。
JVM分代算法
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。
老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
新生代
目前大部分JVM的GC对于新生代都采取Copying算法。
因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space)。
每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
老年代
而老年代因为每次只回收少量对象,因而采用Mark-Compact算法。
- JAVA虚拟机提到过的处于方法区的永生代(Permanent Generation),它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
- 对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。
- 当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。
- 如果To Space无法足够存储某个对象,则将这个对象存储到老生代。
- 在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。
- 当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。
垃圾回收器种类
常用组合:
-
Serial + Serial Old
-
Parallel Scavenge + Parallel Old
-
ParNew + CMS
Java 1.3
Serial:串行
Serial(年轻代)
关键词:复制 + 单线程
a stop-the-world (STW), copying collector which uses a single GC thread.
Serial Old(老年代)
关键词:标记压缩 + 单线程
a stop-the-world, mark-sweep-compact collector that uses a single GC thread
Java 1.8
Parallel:并行
Parallel Scavenge(年轻代)
关键词:复制 + 多线程
a stop-the-world, copying collector which uses multiple GC threads.
Parallel Old(老年代)
关键词:标记压缩 + 多线程
a compacting collector that uses multiple GC threads.
特殊
ParNew(年轻代)
关键词:复制算法 + 多线程
为了配合CMS设计而出。基本上和Parallel Scavenge算法一样。
- a stop-the-world, copying collector which uses multiple GC threads.
- It differs from “Parallel Scavenge" in that it has enhancements that make it usable with CMS。
- For example, “ParNew” does the synchronization needed so that it can run during the concurrent phases of CMS.
CMS(老年代)
关键词:标记清除算法 + 多线程 + 并发
- concurrent(并发)mark-sweep
- a mostly concurrent, low-pause(暂停时间短) collector
- 四个阶段
- 初始标记 initial mark:时间非常短,将垃圾的根部标记
- 并发标记 concurrent mark:时间最长,根据标记的根部并发标记垃圾
- 重新标记 remark:由于是并发标记,因此可能会有新的垃圾产生或者标记的垃圾不再是垃圾,所以需要重新标记
- 并发清理 concurrent sweep:清理标记好的垃圾即可
缺点:
- 内存碎片化严重
- 会产生浮动垃圾(并发清理时产生的新的垃圾,下次垃圾回收时回收)
G1 (Garbage-First)
- 空间整合:基于标记-压缩算法,不产生内存碎片。
- 可预测停顿:可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
- 使用G1收集器时,Java堆的内存布局与其他收集器有很大的区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分 Region(不需要连续)的集合
- G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域(Region),并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。
ZGC
染色指针
HotSpot虚拟机的标记实现方案有如下几种:
- 把标记直接记录在对象头上(如 Serial收集器)
- 把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息)
- 目前在Linux下64位的操作系统中高18位是不能用来寻址的,但是剩余的46为却可以支持64T的空间,到目前为止我们几乎还用不到这么多内存。于是ZGC将46位中的高4位取出,用来存储4个标志位,剩余的42位可以支持4T的内存。
三色标记
在并发的可达性分析算法中我们使用三色标记(Tri-color Marking)来标记对象是否被收集器访问过
- 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
标记步骤:
- 初始时,所有对象都在 【白色集合】中;
- 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
- 从灰色集合中获取对象:
3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2. 将本对象 挪到 【黑色集合】里面。 - 重复步骤3,直至【灰色集合】为空时结束。
- 结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。
读写屏障
- 当对象从堆中加载的时候,就会使用到读屏障(Load Barrier)。这里使用读屏障的主要作用就是检查指针上的三色标记位,根据标记位判断出对象是否被移动过,如果没有可以直接访问,如果移动过就需要进行“自愈”(对象访问会变慢,但也只会有一次变慢),当“自愈”完成后,后续访问就不会变慢。
- 读写屏障可以理解成对象访问的“AOP”操作。