Java垃圾回收

时间:2023-12-05 15:23:32

垃圾收集算法

引用计数

堆中的每个对象都有一个引用计数,当对象被引用时引用计数加1,当对象的引用被重新赋值或超出有效区域时引用计数减1,当一个对象被回收后,它所引用的对象的引用计算减1。当一个对象的引用计数变为0时就被回收。

引用计数的优点:

垃圾收集器可以很快地执行,当一个对象的引用数为0时就可以回收这个对象,垃圾收集交织在程序的正常执行过程中,不用长时间中断程序的正常执行。

引用计数的缺点:

  1. 每次引用计数的增加和减少会带来额外的开销
  2. 无法检测出循环引用

根搜索算法

垃圾检测通过建立一个根对象的集合(局部变量、栈桢中的操作数,在本地方法中引用的对象,常量池等)并检查从这些根对象开始的可触及性来实现。根对象总是可访问的,如果存在根对象到一个对象的引用路径,那么称这个对象是可触及的或活动对象,否则是不可触及的,不可触及的对象就是垃圾对象。

标记清除

分为标记和清除两个阶段,在标记阶段,垃圾收集器跟踪从根对象的引用,在追踪的过程中对遇到的对象打一个标记,最终未被标记的对象就是垃圾对象,在清除阶段,回收垃圾对象占用的内存。可以在对象本身添加跟踪标记,也可以用一个独立的位图来设置标记。

标记清除法是基础的收集算法,其他算法大多时针对这个算法缺点的改进。

有两个缺点:

  1. 效率
  2. 存在内存碎片

复制算法

将内存划分为大小相等的两个区域,每次只使用其中的一个区域,当这个区域的内存用完了,就将可触及的对象直接复制到新的区域并连续存放以消除内存碎片,当可触及对象复制完后,清除旧内存区域,修改引用的值。

这种算法的缺点很明显,可使用内存变为了原来的一半,太过浪费。

一般情况下,新生代中的对象大多生命周期很短,也就是说当进行垃圾收集时,大部分对象都是垃圾,只有一小部分对象会存活下来,所以只要保留一小部分内存保存存活下来的对象就行了,用不着使用一半的内存。在新生代中一般将内存划分为三个部分:一个较大的Eden空间和两个较小的Survior空间(一样大小),每次使用Eden和一个Survior的内存,进行垃圾收集时将Eden和使用的Survior中的存活的对象复制到另一个Survior空间中,然后清除这两个空间的内存,下次使用Eden和另一个Survior,HotSpot中默认将这三个空间的比例划分为8:1:1,这样被浪费掉的空间就只有总内存的1/10了。

这样的内存空间划分是基于这样一种假设,即每次垃圾收集时大部分对象都是垃圾,只有少部分对象存活。如果遇到例外的情况怎么办,在某次垃圾收集时存活下来的对象超过了预留的那个Survior空间的总大小,这就需要依赖其他的内存进行分配担保了(参考分代收集,前面的描述中也说了这是新生代中的方法)

标记整理

普通的标记清除会在内存中留下内存碎片,复制算法如果不想浪费掉50%内存就需要有内存分配担保,一般是内存分代,但总有一代是没有其他代为它担保的。标记整理算法中标记的过程同标记清理一样,但整理部分不是直接清除掉垃圾对象,而是将活动对象统一移动一内存的一端,然后清除边界外的内存区域,这样就避免了内存碎片。也不会浪费内存,不需要其他内存进行担保

分代收集

大多数程序中创建的大部分对象生命周期都很短,而且会有一小部分生命周期长的对象,为了克服复制收集器中每次垃圾收集都要拷贝所有的活动对象的缺点,将内存划分为不同的区域,更多地收集短生命周期所在的内存区域,当对象经历一定次数的垃圾收集存活时,提升它的存在的区域。一般是划分为新生代和老年代。新生代又划分为Eden区,From Survior区和To Survior区。

自适应收集器

监听堆中的情形,并且对应地调用合适的垃圾收集技术。

垃圾收集器

Serial

一个单线程的收集器,在进行垃圾收集时会暂停其他线程的工作,不适合用到Server端的虚拟机,但Client模式的模拟机还是可以用的,因为Client模式下的应用分配到的系统内存一般不大,垃圾收集可以很快完成。优点就是简单高效,没有线程交互开销,可以获得最高的单线程收集效率。

ParNew

Seria的多线程版本,可以多个线程收集垃圾,但如果CPU只有一核且没有超线程,效果就不一定比Serial好了,如果是多核或有超线程,可以保证效果好于Serial,除Seria之外,这是唯一能与CMS收集器配合的垃圾收集器

Parallel Scavenge

使用复制算法的新生代多线程垃圾收集器,Parallel Scavenge收集器的关注点和其他收集器不同,其他收集器的关注点是尽可能缩短垃圾收集时用户线程等待的时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput),即CPU用于运行用户代码的时间与CPU总消耗时间的比值。以缩短用户线程等待时间的收集器适合用于需要与用户交互的程序,而以吞吐量为目标的收集器适合用于不需要和用户太多的交互,以后台运算为目标的任务。

