Java内存管理

Posted EthanPark

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java内存管理相关的知识,希望对你有一定的参考价值。

前文描述了一些关于串行收集器的知识,本文继续针对垃圾收集器进行描述。

并行收集器(parallel collector)

在硬件发展到今天,很多的机器上面的物理内存会更高,并且拥有更多的CPU资源。并行收集器,也被称作吞吐收集器,就是用来更好的利用多个CPU性能,增加垃圾回收的吞吐量的一种垃圾收集器。

并行收集器的年轻代回收

年轻代使用的算法在串行收集器和并行收集器上是一致的。只是使用了多个CPU,通过降低垃圾收集的计算负载来增加了应用的吞吐。如下图:

该图说明了年轻代垃圾回收在串行收集器和并行收集器中的不同,以及STW时间上的差距。

并行收集器的老年代回收

即使使用并行收集器,老年代和永久代的收集,仍然是以STW的方式进行的,多数还是以滑动压缩的方式来进行收集。

何时使用并行收集器

应用使用并行收集器可以令那些包含多个CPU的服务器在进行垃圾回收的时候,减少暂停时间。使用并行收集器的应用通常都是需要大量CPU资源的,比如类似电商订单处理,科学计算等等。

当然,开发者可能会更优先使用parallel compacting collector而不是parallel collector,因为parallel compacting collector是针对年轻代和老年代都有并行优化,而parallel collector只是针对年轻代。

配置并行收集器

在J2SE 5.0 release中,并行收集器在企业级服务器上是默认配置的。在其它的设备之上,并行收集器可以通过使用JVM参数:

-XX:+UseParallelGC

来进行指定。

并行压缩收集器(parallel compacting collector)

并行压缩收集器在J2SE 5.0 update 6中引入。并行压缩收集和并行收集的不同之处在于,并行压缩收集针对老年代回收使用了一个全新的算法。

注意:并行压缩收集器迟早会替代掉并行收集器。

并行压缩收集器的年轻代回收

在年轻代的收集上,并行压缩收集器和并行收集器的算法是一样的,具体可以参考并行收集器的年轻代回收。

并行压缩收集器的老年代回收

当配置了并行压缩收集器的时候,老年代和永久代仍然是以STW的方式来进行收集的,但是配合的是并行滑动压缩算法。收集器会分为三个阶段:

  • 标记阶段:首先,每一代会在逻辑上去分成不同的固定大小的区域,从最开始可达的对象开始,由分配给GC的线程开始执行标记,所有存活的的对象都会进行并行标记。一旦一个对象认为是存活的,这个对象的数据区域的大小和位置都会记录。
  • 总结阶段:总结阶段会针对区域进行操作。因为之前进行的老年代回收,老年代中会有相当数量的存活对象是在最起始的区域的,这部分起始区域也会包含大部分的存活对象。回收这部分空间起始在回收率上面是很差的,因为存活对象密度很高,很少存在可以回收的空间(前文提到的weak generational hypothesis)。所以总结阶段最开始就是要测试区域的密度,从最开始的对象开始检查,直到碰到某一个对象,这个对象可以回收,那么这个对象的右侧的空间(包括该对象空间)才有回收的价值。而左侧的空间也就是dense prefix,这个区域的对象不需要变动的。只有这个区域以外的空间才需要进行压缩,释放掉不可达对象的空间。总结阶段会计算并存储所有存活对象的空间。注意:总结阶段是一个串行的阶段,并行是可以实现的,但是在性能上面的影响远没有标记和压缩阶段那么重要。
  • 压缩阶段:垃圾回收线程会根据总结阶段的数据判断哪个区域需要进行回收,然后所有的线程可以独立的将数据拷贝到待填充的区域。这样可以令Heap空间压缩到起始位置,从而让出一大块连续的空间。

何时使用并行压缩收集器

和并行收集器一样,并行压缩收集器同样在多CPU环境下对于提高应用的GC吞吐是非常好的。不过并行压缩收集器比并行收集器在老年代上的回收更加高效,拥有更高的吞吐和更短的暂停时间。但是,当服务器上面跑多个应用的时,应用无法独占所有的CPU资源,就需要考虑适当削减GC的线程数了。可以通过配置:

-XX:ParallelGCThreads=n

来指定,当然,也可以考虑使用不同的收集器。

配置并行压缩收集器

如果需要使用并行压缩收集器,必须通过JVM参数来指定,配置如下:

-XX:+UseParallelOldGC

并发标记-替换收集器(CMS)

对很多应用来说,端到端的吞吐可能没有快速响应那么重要。年轻代的收集通常也不会引起长时间的停顿。然而,老年代的收集,尽管频率很低,但是却会消耗相当的时间,尤其是在堆栈空间很大的情况下。为了解决这个问题,HotSpot JVM包含了一个收集器,称为concurrent mark-sweep(CMS) collector,通常CMS收集器也被称为低延时收集器。

CMS收集器的年轻代回收

CMS收集器针对年轻代的收集使用的算法和并行收集器,并行压缩收集器的算法是一致的,在此不再多说。

CMS收集器的老年代回收

CMS执行的绝大多数垃圾回收操作都是与应用的执行一起并发执行的。

