垃圾回收关注的是堆heap内存,需解决三个问题:哪些内存需要回收?什么时候回收?如何回收?
垃圾回收针对不同的分区又分为MinorGC和FullGC,不同分区的触发条件又有不同。总体来说GC的触发分为主动和被动两类:
System.gc()
发起GC(不一定马上甚至不会GC)无论哪种情况,GC发起的方式都是一致的:
在对象中添加一个引用计数器,每当一个地方引用它时,计数器就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
问题:循环引用,循环依赖
又叫根搜索算法,通过一系列的GC Roots,也就是根对象作为起始节点集合,从根节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连。
步骤:
为了减少停顿时间,需要让垃圾回收器和用户线程同时运行,即并发标记。
理论前提:该算法的全过程都需要基于一个能保障一致性的快照中才能够分析,这意味着必须全程冻结用户线程的运行。
三色标记
在遍历对象图的过程中,把访问的对象按照<是否访问过>这个条件标记成以下三种颜色:
但垃圾回收器和用户线程同时运行的。
垃圾回收器在对象图上面标记颜色,而同时用户线程在修改引用关系,引用关系修改,对象图就发生变化,这样就有可能出现两种后果:
被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程。
可达性分析算法的起点是一组称为GC Roots的对象,包括:
HotSpot怎么快速找到GC Root?
之所以要快速,是因为,执行GC时,是Stop The World,进程响应中断,GC后还要进行对象引用链追溯、对象的复制拷贝等工作,故而GC Roots遍历需要极高的效率。
包括HotSpot在内的现代JVM采取用空间换时间的策略,核心思想:提前将GC Roots的位置信息记录起来,GC时,按图索骥,快速找到它们。
HotSpot使用一组称为OopMap的数据结构。ordinary object pointer,普通对象指针,就是指一个Java对象,在JVM中或Hotspot源码层面,对应一个C++实例。在Java层面,叫Java对象,在JVM层面,叫oop。与之对应的就是klass,就是一个Java类在JVM中对应的C++实例。Map实际上是地图。
在类加载完成时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在栈和寄存器中哪些位置是引用。在GC扫描时,就可以直接知道哪些是可达对象。
GC Roots的位置信息也就是在OopMap中。HotSpot源码中关于OopMap相关数据的创建代码分散在各个地方,可以通过在源码目录下搜索new OopMap关键字找到它们。在函数返回,异常跳转,循环跳转等时刻,JVM将记录OopMap相关信息供后续GC时使用。
JVM需要知道一个64bit的数据是一个引用还是一个long型变量?如果它不知道的话,如何进行内存回收呢?
保守式GC和准确式GC:
保守式GC:虚拟机不能明确分辨上面说的问题,无法知道栈中的哪些是引用,采用保守的态度,如果一个数据看上去像是一个对象指针(比如这个数字指向堆区,那个位置刚好有一个对象头部),那么这种情况下就将其当作一个引用。这样把可能不是引用的也当成引用,现实点的说就是懒政,这种情况下是可能产生漏网之鱼没有被垃圾回收的
准确式GC:明确知道一个64bit的数字是一个long还是一个对象引用。现代商业JVM均采用这种更先进的方式,JVM知道栈中和对象的结构中每一个地址单元里装的是什么东西,不会错杀漏杀。
安全点:
HotSpot只在特定的位置生成OopMap,这些位置称为安全点。程序执行过程中并非所有地方都可以停下来开始GC,只有在到达安全点是才可以暂停。安全点的选定基本上以是否具有让程序长时间执行的特征选定的。比如说方法调用、循环跳转、异常跳转等。具有这些功能的指令才会产生Safepoint。
Mark-Sweep,最基础:
不足:
对应的垃圾收集器是CMS收集器
Mark-Compact,复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点, 提出标记-整理算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
对应的垃圾收集器是Serial Old收集器、Parallel Old收集器。
Copying,为解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。
不足:内存缩小为原来的一半
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存划分为一块较大的(新生代)Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor当回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的额Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被浪费。当然,98%的对象可回收只是一般场景下的数据,没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配。
当前商业虚拟机都采用这种算法,思想:对堆内存区域进行分代,新生代和老年代,不同的区域采用不同垃圾收集算法。新生代用复制算法,老年代用标记-整理或标记-清除算法。
年轻代引用老年代的这种跨代不需要单独处理。
但是老年代引用年轻代的会影响young gc,这种跨代需要处理
主要垃圾收集器如下,图中标出它们的工作区域、垃圾收集算法,以及配合关系:
最基础、历史最悠久的收集器。如同它的名字(串行),它是一个单线程工作的收集器,使用一个处理器或一条收集线程去完成垃圾收集工作,进行垃圾收集时,必须暂停其他所有工作线程(STW,独占式),直到垃圾收集结束。适合单CPU服务器。
Serial是一个新生代收集器,Serial Old是Serial收集器的的老年代版本。Serial/Serial Old收集器的运行过程如图:
实质上是Serial收集器的多线程并行版本,使用多条线程进行垃圾收集,多CPU,停顿时间比Serial少。ParNew/Serial Old收集器运行示意图:
ParallerGC,一款新生代收集器,基于标记-复制算法实现,也能够并行收集。和ParNew有些类似,但Parallel Scavenge主要关注的是垃圾收集的吞吐量。高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。吞吐量,就是CPU用于运行用户代码的时间和总消耗时间的比值,比值越大,说明垃圾收集的占比越小。
Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。同样是老年代的收集器,采用标记-清除算法。GC分为四步:
CMS GC运行示意图如下:
Garbage First(G1)GC是垃圾收集器的一个颠覆性的产物,开创局部收集的设计思路和基于Region的内存布局形式。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异。以前的收集器分代是划分新生代、老年代、持久代等。
G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。
这样就避免收集整个堆,而是按照若干个Region集进行收集,同时维护一个优先级列表,跟踪各个Region回收的价值,优先收集价值高的Region。
G1收集器的运行过程大致可划分为以下四个步骤:
对比
有CMS,还要引入G1?G1主要解决内存碎片过多的问题。
CMS优点:CMS最主要的优点在名字上已经体现出来——并发收集、低停顿。
CMS3个明显的缺点:
如何选择GC,即需要考虑各个GC的适用场景:
新创建的对象优先在新生代Eden区进行分配,如果Eden区没有足够的空间时,就会触发Young GC来清理新生代。
频繁Minor GC?通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,可通过增大新生代空间-Xmn
来降低Minor GC的频率。
触发条件有多个,Full GC时会STW,STOP THE WORD。
System.gc()
方法-XX:+ DisableExplicitGC
来禁止RMI
调用System.gc()
。jmap -dump
等命令Full GC的排查思路大概如下:
# 查看堆内存各区域的使用率以及GC情况
jstat -gcutil -h20 pid 1000
# 查看堆内存中的存活对象,并按空间排序
jmap -histo pid | head -n20
# dump堆内存文件
jmap -dump:format=b,file=heap pid
jmap -histo
命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象- XX:MaxTenuringThreshold
-XX:PretenureSizeThreshold
- XX:MaxTenuringThreshold
才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。垃圾回收的过程将伴随着对象的迁徙,而一旦对象迁徙之后,之前指向它的所有引用(包括栈里的引用、堆里对象的成员变量引用等等)都将失效。