垃圾回收
垃圾
没有任何引用的一个对象或者一堆对象,被称为垃圾。
在java中,有垃圾回收器自动回收垃圾,不用程序员处理,所以开发效率比较高,运行效率比较慢;在c中,需要程序员手动处理垃圾,所以开发效率比较慢,运行效率比较高。
寻找垃圾的算法
- 引用计数:在对象上一个计数器,记录了这个对象有多少引用。少了一个引用,计数器就会减一。如果计数器为0,说明没有引用,这个对象就是垃圾了。但是,引用计数算法无法解决循环引用的问题。例如:A引用B,B引用C,C引用A,那么A、B、C都有一个引用,但是没有其他引用再指向A、B、C,这是一堆垃圾,却没有被引用计数算法识别出来。
- 根可达算法:有一个根对象的集合,从根对象开始搜寻,可以到达的对象都是有效对象,无法到达的对象都是垃圾。
根对象包括:
(1)虚拟机栈(栈帧中的局部变量)
-
当前正在执行的方法中的局部变量(包括参数、临时变量等)引用的对象。
void foo() { Object obj = new Object(); // obj是GC Root(当前方法的局部变量) }- 只要方法未执行完,
obj引用的对象就存活。
- 只要方法未执行完,
(2)方法区中的静态变量
-
类的静态成员变量(
static修饰)引用的对象。class MyClass { static Object staticObj = new Object(); // staticObj是GC Root }
(3)方法区中的常量引用
-
常量池(
final static常量)引用的对象。class MyClass { final static String CONST = "Hello"; // CONST是GC Root }
(4)本地方法栈(JNI引用)
- JNI(Java Native Interface)调用本地方法(C/C++代码)时引用的Java对象。
(5)活跃线程对象
-
当前所有正在运行的线程(
Thread实例)及其栈中的局部变量。Thread t = new Thread(() -> {...}); t.start(); // t是GC Root
(6)JVM持有的系统对象
- JVM内部管理的对象(如类加载器、系统类等)。
垃圾回收算法
标记-清理
扫描堆区的对象,将有用的对象和无用的对象标记出来,清理无用的对象。
优点:清理速度快
缺点:产生很多内存碎片
复制算法
将堆区分成两块,每次只有一块被使用。要清理垃圾时,扫描有用的对象,复制到另一块,然后将原来的区域清理
优点:不会产生内存碎片,要复制对象,效率不高
缺点:只能使用一半的空间,很浪费
标记-整理
扫描堆区的对象,将有用的对象标记出来,整理到前面去。然后将后面的无用对象清理掉
优点:不会产生内存碎片,也不会浪费内存
缺点:效率很低
JVM堆区分代模型
JVM为了更高效的进行垃圾回收,将堆区使用了分代模型
G1之前的垃圾回收器,不仅在逻辑上进行了分区,在物理层面也进行了分区
G1垃圾回收器只在逻辑上进行了分区
分区模型
堆区被分为两块区域,新生代和老年代,划分比例默认是1:2可以通过-XX:NewRatio参数修改。一般刚创建的对象,或者没有经过太多次GC的对象会放在新生代;经历过多次GC,依然存活的对象会放在老年代。
新生代中存活的对象很少,垃圾很多,所以适合用复制算法;老年代中存活对象很多,垃圾很少,适合用标记-清理算法。
新生代又被划分为一个eden区和两个survivor区,划分比例是8:1:1。为什么这么划分呢?新生代采用的是复制算法,如果将新生代分成两块区域,那每块的区域都很大,但是,新生代存活的对象非常少,导致每次GC的时候,要扫描的区域很大,需要复制的对象又很少,算法效率就很低。多了一个eden区,而且占大量空间,survivor区占少量空间,每次GC扫描eden区和一个survivor区的空间,存活的对象复制到另一个survivor区,存活对象少,另一个survivor空间够用。
MinorGC:当新生代不够用的时候,会触发MinorGC,只回收新生代的垃圾
FullGC:当老年代不够用的时候,会触发FullGC,会回收新生代和老年代的垃圾
对象分配
栈上分配
刚创建出一个对象,如果这个对象很小,而且没有逃逸,只有简单的属性,那么这个对象会优先存储在线程栈上。随着方法执行结束,对象随着栈帧弹出栈,不需要垃圾回收器回收。
线程本地分配TLAB
一般刚创建的对象会分配到eden区,线程需要在eden区抢占空间。多个线程就会导致并发操作,会降低效率。所以,在eden区,会为线程开辟一块空间(大小约为eden区大小的1%),属于线程私有。线程要分配对象时,优先在这块区域分配,避免多线程并发的情况。
当TLAB区域满了,或者进行GC的时候,TLAB区域会被释放。
对象在分代模型的变迁
- 对象尝试在线程栈上分配
- 不满足栈上分配条件,判断对象是否足够大,如果足够大,在老年代分配,否则在eden区分配
- 如果对象在eden区分配,经历一次GC,会从eden区到survivor区
- 再经历一次GC,会从survivor区到另一个survivor区,在经历多次GC后任然存活下来,迁移到老年代
对象从新生代到老年代的年龄界限默认是15(CMS垃圾回收器是6)。
还有一些情况,对象在不满足年龄的情况下,也会提前进入老年代:
- 动态年龄判定:在经历一次MinorGC后,发现survivor区的对象,年龄1的占33%,年龄2的占33%,年龄3的占34%。由于年龄1的对象加上年龄2的对象数量已经占survivor区超过一定比例,这个比例由参数TargetSurvivorRatio决定。所以会将年龄2及往上年龄的对象直接放到老年代。
- 分配担保机制:在执行MinorGC之前,JVM会判断老年代连续内存空间的大小是否大于新生代所有对象的大小总和,如果不大于,检查是否允许担保失败。如果允许,再检查老年代连续内存空间的大小是否大于新生代所有对象的大小的平均值,如果大于,进行MinorGC。如果小于,进行FullGC
垃圾回收器
新生代的垃圾回收器有:Serial、Parallel Scavenge、Parallel New
老年代的垃圾回收器有:Serial Old、Parallel Old、CMS
G1垃圾回收器新生代和老年代都会使用
Serial
Serial是用于新生代的垃圾回收器,单线程,在垃圾回收线程工作时,业务线程必须停止。
Serial一般与Serial Old一起使用。
Parallel Scavenge
Parallel Scavenge是用于新生代的垃圾回收器,多线程,在垃圾回收线程工作时,业务线程必须停止。
Parallel Scavenge一般与Parallel Old一起使用。JVM默认是这种组合。
Parallel New
Parallel New是用于新生代的垃圾回收器,多线程,在垃圾回收线程工作时,业务线程必须停止。
Parallel New一般与CMS一起使用,它与Parallel Scavenge区别就是可以和CMS一起使用,但是不能与Parallel Old一起使用。
Serial Old
Serial Old是用于老年代的垃圾回收器,单线程,在垃圾回收线程工作时,业务线程必须停止。
Serial Old清理过程是先标记,然后清理垃圾,然后整理存活对象放到一起,消除碎片。
Serial Old一般用于清理几十兆的内存
Parallel Old
Parallel Old是用于老年代的垃圾回收器,多线程,在垃圾回收线程工作时,业务线程必须停止。
Parallel Old使用的是标记-整理算法。
Parallel Old一般用于清理几个G的内存
CMS
CMS是用于老年代的垃圾回收器,是在垃圾回收阶段,工作线程可以正常运行的垃圾回收器。
CMS使用的是标记-清理算法。
CMS一般用于清理十几个G的内存
CMS的垃圾清理分为四个阶段:
- 初始标记:STW,将根对象标记出来
- 并发标记:不会STW,用可达性算法,将有用对象标记出来
- 重新标记:STW,用可达性算法,将有用的对象重新标记一遍。因为在并发标记阶段,工作线程可能会产生新的垃圾,也可能将原来的垃圾变成不是垃圾,所以需要重新标记
- 并发清理:不会STW,将垃圾清理掉。这个阶段会产生浮动垃圾,只能下次清理
CMS的问题
问题1
由于CMS在垃圾清理阶段,工作线程是运行的,所以会有浮动垃圾产生,而且CMS不会清理浮动垃圾。工作线程在运行,可能会有新的对象从新生代到老年代,而浮动垃圾一直在产生占据老年代空间,可能会导致CMS的垃圾清理阶段,老年代的空间不够用,此时,Serial Old会出来工作,一个线程在处理垃圾,效率极低。
解决办法
降低CMS的触发阈值,让FullGC提前触发,给浮动垃圾预留足够的空间。
例如,将-XX:CMSInitiatingOccupancyFraction参数设置低一些,假设设置成50%,那么老年代空闲空间只有一半的时候就会触发FullGC了,此时,就算产生浮动垃圾,也有50%的空闲空间去存储,降低了出现新生代的对象放不下的问题概率。
问题2
由于CMS采用的是标记-清理算法,时间长了,会产生很多空间碎片。
解决办法
定期整理一次空间,消除空间碎片。
有两个参数设置:
- -XX:UseCMSCompactAtFullCollection:开启整理过程
- -XX:CMSFullGCsBeforeCompaction:设置经历多少次FullGC后,整理一次空间碎片
这两个参数要同时使用。
三色标记算法
对象在逻辑上分为黑色、灰色和白色。黑色表示对象自身被标记,并且其引用的对象也被标记;灰色表示自身被标记,但是其引用的对象没有被标记;白色表示自身和其引用的对象都没有被标记。
标记过程:
(1)初始时所有对象都被设置为白色。
(2)从GC ROOTS开始,将它们标记为灰色,并加入一个工作列表。
(3)遍历每个灰色对象的子对象,并将它们标记为灰色,同时将它们添加到工作列表中。
(4)对于每个灰色对象,如果其所有子对象都已经处理完毕,则将其标记为黑色,并且移出工作列表。
(5)重复步骤3和4,直到工作列表为空为止。
当所有对象都被处理完毕后,白色的对象就被视为不可达,并可以进行垃圾收集。
CMS和G1出现的漏标问题
漏标现象发生在并发标记过程,例如:某一灰色对象引用一个对象,但是这个对象还没有被标记,在并发标记时,这个引用消失了,但是某一黑色对象引用了这个对象,但是黑色对象的引用不会再扫描了,导致垃圾回收器会以为这个对象没有引用,被当成了垃圾。
CMS的解决方案是增量标记:当黑色对象新增引用的时候,将黑色对象变成灰色对象,重新标记时再次扫描其引用。
G1的解决方案是SATB:当灰色对象对子对象的引用消失时,将引用存入堆栈中,下次还可以扫描到白色的对象,然后根据RSet判断这个对象是否有引用,没有就当做垃圾处理。