Parallel Scavenge可以通过参数设置每次垃圾收集需要停顿的时间和吞吐量目标,但停顿时间并不是越小越好,这是以牺牲吞吐量和新生代空间为代价的,因为要使垃圾收集停顿时间缩小,只能进行少量多次收集,或减小需要收集的空间大小。

还有一个-XX:UseAdaptiveSizePolicy参数,指定这个参数后,就不需要手工指定新生代的大小、Eden区和Survior区的比例大小和晋升老年代对象年龄等细节参数了,虚拟机会根据收集到的信息动态调整这些参数,这称为自适应策略。

Serial Old

Serial的老年代版本,单线程收集器,使用"标记-整理"算法,主要被Client模式下的虚拟机使用,当被使用在Server模式时主要有两个用途:

  1. 与Parallel Scavenge配合使用
  2. 作为CMS收集失败时的备选方案。

Parallel Old

Parallel Scavenge的老年代版本,使用"标记-整理"算法,JDK1.6后提供的,在此之前,如果新生代选择了Parallel Scavenge,老年代只能选择Serial Old,由于Serial Old是单线程的垃圾收集器,可能会影响收集性能。Parallel Old出现后,就可以分别在新生代和老年代选择Parallel Scavenge和Parallel Old组合了。

CMS(Concurrent Mark Sweep)

以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法,整个回收过程分为以下4步:

  • 初始标记(CMS Initial Mark)
  • 并发标记(CMS Current Mark)
  • 重新标记(CMS Remark)
  • 并发清楚(CMS Concurrent Sweep)

初始标记与重新标记阶段仍会暂停用户线程的运行。

初始标记只是记录下GC Root能直接关联到的对象,速度很快。

并发标记就是GC Roots Tracing了,速度较慢,但可以和用户线程同时运行。

重新标记是修正并发标记时由于用户线程运行导致的标记记录变动,这个阶段会使用户线程停顿,停顿时间比初始标记略长,但仍小于重新标记。

并发清除就是清除垃圾对象了,耗时较长,但可与用户线程同时工作。

CMS的缺点

  1. 对CPU资源敏感,并发阶段和用户线程同时运行,影响服务器的响应速度,尤其是CPU核心数少时
  2. 无法处理浮动垃圾,由于并发阶段用户线程同时在运行,可能会在垃圾收集过程中产生新的垃圾,CMS无法处理这部分浮动垃圾,由于在进行垃圾收集时用户线程同时在运行,需要额外的内存空间,所以不能等到内存满时再进行GC,需要预留一部分空间,如果预留的这部分空间不够GC时用户线程创建新对象使用,就会使用预备方法,使用Serial Old进行一次Full GC。
  3. CMS基于“标记-清除”算法,进行垃圾回收后会存在内存碎片,当申请大的连续内存时可能内存不足,此时需要进行一次Full GC,可以通过参数指定进行Full GC后或进行多少次Full GC后进行一次内存压缩来整理内存碎片。

G1(Garbage First)

基于"标记-整理"算法,避免了内存碎片的问题,并可精确地控制垃圾回收时的停顿。

G1收集器可以实现基本不牺牲吞吐量的前提下完成低停顿的内存回收,不同于之前的垃圾回收器,G1收集器的回收区域不是整个新生代或老年代,而是将整个Java堆划分为多个固定大小的区域,并跟踪这些区域里的垃圾堆积程度,在后台维护一个优先列表,优先回收垃圾最多的区域。区域的划分使每次回收时间变短,而优先级的划分使得每次回收的区域可以回收最多的垃圾,这就使用G1收集器可以在有限的时间内获取最高的收集效率。

内存分配与回收策略

对像优先在新生代Eden区分配,当Eden区没有足够的内存时会发生一次Minor GC(新生代GC,Major GC或Full GC是老年代GC)

大对象可以直接在老年代分配内存,可以通过参数指定一个大小,大于这个大小的对象直接在老年代中分配内存。

进行Minor GC时,Eden区和一个Survior区中存活的对象会被复制到另一个Survior区,一个对象每在一次Minor GC中存活下来一次后这个对象的年龄就加1,当这个对象的年龄大于一定值(默认15)就会进入老年代。

如果Survior中相同年龄的对象占用的空间大于Survior空间的一半,那么年龄大于或等于这个年龄的对象会直接进入老年代,而不用等到达到特定年龄

当进行Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果小于,判断是否开启了HandlerPromotionFailure允许担保失败,如果开启了就只进行Minor GC,否则进行Full GC。由于使用的之前Minor GC时的平均大小,如果某一次突然大小变大,导致老年代剩余空间不够,即担保失败,会再进行一次Full GC。

finalize

GC时会对活动对象进行标记,没有被标记的对象就是垃圾对象,但垃圾对象不会直接被清除,垃圾收集器还会判断是否需要执行对象的finalize方法,如果对象没有覆写finalize方法或它的finalize已经被执行过一次,那么是没有必要执行的,否则就认为是有必要执行的,当被判断为有必要执行时,这个对象会被放入一个F-Queue队列中,由一个后台的低优先级的Finalizer线程执行队列中的对象的finalize方法,对象可以在这个方法中中复活自己,即重新被其他对象引用,但这个函数只会被垃圾收集器运行一下,第二次回收这个对象时这个函数不会再被调用。稍后GC会对F-Queue队列中的对象执行第二次标记。