收集器
JVM通过GC来回收堆和方法区中的内存。
GC的基本原理:找到程序中不再被使用的对象,然后回收这些对象所占用的内存。
通常采用收集器的方式实现GC,主要的收集器有引用计数收集器和跟踪收集器。
引用计数收集器
引用计数收集器,通过计数器记录对象是否被引用。当计数器为零时,说明此对象已不再被使用,于是可进行回收。
弊端:
引用计数需要在每次对象赋值时,进行引用计数器的增减,它有一定的消耗。另外,引用计数器对于循环引用的场景没有办法实现回收。JDK在实现GC时,未采用这种方式。
跟踪收集器
跟踪收集器,全局记录数据的引用状态。基于一定条件的触发(如:定时或空间不足时),执行时需要从根集合来扫描对象的引用关系,这可能会造成应用程序暂停,主要有复制、标记-清除、标记-压缩三种实现算法。
复制:从根集合扫描出存活的对象,并将找到的存活对象复制到一块新的完全未使用的空间中。
优缺点:复制收集器方式仅需从根集合扫描所有存活的对象,当要回收的空间中存活对象较少时,复制算法会比较高效,其带来的成本是增加一块空的内存空间及进行对象的移动。
标记-清除:标记-清除采用的方式为从根集合开始扫描,对存活的对象标记,标记完毕后,再扫描整个空间中未标记的对象,并进行回收。
优缺点:标记-清除动作不需要进行对象的移动,且仅对其不存活的对象进行处理。在空间中存活对象较多的情况下较为高效,但由于标记-清除采用的为直接回收不存活对象所占用的内存,因此会造成内存碎片。
标记-压缩:标记-压缩采用和标记-清除一样的方式对存活的对象进行标记,但是清除时则不同。在回收不存活对象所占用的内存空间后,会将其他所有存活对象都往左端空闲的空间进行移动,并更新引用其对象的指针。
标记-压缩在标记-清除的基础上,还须进行对象的移动,成本相对更高,好处则是不产生内存碎片。
JDK中可用的GC:
程序中大部分对象的存活时间都是较短的,少部分对象是长期存活的。基于这个分析,JDK将JVM堆划分为新生代和旧生代,并基于新生代和旧生代中对象存活时间的特征提供了不同的GC实现。
新生代可用GC:
JDK认为新生代中的对象通常存活时间较短,因此选择了基于Copying算法来实现对新生代对象的回收。
根据Copying算法的介绍,在执行复制时,需要一块为使用的空间来存放存活的对象,这就是新生代被划分为Eden区、From区、To区的原因。Eden Space存放新创建的对象,From区或To区的其中一块用于在Minor GC触发时作为复制的目标空间,当其中一块为复制的目标空间时,另一块中的内从则会被清空。
JDK提供了串行GC、并行回收GC、并行GC三种方式来回收新生代对象所占用的内存,对新生代对象所占的内存进行的GC又称为Minor GC。
JDK提供了串行、并行及并发三种GC来对旧生代及持久代对象所占用的内存进行回收。
Full GC
当旧生代和持久代触发GC时,其实是对新生代、旧生代及持久代都进行GC,因此又称为Full GC。当Full GC被触发时,首先按照新生代所配置的GC方式对新生代进行GC,然后按照旧生代的GC方式对旧生代、持久代进行GC。
除直接调用System.gc外,触发Full GC执行的情况,有一下4种:
- 旧生代空间不足
- 持久代空间满
- CMS GC失败触发GC(CMS GC时出现promotion failed和concurrent mode failure)
- 统计得到的Minor GC寄生到旧生代的平均大小大于旧生代的剩余空间
大多数情况下,只须配置JVM堆的大小和持久代的大小就可以让GC符合应用的要求运行。
对访问量、数据量较大的应用而言,如果确定GC对应用的运行状况造成了影响,则可根据应用状况来精确控制内存空间中每个代的大小,选择GC方式及调整GC参数,尽可能降低GC对应用运行的影响。