CMS收集器的收集周期的开端会暂停短暂的时间,称之为initial mark,和前面描述的标记阶段相类似,用来识别应用可以直达的一些存活对象。然后,并发执行标记操作,称为并发标记阶段,收集所有从这个集合可到达的活对象被传递的标志。因为在执行标记的时候,应用还在运行,并且更新引用的字段,所以当并行执行标记的时候,无法保证标记结束之后,所有的存活的对象仍然是存活的。为了解决这个问题,应用要再次暂停一下,称之为remark阶段,通过重新遍历有更新的对象来完成标记。因为remark暂停比初始化标记工作量要大,可以通过多线程来增加效率。

remark阶段结束的时候,所有堆中的存活对象都能保证被标记到,之后的concurrent sweep阶段会回收全部标记的垃圾。如下图,展示了老年代在串行mark-sweep-compact和CMS的区别。

因为有些诸如在冲标记阶段重新访问对象等任务,会为垃圾收集器带来一些额外的工作负载,所以CMS在减少暂停时间的同事,带来的代价就是额外的计算负载。

CMS收集器是为一个不进行压缩的垃圾收集器。也就是说,当其释放了非存活对象的空间以后,CMS收集器是不会挪动对象,进行碎片压缩的,如下图:

这样可以节省STW的时间,但是带来的问题就是空闲的空间并不是连续的空间,收集器无法在仅仅使用一个简单的指针来告诉JVM下一个存放对象的空间的位置(bump-the-pointer技术)。相反,CMS收集器需要维护一个空闲空间列表。这个列表指向一些空闲的区域,每次需要为对象释放空间的时候,都会根据这个空间空间列表来查找空闲的空间。因为每次分配空间都要查找这个列表,所以这种情况下,是无法通过bump-the-pointer技术来实现快速分配对象的,对象空间的分配的代价要高得多。这也同样会为年轻代的回收带来额外的代价,因为大多数的老年代垃圾回收的触发都是由于年轻代回收时,年轻代对象升级到老年代对象造成的。

CMS收集器的另外一个缺点在于,它比一般的垃圾回收算法对于堆的大小要求更高。因为应用在标记阶段,仍然是需要持续运行的,持续运行的应用也需要不断的执行分配空间操作的。另外,尽管CMS收集器在标记阶段保证能够识别全部的存活对象,但是有一定的可能就在这个阶段,其中存活的对象再次变成了垃圾,需要等到再下一次的垃圾回收才能回收掉。

这类垃圾也称之为floating garbage对象。

最后,整个老年代会因为没有执行压缩产生很多碎片。为了解决碎片问题,CMS收集器会跟踪一些常用对象的大小,评估未来的需求,然后将一些空闲的内存块分割或者合并来提高堆的利用率。

和其他的垃圾收集不太一样,CMS收集器的触发并不是在老年代没有额外空间。如上面所说,CMS收集的同时,应用是仍然在不断的释放对象的,如果老年代满了才执行GC,可能造成应用对象没有足够空间释放的情况。所以,CMS收集器会尽早执行垃圾回收。如果CMS收集失败了(空间不足),CMS会使用和串行以及并行所使用的更消耗时间的mark-sweep-compact算法。为了避免CMS算法的降级,CMS收集器的收集都是基于一些之前收集的统计数据来的,包括老年代被占用的速度和收集的次数等。CMS收集器还会在老年代的占用比超过一个初始化的占用比的情况下出发收集。而这个初始化的占比是通过JVM参数进行配置的。

XX:CMSInitiatingOccupancyFraction=n

其中n表示的就是老年代的一个占比,默认值为68.

总的来说,相对并行收集器来说,CMS收集器能够降低老年代的STW时间,当然,代价是会稍微延长年轻代收集的时间,以及降低吞吐量和限制堆大小等。

增量模式

CMS收集器在并发阶段以一种增量的模式执行。增量模式的意思就是减少并发阶段对于应用的影响,通过短时间的停止并发阶段的执行,跳回到应用的执行来尽可能减少长时间并发阶段处理对于应用的影响。收集器的工作就是将两次年轻代回收之间的时间分片,然后由CMS的GC的应用来进行调度执行。这一特性在应用服务器只有少量的处理器但是要求很低的延时的情况下,是非常有用的。

何时使用CMS收集器

如果开发者的应用需要相对更短的延时,并且可以将处理器资源交给GC执行的情况下,可以选择CMS收集器。通常来说,应用如果拥有相对来说较大的存活的数据集合(较大的老年代),并且运行的应用的服务器CPU个数多于2个,都可以考虑使用CMS收集器。比如Web服务器。在任何应用需要考虑低延时的情况下,都可以考虑使用CMS收集器。即使运行在单CPU的机器上,合理配置了老年代的大小的情况下,CMS收集器对于交互式应用的响应也是相对不错的。

配置CMS收集器

如果开发者想要使用CMS收集器,必须通过JVM参数:

-XX:+UseConcMarkSweepGC

来指定,如果想要使用增量模式,可以通过JVM参数:

XX:+CMSIncrementalMode

来指定。

以上是关于Java内存管理的主要内容,如果未能解决你的问题,请参考以下文章

springboot情操陶冶-jmx解析

java中GC回收和内存分配

Rust入坑指南:齐头并进(下)

有了CopyOnWrite为何又要有ReadWriteLock?

JAVA内存管理之堆内存和栈内存

内存管理技术