JVM-GC(1)
分代收集理论
分代收集建立在两个分代假说之上:
- 弱分代假说:绝大多是对象都是朝生夕灭的/大多数对象都在年轻时死亡。
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡/越老的对象越不容易死亡。
如果大量需求回收的对象都是朝生夕灭的,把它们都集中放在一个区域,那么回收时只需标记存活的对象,就能以低消耗回收到大量的对象;
相对的,剩下的难以消亡的对象,也可以集中放在一起,就可以减少对这个区域进行对象的标记和回收,也是减低收回的消耗。
GC算法
4种最基本的垃圾回收策略:
- 标记-清除
- 标记-复制
- 标记-整理
- 引用计数
标记-清除
最基础的垃圾回收算法。分为标记
和清除
两个阶段。标记阶段可标记需要回收的对象或存活对象,再对未标记的对象进行回收。
缺点:
- 执行效率不稳定,如果Java堆中包含大量对象,其中大部分都是需要被回收的,这时就需要大量的标记和清除的动作,导致标记和清除这两个阶段的执行效率会由于对象数量的增加而降低。
- 导致内存空间碎片化的问题。标记、清除之后会产生大量
不连续
的内存碎片,碎片太多会导致下次分配大对象时无法找到足够的连续内存空间而提前触发GC。
标记-复制
为了解决标记-清除
算法中,面对大量可回收对象时执行效率低的问题。
一种实现:半区复制算法
将可用内存划分为大小相等的两块,分别为fromspace
和tospace
。每次只使用使用其中一块。当这块内存用完了,就把还存活的对象复制到另一块,然后再把这块内存清除。如果内存中对象大多数是都是存活的,那这种算法将会产生大量的内存空间复制开销;相反的,对于大多是对象都是可回收的情况,就只要复制少数对象。由此可以推断,标记-复制算法似乎适用于存放生命周期短的对象区域。
由于每次回收都是针对半个区域整体进行,所以这半个区域不会存在内存碎片,后面进行内存分配时就不需要考虑空间碎片的复杂情况,只要移动堆顶指针按顺序分配即可——指针碰撞
。
缺点:
- 可用内存只有原来的一半,会造成空间浪费。
Apple式回收
标记-整理
复制算法在对象存活率较高的是很好需要进行较多的复制操作,会使回收器效率降低。当内存中所有对象都100%存活的情况,复制算法就失去了意义,所以和上述复制算法介绍最后推断的一致,复制算法一般不适用于老年代,而是适用于年轻代。
标记-整理
算法与标记-清除
算法本质的区别在于:标记-整理
属于移动式
回收算法,而标记-清除
属于非移动式
回收算法。这里的移动指的是移动存活的对象。
是否移动存活对象的对比
- 移动:如果移动存活对象,尤其是移动每次回收都有大量存活对象的老年代,移动和更新这些对象的引用都是一种极重的操作,还有可能会造成
STW
的情况。 - 不移动:如果不移动存活对象,就会像标记-清除那样带来内存碎片化的问题,这时就需要依赖
空闲链表
的内存分配策略,而复制
算法和标记-整理
算法通常使用简单且快速的顺序分配
策略,与分区适用空闲链表分配
相比,其速度较快,而更重要的是,顺序分配
实现的简单性通常意味着更高的可靠性。
总结
基于吞吐量的比较
尽管快速的回收很重要,但更快的回收并不意味着程序的吞吐会变高。在一个配置良好的系统中,垃圾回收应当只占用系统整体的一小部分资源,如果更快的回收器会占用系统资源,很有可能会使系统的吞吐量降低。
在不同的场景下-堆小时使用复制
算法,或使用不同的优化手段-在标记-清除
中使用懒惰清扫
,都会有不错的性能。
基于停顿时间的比较
另外一个需要关注的指标是,垃圾回收会给系统的执行造成停顿。
基于内存空间的比较
并发与并行
- 并行:并行描述的是垃圾收集器线程之间的关系,通常默认此时用户线程处于等待状态。
- 并发:并发描述的是垃圾收集器线程与用户线程之间的关系,说明统一时间内,垃圾收集器线程与用户线程都在运行。
垃圾收集器
Serial收集器
Serial收集器是一个单线程工作的收集器,这里的单线程有两个含义:
- Serial收集器使用单线程进行垃圾收集。
- Serial收集器在进行垃圾收集时,用户线程处于等待状态,也就是STW。
(此处应有图)
Serial收集器一般应用在新生代中,配合老年代的Serial Old收集器使用。其中,新生代使用的是标记-复制
算法,老年代使用的是标记-整理
算法,两种收集器在收集阶段都是需要STW的。
由于Serail收集器在进行垃圾收集处理时,单线程收集的同时还会造成STW,所以对于新生代内存比较大和有低延迟要求的系统不太友好:当堆空间较大时意味着单线程模式下扫描堆会更耗时,而收集时需要STW意味着用户线程会在垃圾收集的这段时间内无法工作。相反,Serial收集器在一些新生代内存比较小的场景下表现较好。
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本。
(此处应有图)
从运行示意图中可以看出,ParNew在新生代进行收集时,是以多线程模式来处理的,同时也会造成STW。
除了可以和Serial Old收集器配合外,ParNew还是CMS收集器唯一能配合使用的新生代收集器。为什么CMS不能和Parallel Scavenge收集器配合使用呢?是因为Parallel Scavenge收集器和CMS不一样,没有使用HotSpot定义的那套GC框架。
HotSpot有一个分代GC框架,Serial/Serial Old/ParNew/CMS都是基于这个框架实现。使用这个框架的新生代收集器和老年代收集器可以任意搭配使用,就是所谓的“mix-and-match”。而Parallel Scavenge和G1不是使用这个GC框架来实现的,所以这就是为什么CMS只能选择ParNew作为新生代收集器来搭配使用的原因。
目前CMS已经把ParNew并入自身实现中,取消了-XX:+UseParNewGC
参数。默认情况下,ParNew开启的工作线程数与处理器核心数量相同,在处理器核心非常多的环境中,可以使用-XX:ParallelGCThreads
参数来配置ParNew开启的垃圾收集线程数。
Parallel Scanvenge收集器
Parallel Scanvenge收集器也是一款新生代收集器,同样是基于标记-复制算法。和其他垃圾收集器不同,Parallel Scanvenge收集器关注的吞吐量,所以也叫Throughput收集器。吞吐量是评价垃圾收集器的一个重要指标,吞吐量=/。
Parallel Scanvenge收集器有3大特性参数:
- -XX:MaxGCPauseMillis 控制最大垃圾收集器停顿时间
- -XX:GCTimeRatio 设置吞吐量大小
- -XX:+UseAdaptiveSizePolicy 自适应调节策略
-XX:MaxGCPauseMillis
允许设置一个大于0的毫秒数。将停顿时间设置过小并不意味着垃圾收集所花费的时间一定少或者收集速度块,垃圾收集停顿时间是以牺牲吞吐量和新生代大小作为代价换取的:当新生代设置的小一些,会直接导致垃圾收集得更频繁,使得用户线程的总停顿时间相应变长,吞吐量也会随之下降。(所以应该怎么设?和调优有关。)
-XX:GCTimeRatio
应当为一个大于0小于100的整数。如果设置为N,那么用户代码执行时间:总执行时间=N:N+1,而垃圾收集器的允许执行时间占比为1/N+1.
-XX:+UseAdaptiveSizePolicy
当开启时,不需要人工指定新生代的大小-Xmn
、Eden和Survivor区的比例-XX:SurvivorRatio
、晋升老年代对象大小-XX:PretenureSizeThresHold
等细节参数,JVM会根据当前系统的运行情况收集性能监控信息,动态的去调整这些参数。
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。
Serial Old收集器有一种特殊的使用场景:作为CMS收集器触发并发失败Concurrent Mode Failed
时的后备预案。
(此处应有图)
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记-整理算法实现。在关注吞吐量或者处理器资源稀缺的场合,可以考虑Parallel Scavenge+Parallel Old组合。
(此处应有图)
网友评论