一、概述
Java最大的一个特点就是不用开发人员手动释放对象的内存,这些任务就交给了jvm来做。垃圾收集器有很多分类,如按照并行(ParNew、Parallel Scavenage、Parallel Old、CMS并发标记阶段、g1)、并发(cms并发标记和并发清理阶段、g2)和串行(Serial、Serial Old/PS Mark-Sweep),按照算法分引用计数和跟踪算法,按照性能指标比如吞吐量(Parallel Scavenge、Parallel Old)、低停顿(CMS)、增量式(G1)。
HotSpotVM的内存结构
二、基本算法
gc实现虽然很多,像串行、并行、并发、分代,但是最基本的算法却只有几种:引用计数、标记-清除算法、拷贝和整理,其中拷贝和整理算法还是以标记清除为基础的。
1、引用计数
每个对象记录被引用的次数,每多一个引用计数器加一,少一个引用计数器减一。如果引用次数为0了,就表示可以回收了。但是这里有一个问题,对象循环引用的时候,没有办法判断该不该回收。例如A引用B,B引用A,但是A、B都没有被别的对象引用,此时A、B计数器都不为0,但是也应该回收的。
2、标记-清除算法Mark-Clean
通过GC Roots遍历被引用的对象,标记为可达,未被遍历到的对象就是不再被引用的对象,然后遍历一遍把不再被引用的对象占用的内存回收掉。但是这种算法会带来一个问题,就是内存碎片问题,如果每次按照对象大小分配,内存用完之后回收,可用空间零散散布于这个堆中,下次申请比较大的内存的时候,可能就申请不到很大的连续空间,只有一堆不连续的小空间,此时申请内存就会失败,而从外部来看,堆中总共还有足够的空闲空间来分配。
对于JVM,GC Roots包括栈中引用的对象、方法区类变量引用的对象(类型信息也要被回收的,怎么会作为roots呢?)、本地方法栈引用的对象(还有什么?)
3、拷贝算法Copy
针对标记-清除算法的碎片问题,出现了一种改进的算法,把堆内存划分为两块,每次使用一块,当一块满的时候,就去标记使用中的对象,然后把使用中的对象拷贝到另外一个块里连续的内存空间中,然后把这个块的内存全部回收,后续创建对象申请空间就直接在另外一个块中申请。然后另外一个空间满的时候重复此过程。此算法解决了内存碎片问题,但是会降低内存的使用效率,一直都只有一半的空间被使用。同时考虑这样一个问题,如果有些对象一直被使用,如java.lang.Object这种在jvm的生命周期中一直会被使用的对象,copy来copy去的还是效率很低的。
4、标记-清理算法(也叫压缩算法)Mark-Sweep
针对拷贝算法的空间使用效率问题,标记整理算法给出了解决办法。在内存空间占满的时候,先通过GC Roots标记出存活的对象,然后把存活的对象从堆空间的一端,移动到另外一端,并使得他们占用的内存空间是连续的。移动完后,另一端就是一大块连续的空间,后续分配就在连续的空间进行,如果再次满了,就重复标记整理过程。但是这种算法依然存在拷贝算法存在的一个问题,就是长期存活的对象会被移动来移动去的。
5、分代收集算法
针对标记-清理算法中出现的问题,分代收集算法给出了解决办法。分代收集算法把根据对象存活的时间把堆内存空间多个部分,一般有新生代、中生代和老生代,不同的世代中的对象一般会用不同的算法来收集,如新生代和中生代很多朝生夕死的对象用拷贝算法拷贝效率也比较高,收集频率也比较高。老生代因为存活时间都很长了,并且可能一直存活下去,拷贝来拷贝去拷贝量很大,所以一般采用标记整理算法,收集的频率一般也比较低。
6、自适应算法
不同情况下,不同的收集器工作的好坏程度不一样,可以根据运行时状况动态调整。另外堆不同区域可以使用不同的算法。
三、垃圾收集器
垃圾收集器是特定于实现的收集器,都是基于以上算法实现的。不同的虚拟机,实现的细节也不太相同,此处只是一些概念和思想上的讲解。
1、Serial
Serial是新生代的单线程垃圾收集器,使用Copy算法。当进行垃圾收集的时候,别的线程都停止工作,Stop the World。这种收集器在多cpu上无法充分利用cpu资源,另外由于是单线程工作,停止时间也比较长。
在HotSpot JVM中使用-XX:+UserSerialGC会启动该收集器,同时老生代用SerialOld。是client模式默认的收集器。
该收集器可以跟老生代的Serial Old、CMS协同工作。
2、SerialOld
使用Mark-Sweep算法,是Serial的老生代GC收集器版本,单线程,Stop the World。
该收集器可以跟新生代的Serial、SerNew、Parallel Scavanage 协同工作。
该收集器还作为CMS失败时候的后备收集器。
3、ParNew
新生代收集器,是Serial的多线程版本,可以充分利用机器的cpu,收集的时候Stop the World。在单线程下也可以利用多线程,但是效率可能不如Serial,如果是多cpu,效率会很高。
在HotSpot JVM中使用-XX:UseParNew会启用该收集器,同时老生代使用SerialOld。
该收集器可以跟老生代的SerialOld、CMS协作收集。
4、Parallel Scavenge
新生代的收集器,也叫PS Scavenge,比起ParNew,此收集器的目的是达到一个可控的吞吐量,所以也叫做吞吐量优先收集器。可以通过参数-XX:+UseAdaptiveSizePolicy使用自适应内存调节策略,同时设置上-XX:MaxGCPauseMillis设置最大停机时间,通过-XX:GCTimeRatio设置吞吐量。
通过参数-XX:+UseParallelGC启用该收集器,此时老生代使用Serial Old(也叫PS MarkSweep,是在Serial Old的外边加了一层壳,本质上还是serial,很多地方也这么叫)
此收集器可以跟老生代Serial Old和Parallel Old协作。
5、Parallel Old
老生代并行收集器,Parallel Scavenge的老生代版本,吞吐量优先(但是没有相关参数设置老生代度量的?)
通过参数-XX:+UseParallelOldGC启用,新生代是Parallel Scavenage收集器。
注意:在jconsole中看到,-XX:+UseParallelOldGC时候老生代的内存收集器是PS MarkSweep,不是Parallel Old这个名字,原因这里有。
6、CMS/ConcurrentMarkSweep
老生代收集器,Concurrent Low Pause Collector,收集阶段分为四个阶段:
a、初始标记(initial mark),串行执行。从GC roots开始标记由roots直接关联的对象
b、并发标记(concurrent mark),并发执行。用户线程和标记线程同时工作,根据上一步标记出的对象进一步标记整个引用链。
c、重新标记(remark),串行执行。在并发标记完成后,再进行一次并行标记,处理在并发标记阶段引用改变的对象。
d、并发收集(concurrent mark),并发收集。gc线程跟用户线程一起运行,收集内存。
此收集器用的是Mark-Sweep算法,会导致内存随便,可能会出现总内存足够,但是分配某个大小的内存时候没有空间的情况,此时就是用Serial Old收集器作为失败后的收集器来收集。
通过参数-XX:+UseConcMarkSweepGC启用该收集器,新生代收集器默认是ParNew,并发模式失败后是用Serial Old收集器。
7、G1收集器
Garbage First,将在jdk1.7中发布。比起cms有两个优势,一是g1基于mark-compact算法,不会产生内存碎片;二是可以精确控制停顿。该收集器是把整个堆(新生代、老生代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。
四、终结
finalize方法是对象被gc前必须运行的。在gc时候,发现对象已经不再存活,就会判断对象是否有finalize方法, 如果有并且没有被运行过,就会运行该方法。运行该方法的时候对象可能会复活,比如把自己赋值给了一静态变量,或者一个存活对象的某个属性,这样该对象就不会被回收了。
运行完终结方法后,垃圾收集器会再次扫描不再被引用的对象,如果这次发现前一遍扫描到的对象仍然没有存活,就会释放掉它所占用的内存。终结方法只会运行一次,等下次该对象再次不在被引用的时候,就不会在运行该方法。
在Hotspot VM中,调用终结方法的是一个Finalizer线程,要终结的对象都被放到了F-Queue中,如果finalize方法运行时间过长,虚拟机会终止方法运行并回收对象的。
五、对象的可触及性和引用
堆中对象可以分为6种状态:强可触及、软可触及、弱可触及、影子可触及、可复活和不可触及。是根据从GC roots对象通过什么方式触及到该对象的。软、弱、影子可触及是通过Reference对象实现的。
软引用:内存不够的时候(将要OOM之前)会回收这些对象占用的内存,软引用可以用作缓存。
弱引用:每次gc的时候都会回收该引用引用的对象,弱引用可用作规范映射。
影子引用:放进影子引用的对象不能再在程序中得到,但是该对象被gc的时候你可以得到通知。
三个引用对象都可以关联队列,其中影子引用必须关联。