垃圾收集 Garbage Collection 通常被称为GC,即Java垃圾回收机制。
Java中默认声明的就是强引用,:只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError
在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,实现缓存技术,比如网页缓存,图片缓存等。
无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
通过一个虚引用申明。那么它就和没有任何引用一样,它随时可能会被回收仅用来处理资源的清理问题,比Object里面的finalize机制更灵活。get方法返回的永远是null,Java虚拟机不负责清理虚引用,但是它会把虚引用放到引用队列里面。
引用队列
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾s回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。
在程序运行时,内存空间是有限的,那么如何及时的把不在使用的对象清除,将内存释放出来,这就是GC要做的事。
理解GC机制就要从“GC的区域在哪里”“GC回收的对象是什么”“什么时候出发GC”“GC做了什么事”
1.1 需要GC的内存区域
jvm中,程序计数器、虚拟机栈、本地方法栈都是随着线程而生随着线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于Java堆和方法区中,在程序运行期间,这部的内存的分配和使用都是动态的
1.2 GC回收的对象是什么
需要进行回收的对象就是不在存活的对象,那么如何判断一个对象是否存活的,有两种常用方法:引用计数法和可达性分析法。
在Java语言中,GCRoots包括:虚拟机栈中引用的对象,方法区中类静态属性实体引用的对象。方法区中常量引用的对象。本地方法栈中JNI引用的对象。
生存还是死亡?
都要经历两次标记过程:1.是否有GCroot引用链 2.是否执行finalize()方法释放空间
(1)对象没有覆盖finalize()方法
(2)finalize()方法已经被虚拟机执行过了
这两种情况都判定为不用执行finalize()方法。
1.3 什么时候触发GC
(1)程序调用System.gc时可以触发
(2)系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)
GC又分为Minor GC 和 Full GC (也称Major GC)
MinorGC触发条件:当Eden区满时,触发MinorGC
FullGC触发条件:
1.4 GC做了什么事
主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。
GC常用的算法:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。
目前主流的JVM(HotSpot)采用的时分代收集算法。
1、标记-清除算法
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行GC操作
优点
最大的优点是,标记-清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重的是,这个算法并不移动对象的位置。
缺点
他的缺点就是效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况
图例:
2、 标记-压缩算法(标记-整理)
标记压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有的对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有的存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除,这样就达到了标记-整理的目的。
优点
该算法不会像标记清除算法那样产生大量的碎片空间。
缺点
如果存活的对象过多,整理阶段将会执行较多的复制操作,导致算法效率降低
图例
左边是标记阶段,右边是整理之后的状态。可以看到,该算法不会产生大量碎片内存空间
3、复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
注意
这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内
优点
实现简单;不产生内存碎片
缺点
每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半
图例
4、分代收集算法
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。
具体过程:新生代(Young)分为Eden区,From区与To区
当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。
这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区,
再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。
经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。
老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收。如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。
最基本、发展历史最久的收集器,这个收集器是一个采用复制算法的单线程的收集器,单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。后者意味着,在用户不可见的情况下要把用户正常工作的线程全部停掉,这对很多应用是难以接受的。不过实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效。用户桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代停顿时间在几十毫秒最多一百毫秒,只要不是频繁发生,这点停顿是完全可以接受的。Serial收集器运行过程如下图所示:
说明:1. 需要STW(Stop The World),停顿时间长。2. 简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括使用的也是复制算法。ParNew收集器除了多线程以外和Serial收集器并没有太多创新的地方,但是它却是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作(看图)。CMS收集器是一款几乎可以认为有划时代意义的垃圾收集器,因为它第一次实现了让垃圾收集线程与用户线程基本上同时工作。ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于线程交互的开销,该收集器在两个CPU的环境中都不能百分之百保证可以超越Serial收集器。当然,随着可用CPU数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。ParNew收集器运行过程如下图所示:
Parallel Scavenge收集器也是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器,但是它的特点是它的关注点和其他收集器不同。介绍这个收集器主要还是介绍吞吐量的概念。CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是打到一个可控制的吞吐量。所谓吞吐量的意思就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总运行100分钟,垃圾收集1分钟,那吞吐量就是99%。另外,ParallelScavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。
停顿时间短适合需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务。
虚拟机提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要以为前者越小越好,GC停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切,Parallel Scavenge收集器也被称为“吞吐量优先收集器”。Parallel Scavenge收集器有一个-XX:+UseAdaptiveSizePolicy参数,这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden区和Survivor参数等细节参数了,虚拟机会根据当前系统的运行情况手机性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。
Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理算法”,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK 1.6之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合。运行过程如下图所示:
CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步:
(1). 初始标记,标记GCRoots能直接关联到的对象,时间很短。
(2). 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。
(3). 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。
(4). 并发清除,回收内存空间,时间很长。
其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。运行过程如下图所示:
G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1收集器有以下特点:
(1). 并行和并发。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。
(2). 分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。
(3). 空间整合。基于标记 - 整理算法,无内存碎片产生。
(4). 可预测的停顿。能简历可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。