在Java成为如今炙手可热的编程语言之一,主要原因就包括虚拟机成功引入GC收集这项技术。Java的内存管理避免了绝大多数内存泄漏的问题,同时也让程序员能更专心的处理业务逻辑。每每看到这样话的时候,心里其实无数头马儿奔腾而过。在实际的编码过程中,关于GC收集的事情基本上确实没有考虑过,也做到了专心处理业务逻辑,呵呵呵。不过面试过程关于垃圾收集部分的面试题确实是如剥洋葱一样,一个层接着一层,一个接着一个,辣眼睛。扯的有点远了,对于一个称职的程序员说,不一定能精通这块知识,但是做到熟悉还是很有必要的,除非你一心要写CURD的代码,并且乐此不疲。
在学习垃圾收集器和垃圾收集算法之前,关于JVM的内存分布和管理和什么时候判断这个对象是要回收的对象还是很有必要的,建议学习垃圾收集之前先复习下。《JVM内存分配》、《判断对象是否存活(引用技术法、可达性分析法、最终判定)?》
1、标记-清除算法:
标记-清除算法(Mark-Sweep)是最基础的收集算法,分为标记和清除两个阶段:
标记阶段:标记出所要回收的对象;
清除阶段:统一回收所有已被标记的对象。
配置这个阶段的操作也产生了不足之处:一是效率效率问题,标记和清除的效率都不高;另外一个是空间问题,标记清除之后会产生大量不联系的内存碎片,从而在后续为大对象分配内存时,产生内存不足的假象,但是却会提前触发另一次垃圾回收。说道这里是不是会想起数组这种数据结构,数组的删除动作,产生的内存碎片也是不连续的。
标记清楚算法示意图:
2、复制算法:
为了解决标记-清除算法的效率问题,复制算法(Copying)应用而生。复制算法是将原来的内存一分为二。当其中的一的内存用完时,将还存量的对手,复制到另一块内存之上,然后把已使用过的内存全部清理,这样每次只需要对整个半区进行内存回收,只需要移动栈顶指针,解决了标记-清除算法中内存碎片的问题。但是同时也出现了其他问题,内存缩小为原来的一半,虽然效率提高了,但是也付出了沉痛的代价。先看下流程图:
目前复制算法依然是用来回收新生代(根据对象存活几率而定)对象的算法,不过内存分配比例不是1:1,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
3、标记-整理算法:
复制算法主要针对大部分朝生夕死的新生代对象,针对存活率较高的对象,复制算法就要进行较多的复制,效率会变低。而且需要额外的空间进行分配担保,以应对内存中所有对象100%存活的极端情况。因此,更适合老年代的标记-整理算法(Mark-Compact)出现了。标记-整理算法的标记阶段和标记-清除算法的标记阶段相同,只不过标记对象是老年代,而整理阶段是真将让所有存活的对象都向一端移动,然后直接清除掉端边界以为的内存。
示例图:
4、分代收集算法:
首先分代收集算法并不是一种新的回收算法,而是根据对象不同的生活周期将内存划分为几块,这样虚拟机就可以根据不同的内存和对象采用最适当的收集算法。
堆空间的划分:
在Java虚拟机中,各种对象的生命周期会有着较大的差别,大部分对象生命周期很短暂,少部分对象生命周期很长,有的甚至和应用程序以及Java虚拟机的运行周期一样长。因此,应该对不同生命周期的对象采取不同的收集策略,根据生命周期长短将它们分别放到不同的区域,并在不同的区域采用不同的收集算法,这就是分代的概念。
现在主流的Java虚拟机的垃圾收集器都采用分代收集算法(Generational Collection)。Java堆区基于分代的概念,分为新生代(Young Generation)和老年代(Tenured Generation),其中新生代再细分为Eden空间、From Survivor空间和To Survivor空间。因为Eden空间大多对象生命周期很短,所以新生代的空间划分并不是均分的,HotSpot虚拟机默认Eden空间和两个Survivor空间的所占的比例为8:1。
分代收集:
根据Java堆区的空间划分,垃圾收集的类型分为两种,它们分别是:
Minor Collection:新生代垃圾收集。
Full Collection:对新生代、老年代和永久代(JDK8 取消永久代,Full Collection扫描不到替代永久代的元空间)进行收集,又可以称作Majjor Collection。它的收集频率较低,耗时较长。
当执行一次Minor Collection时,Eden空间的存活对象会被复制到To Survivor空间,并且之前经过一次Minor Collection并在From Survivor空间存活的仍年轻的对象也会复制到To Survivor空间。
有两种情况Eden空间和From Survivor空间存活的对象不会复制到To Survivor空间,而是晋升到老年代。一种是存活的对象的分代年龄超过-XX:MaxTenuringThreshold(用于控制对象经历多少次Minor GC才晋升到老年代)所指定的阈值。另一种是To Survivor空间容量达到阈值。
当所有存活的对象被复制到To Survivor空间,或者晋升到老年代,也就意味着Eden空间和From Survivor空间剩下的都是可回收对象,示例图:
垃圾收集器是内存回收的具体表现。
HotsSpot垃圾收集器的联系图:各个垃圾收集器的连线表示两个垃圾收集器可以配合工作
垃圾收集的种类和特点:
垃圾收集器 | 算法 | 使用位置 | 运作形式 | 可配合收集器 | 描述 |
Serial | 复制算法 | 默认新生代收集器(Client模式) | 单线程 | CMS、Serial Old | 单线程下,Client模式默认新生代收集器 |
ParNew | 复制算法 | 默认新生代收集器 | 单线程 | CMS | Serial的多线程版本,Server默认新生代收集器,线程数=CPU数量 |
Parallel Scavenge | 复制算法 | 新生代 | Parallel Old、Serial Old | 多线程、目标关注吞吐量 | |
Serial Old | 标记-整理算法 | 老年代 | Parallel Scavenge、Serial、ParNew | Serial的老年代版本,单线程,Client模式默认老年代收集器 | |
Parallel Old | 标记-整理算法 | 老年代 | Parallel Scavenge | Parallel Scavenge老年代版本、多线程、关注吞吐量 | |
CMS | 标记-清除算法 | 老年代 | Serial、ParNew | 并发低停顿、关注最短停顿时间 | |
G1 | 复制+标记整理了 | 新生代+老年代 | 面向服务端应用、整体基于标记整理,局部(两个Region见)基于复制 |
上面讲述的是HotsSpot虚拟机也就是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。而对于虚拟机还有很多种类,比如IBM J9等等。补充:
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
-XX:+PrintCommandLineFlagsjvm参数可查看默认设置收集器类型
-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断