垃圾收集
并行收集器:多个GC线程同时运行,在GC线程运行时,挂起用户线程。
并发收集器:多个GC线程同时运行,在某个GC阶段GC线程和用户线程同时运行,但有的阶段还需要挂起用户线程。
程序计数器,虚拟机栈,本地方法栈和方法区内存回收
程序计数器,虚拟机栈和本地方法栈内存自动完成回收。方法区内存在full gc时被回收。
判断对象是否需要回收
计算对象是否需要回收2种判断方法,引用计数法和可达性分析法。
引用计数算法:对一个对象被引用的次数计数,为0则可以回收,缺点时但存在循环引用时无法真正彻底回收。
可达性分析算法:将称为GC Roots的对象作为起始点,向下搜寻,搜寻经过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,这个对象可以回收。
1 | 可作为GC Roots的对象: |
java采用的是可达性分析算法。
java引用
- 强引用:Strong Reference,使用new关键字创建的引用,只要强引用还在,垃圾回收器不会回收被引用的对象。
- 软引用:Soft Reference,有用但非必需的对象。系统在发生内存溢出前会标记被引用对象,回收下面2中类型引用的对象,如果空间还不足,则回收软引用的对象,还不足则抛出内存溢出异常。
- 弱引用:Weak Reference,非必须对象,比软引用更弱,只要有垃圾回收,这些对象都会被回收。
- 虚引用:Phantom Reference,最弱的引用,对象是否有虚引用,不对其生存时间有影响,为一个对象设置虚引用关联的唯一目的就是在这个对象被垃圾收集器回收时收到一个系统通知。必须和ReferenceQueue组合使用,在队列中的对象都是已经被GC收集的对象,在虚引用对象上调用get方法均返回null。
回收对象
在确定对象需要回收时,它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法:
- 对象是否覆盖finalize()。
- 被覆盖的finalize()是否执行过。
当对象没有覆盖finalize()方法或finalize()已经被执行过,则虚拟机认为没必要执行finalize()方法。
当对象被判断为有必要执行finalize()方法后,对象会被放到F-Queue队列中,由虚拟机自动建立一个低优先级的Finalizer线程去执行对象finalize()方法。注意,虚拟机会触发finalize()方法,但不保证会等待它执行结束。之后GC将会对F-Queue队列中的对象进行第二次标记,如果在finalize()执行中,对象将自己的引用重新与引用链上的任何一个对象建立联系,第二次标记时它将被移出“即将回收的集合”。
GC将在“即将回收的集合”中的对象回收。
回收的情况: - 没有覆盖finalize();
- 覆盖finalize(),但在方法中没有与引用链上的其他对象建立联系。
常见垃圾收集算法
几种常见的垃圾收集算法。
标记-清除算法
Mark-Sweep算法,分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。是最基础的算法。
缺点:
- 两个阶段的效率都不高。
- 标记清除后的内存空间不连续,有可能大对象在垃圾收集后还是无法分配,触发再次垃圾回收。
复制算法
Copying算法,将内存按容量分为大小相等的2块,每次只使用其中一块,当这块内存用完时,将还存活的对象复制到另外一块上面,将这块一次性完全清理掉。
- 优点:内存分配时不用考虑碎片问题,只要移动堆顶指针,按顺序分配内存,实现简单,运行高效
- 缺点:内存缩小为原来的一半,浪费空间。当对象存活率高时,效率变低。
对复制算法的改进,将内存分为3块,一块较大,另外两块较小且相等。每次使用较大的和较小的一块,当回收时将存活的复制到另外一块较小的,清理刚使用的较大的和其中较小的一块,再次使用时,使用较大的和未被清理的较小一块。
在hotspot中,分为3块,1块Eden和2块Survivor,默认比例为8:1:1。survivor一块为form,一块为to。先使用Eden和from,然后将存活的对象复制到to,清理Eden和from,然后使用Eden和to,下次则将存活的复制到from,清理Eden和to,使用Eden和from,如此往复。如果在回收时未使用的survivor区空间不够,则需要年老代担保,剩下的对象将直接进入年老代分配。标记-整理算法
主要用于年老代,标记过程同标记清除算法一样,但后续不是对可回收对象进行清理,而是让所有存活对象向一端移动,然后清理掉边界以外的内存。分代收集算法
分代收集算法是根据对象存活周期将内存分分为几块,一般分为新生代和年老代,然后根据各个年代的特点采用最适当的收集算法。一般新生代使用复制算法,年老代采用标记-清除算法或标记-整理算法。
Hotspot jvm算法实现
使用可达性分析算法进行对象是否可回收判断,在类加载的时候使用一组OopMap数据结构记录对象内各个偏移量指向的数据类型,在GC扫描时直接获得哪些地方存放着对象引用,快速完成GC Roots的枚举。
GC只会在用户线程到达SafePoint时进行,安全点选取是以“具备让程序长时间执行的特征为准”,最明显的特征是指令序列的复用,例如方法调用、循环跳转、异常跳转等。让用户线程中断的方式有2种,抢先式中断和主动式中断:抢先式中断,在GC发生时将用户线程全部挂起,如果发现有的线程没有到达安全点,则恢复线程,让它执行到安全点。主动式中断则是简单的设置一个标志位,各个线程执行时主动轮询这个标志,发现中断标志为真时就自动中断挂起,轮询标志的地方和安全点是重合的。
安全区域用于解决在GC前就挂起的线程运行问题,当线程执行到安全区域时,首先标记自己进入安全区域,当这段时间JVM发起GC时,就不用管这些线程,当这些线程要离开安全区域时,首先检查系统是否完成了根节点的枚举或整个GC过程,如果完成了,线程继续执行,否则线程继续挂起等待,直到收到可以离开安全区域的信号。
Hotspot对不同分代区域使用不同的垃圾收集算法实现的垃圾收集器。
jvm垃圾收集器
新生代垃圾收集器:Serial,ParNew,Parallel Scavenge
年老代垃圾收集器:Serial Old,Parallel Old,CMS
跨代垃圾收集器:G1
新生代垃圾收集器
新生代收集器都使用复制算法。
Serial
最基本的,单线程垃圾收集器。进行垃圾收集时,暂停其他所有用户线程,直到它收集结束。
ParNew
Serial收集器的多线程版本。进行垃圾收集时暂停其他所有用户线程,直到它收集结束。在单核心CPU中,ParNew性能不会优于Serial。使用-XX:+UseConcMarkSweepGC时的默认新生代收集器。
Parallel Scavenge收集器
吞吐量优先的收集器,多线程垃圾收集器,进行垃圾收集时,暂停所有用户线程,直到垃圾收集结束。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。使用参数-XX:MaxGCPauseMillis和-XX:GCTimeRation控制GC时间。
-XX:MaxGCPauseMillis 关注停顿时间 ,大于0的毫秒数,收集器 尽可能保证 垃圾收集花费时间不超过设定值,需要根据实际情况合理设置。如果过小,会影响系统吞吐量。
-XX:GCTimeRation 关注吞吐量 ,大于0小于100的整数n,代表系统运行用户代码和垃圾收集时间的比值(n=用户运行代码时间/垃圾收集时间=n/1)。GC所占时间比率为1/(1+n)
-XX:+UseAdaptiveSizePolicy 不需要手动指定新生代大小,新生代中eden和survivor的比例,晋升年老代对象大小等细节参数,虚拟机会根据当前系统运行情况收集性能监控数据,动态调整这些参数以提供合适的停顿时间或最大吞吐量。
年老代垃圾收集器
Serial Old
单线程收集器,使用 “标记-整理”算法,进行垃圾收集时,暂停所有用户线程,直到垃圾收集结束。client模式下虚拟机使用。作为使用-XX:+UseConcMarkSweepGC开启CMS收集器收集失败时的后备预案。
Parallel Old
Parallel Scavenge的年老代版本,使用多线程和“标记-整理”算法。进行垃圾收集时,暂停所有用户线程,直到垃圾收集结束。
CMS
以获取最短回收停顿时间为目标的收集器。适合希望系统停顿时间最短的系统,如WEB系统。基于 “标记-清除” 算法的 多线程并发 收集器。
收集过程分为4个步骤:
1 | 初始标记:暂停用户所有线程,进行GC Roots可直接到达对象的标记 |
CMS收集器由于可以和用户线程并发进行,所以停顿时间较低,CMS通常配合ParNew或Serial收集器使用。
有3个明显缺点:
CMS收集器默认开启垃圾回收线程数为(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收垃圾收集线程不少于25%的CPU资源,并且随着CPU数量增加而下降。CPU数量=CPU物理核心数,一个核心一个线程。可以使用参数限制GC回收线程数量。当CPU数量不足4个时,由于线程切换,CMS收集器性能并不好。
CMS收集器由于是并发标记阶段和用户线程一起运行,所以还会有一部分垃圾无法在当次垃圾(浮动垃圾)回收中处理。由于在进行垃圾回收最后阶段时用户线程还在运行,如果此时回收未在年老代空间分配完之前完成或剩余空间不够,可能引起“Concurrent Mode Failure”导致使用Serial Old进行Full GC。使用-XX:CMSInitiatingOccupancyFraction可以设定触发CMS执行垃圾回收时的容量阈值,默认68,当年老代容量达到68%时执行GC。解决cms失败的2个方式:降低触发阈值,但会造成频繁GC;增大新生代,同时降低触发阈值,调整full gc时压缩碎片的频次。
1
2CMSInitiatingOccupancyFraction大小的确定:
{年老代-(eden+survivor)}/年老代*100%CMS基于“标记-清除”算法实现,在收集结束后会产生大量空间碎片,空间碎片会造成大对象无法分配问题。CMS收集器提供了-XX:+UseCMSCompactAtFullCollection在full gc之前开启碎片的合并整理,但在碎片整理时,停顿时间不得不变长,配合使用另外一个参数-XX:CMSFullGCsBeforeCompaction设定在进行了多少次不压缩的full gc后进行一次带压缩的full gc,以此减少停顿时间。
G1收集器
G1收集器用来替换CMS收集器,整体基于“标记-整理”算法,局部基于“复制”算法,多线程并发收集器。G1同时用于新生代和年老代,且可指定最长垃圾回收停顿时间。
G1不在物理内存中划分出整块的新生代或年老代,而是将堆划分为多个大小相等的region,region可能用于新生代也可用于年老代。在内存回收时,优先回收垃圾较多的region。
大致分为4个步骤:
- 初始标记 暂停用户所有线程,进行GC Roots可直接到达的对象标记。
- 并发标记 同用户线程一起运行,对垃圾对象进行标记。
- 最终标记 暂停用户所有线程,修正标记。
- 筛选回收 (目前)暂停用户所有线程,对回收价值高的region进行垃圾回收。
何时进行Minor GC,Major GC,full GC,OOM触发条件
Minor GC为新生代GC,当新生代空间不够触发。
Major GC为年老代GC,一般触发时即使full gc,但在CMS中,在年老代空间超过阈值时触发CMS GC,即Major GC,并不触发full gc。
full gc,进行元数据区(方法区),新生代,年老代的gc,在年老代空间满或空间不够年轻代晋升年老代时需要的空间时触发,或在年老代提供的担保容量小于历次晋升到年老代对象的平均大小或不允许担保失败时。
经过GC,空间仍然不够新生对象存放,触发OOM。
内存分配
对象优先在Eden分配。大对象(例如超长字符串,数组)直接进入年老代。长期存活的对象进入年老代(默认经过15次GC,可用调节参数)。如果Survivor空间中相同年龄所有对象大小总和超过survivor空间的一半,年龄大于或等于该年龄的对象直接进入年老代,无须等到jvm设置的年龄要求。