GC就是JVM中自动内存管理机制的具体实现,GC的工作任务主要可以划分为两大块,分别是内存的动态分配和垃圾回收。 在内存分配之前,GC首先会对内存空间进行划分,考虑到JVM中存活对象的生命周期会具有两极化,因此会采取不同的垃圾回收策略,分代收集由此诞生。目前几乎所有的GC都是采用分代收集算法执行垃圾回收的,所以Java堆区如果还要更进一步细分的话,还可以划分为新生代,老年代,其中新生代又可以划分为Eden
空间,From Survivor
空间(幸存区),To Survivor
空间(幸存区)。当内存空间划分完成
后,GC就可以为新对象分配内存空间,并区分出存储在内存中的对象哪些是存活的,哪些是已经死亡的,如果对象已经死亡,就可以将其标记为垃圾。为了避免内存溢出,GC会释放掉这些已经死亡的对象所占的内存空间,便于有足够的内存空间分配给新的对象实例,一般来说当内存空间消耗到一定的阈值,GC就会执行垃圾回收。
JVM给每个对象定义了一个对象年龄计数器。一个新对象在创建后,会被分配到堆区年轻代的Eden
空间,如果该对象经历了一次Minor GC
后仍然存活着,并且还能被年轻代幸存区(Survivor)所容纳,假设将其转移到From Survivor
中,并将其年龄增加到1岁,当From Survivor
的空间到达一定的阈值,就会进行一次Minor GC
,将垃圾对象进行回收,并将仍然存活的对象复制到年轻代的To Survivor
内,如果该对象还活着,就对该对象的年龄加1岁,如果该对象年龄增加到一定程度(默认15岁),就会晋升为老年代对象。
针对不同年龄段的对象分配原则如下所示。
(1)对象优先分到在Eden
区,如果Eden
区没有足够的空间时,虚拟机执行一次Minor GC
;
(2)大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden
区和两个Survivor
区之间
发生大量的内存拷贝(新生代采用复制算法收集内存)
(3)长期存活的对象进入老年代,虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC
,那么对象会进入
Survivor
区,之后每经过一次Minor GC
那么对象的年龄增加1,直到阈值对象进入老年区;
(4)动态判断对象的年龄。如果Survivor
区中相同年龄的所有对象大小的中和大于Survivor
空间的一半,年龄大于或等于该年龄可以直接进入老年代;
(5)空间分配担保,每次进行Minor GC
时,JVM会计算Survivor
区移至老年代区的对象的平均大小,如果这个值大于老年区
的剩余大小则进行一次Full GC
,如果小于检查HandlePromotionFailure设置,如果true则只进行Minor GC
,如果false则进行
Full GC
。
大多数的对象都在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们说发生了一次 Minor GC
。
经过一系列的Minor GC
依然存活的对象会被复制到老年代区,这块内存区域一般大于年轻代,因为它更大的规模,GC发生的次数
比年轻代的少。当对象从老年代消失时,我们说Major GC
(或Full GC
)发生了。
除了System.gc
外 触发Full GC执行的情况有如下四种。
1.老年代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下
错误java.lang.OutMemoryError:Java heap space
。
为了避免以上两种情况引起的Full GC
,调优时应尽量做到让对象在Minor GC
阶段被回收、让对象在新生代多存活一段时间即
不要创建过大的对象及数组。
2.Permanet Generation空间占满
Permanet Generation
(持久代)中存放的为一些Class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation
可能会被占满,在未配置为采用CMS GC
的情况下会执行Full GC
。如果经过Full GC
仍然回收不了,那么JVM会抛出错误信息:java.lang.OutOfMemoryError:PermGen space
。
为避免Perm Gen
占满造成Full GC
现象,可采用的方法增大Perm Gen
空间或转为使用CMS GC
。
3.CMS GC时出现promotion failed 和 concurrent mode failure`
对于采用CMS进行老年代GC的程序而言,尤其要注意日志中是否有Promotion Failed
和Concurrent Mode Failure
两种状况,
当着两种状态出现时可能会触发Full GC
。Promotion Failed
是在进行Minor GC
时,Survivor Space
放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure
是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。
应对措施为增大survivorspace
老年代空间或调低触发并发GC的比率。
4.统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到老年代导致老年代空间不足的现象,在进行Minor GC
时
做了一个判断,如果之前统计得到的Minor GC
晋升到老年代的平均大小大于老年代的剩余空间,那么直接触发Full GC
。
目前有两种比较常用的垃圾标记算法,分别是引用计数算法和根搜索算法。
引用计数法在GC执行垃圾回收之前,首先要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会执行在垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们称之为垃圾标记阶段。
引用计数器的实现很简单,对于一个对象A,只要有任何一对对象应用了A则A的计数器就加1,当引用失效时,引用计数器就减1,只要对象的计数器为0,则表示该对象不可能被使用,也就是说,引用计数器的实现只需要对每个对象配置一个整形的计数器即可。引用计数器算法的一大优势就是不用等到内存不够用的时候才进行垃圾回收,完全可以在赋值操作的同时检查计数器是否为0,
如果是的话就可以立即回收。
缺陷:对象循环依赖,A对象依赖B对象,同时B对象又引用A对象,这样引用计数法就不能将该两个不再使用的对象标记为垃圾对象。
HotSpot和大部分JVM中是是使用根搜索算法作为垃圾标记的算法实现。引用计数法尽管实现简单,执行效率也不错,但本身却存在
一个较大的弊端,甚至会影响到垃圾标记的准确性。相对于引用计数法而言,根搜索算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决在引用计数算法中一些已经死亡的对象由于循环相互依赖而未被回收的,能被正常的回收掉。
简单来说,根搜索算法是以根对象集合作为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中的存活对象都会被根对象集合直接或间接连接着)如果目标对象不可达时,就意味着该对象已经死亡,便可以将其标记为垃圾对象。只有能够被根对象集合直接或间接连接的对象才是存活的对象。
这个稍微提一下,堆区的年轻代空间中的,两个幸存区(From Survivor
To Survivor
)的垃圾回收算法就是使用的复制算法
,缺陷 将一块内存一分为二,每次只使用其中一半内存,垃圾回收的时候讲存活的对象全部复制到另一半内存中。永远都只用其中一块内存空间,提高了效率牺牲了内存的一般大小。
由于JDK的版本处于高速迭代过程中,因此Java从发展至今已经衍生了众多的GC版本,比如Serial/Serial Old
收集器、ParNew
收集器、Parallel/Parallel Old
收集器、CMS(Concurrent-Mark-Sweep)收集器,以及从JDK7Update4版开始提供的G1(Garbage-First)收集器等。
基于分代的概念,不同的代空间均活动着不同的GC,比如Serial
收集器就是一个典型的新生代垃圾收集器,它采用复制算法
回收新生代无用对象的内存空间。
从不同的角度来分析垃圾收集器,可以将GC分为不同的类型。
按线程来分,可以分为串行垃圾回收器和并行的垃圾回收器。串行垃圾回收器一次只能使用一个线程进行垃圾回收,而并行垃圾回收器可以一次将开启多个线程同时进行垃圾回收。在并行能力较强的CPU上,使用并行垃圾回收器可以缩短GC的停留时间。
按工作模式来分,可以分为并发式垃圾回收器和独占式垃圾回收器。并发式垃圾回收回收器与应用程序交替工作,以尽可能减少
应用程序的停顿时间;独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。
按碎片处理方式可分为压缩式垃圾回收器和非压缩式垃圾回收器。压缩式垃圾回收器会在回收完成后,对存活的对象进行压缩
整理,消除回收的碎片;非压缩式的垃圾回收器不进行这步操作。
按工作的内存空间,又可以分为年轻代垃圾回收器和年老代垃圾回收器。
如果说Serial
是新生代的单线程垃圾收集器,那么ParNew
收集器则是Serial
收集器的多线程版本。ParNew
收集器除了在采用并行的回收方式执行内存回收外,两款垃圾回收收集器之间几乎没有任何去呗,因为ParNew
收集器在新生代中同样也是采用复制算法和Stop the world
机制。
如果说ParNew
收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,确实可以更快的完成垃圾收集,提升程序的吞吐量,但是如果单个CPU的环境下,ParNew
收集器不见得比Serial
收集️器更高效。虽然Seria
l收集器是基于串行回收的,但是由于CPU不需要频繁的做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。
HotSpot的新生代中除了拥有ParNew收集器是基于并行回收的以外,Parallel收集器同样也采用了复制算法,并行回收和Stop-the-World
机制,和ParNew
收集器不同,Parallel收集器可以控制程序的吞吐量大小,因此也被称为吞吐量优先的垃圾收集器。和
Serial
收集器一样,Parallel
收集器也提供用于执行老年代的垃圾收集的Parallel Old
收集器,Parallel
Old收集器采用了标记-压缩算法,但同样也是基于并行回收和Stop-the-World
机制。在程序吞吐量优先的应用场景中,Parallel
收集器和Parallel Old
的收集器的组合,在Server模式下的内存回收性能很不错。在程序开发过程中,开发人员可以通过选项
-XX:+UseParallelGC
手动指定使用Parallel收集器执行内存回收任务。
CMS全称是Concurrent-Mark-Sweep
。在程序吞吐量优先的应用场景中,Parallel
收集器和Parallel Old
收集器的组合,在Server模式下的内存回收性能还不错。但是在某些响应速度要求比较高的项目中,大家总是希望系统能够快速做出响应,而不是过多的延迟。于是就诞生了CMS收集器,它是一款优秀的老年代垃圾收集器,也可以被称作Mostle-Concurrent
收集器。CMS天生为并发而生,低延迟是它的优势,不过垃圾收集算法却没有采用标记-复制算法,而是采用标记-清除算法,并且也会因为Stop-the-World
机制而出现短暂的暂停。
G1收集器全程Garbage-First。G1垃圾收集器完全支持在JDK7 Update 4和以后的版本。G1收集器是一个服务器类型的垃圾收集器,目标用于大存储器的多处理器机器。它符合垃圾收集以很高的概率 ,暂停时间的目标,同时实现高吞吐量。整个堆的操作,比如全局标记,同时执行的应用程序线程。这可以防止中断堆或实时数据的大小比例。
G1收集器的设计初衷是为了替代CMS收集器而生,自身的目标是以更高的计算成本为代价,最小化STW中断时间。G1更适合于低延迟的应用程序,比如Web服务器,简单来说,G1是一款基于并行和并发、低延迟以及暂停时间更加可控的区域划分代垃圾收集器。