垃圾收集概述
垃圾收集由两步构成:查找不再使用的对象,释放这些对象所管理的内存。jvm从查找不再使用的对象入手。jvm通过定期扫描来查找不再使用的对象。一旦发现垃圾对象,jvm会回收这些对象所持有的内存,把他们分配给需要内存的其他对象。
分代垃圾收集器
虽然实现的细节千差万别,但所有的垃圾收集器都遵循了同一个方式,即根据情况将堆划分成不同的代。这些代被称为老年代和新生代。新生代又被进一步划分为不同的区段,分别称为Eden空间和Survivor空间,采用分代机制的原因是很多对象的生产时间非常短。
新生代是堆的一部分,对象首先在新生代中分配。新生代填满时,垃圾收集器会暂停所有的应用线程,回收新生代空间。不再使用的对象会被回收,仍在使用的对象会被移动到其他地方。这种操作被称为Minor GC。
对象不断的移动到老年代,最终老年代也会被填满,jvm需要找出老年代中不再使用的对象,并对它们进行回收,简单的垃圾收集算法直接停掉所有的应用线程,找出不再使用的对象,对其进行回收,接着对堆空间进行整理。这个过程被称为Full GC。
GC算法
jvm提供以下4种不同的垃圾收集算法。
- Serial(串行)垃圾收集器
Serial垃圾收集器是四种垃圾收集器中最简单的一种。Serial使用单线程清理堆内容,无论是进行Minor GC还是Full GC,清理堆空间时,所有的应用线程都会被暂停。进行Full GC时,他还会对老年代空间的对象进行压缩整理。通过-XX:+UseSerialGC标志可以启用Serial收集器 - Throughput(吞吐量)垃圾收集器
Throughput收集器是Server级虚拟机的默认收集器。Throughput收集器使用多线程回收新生代空间,Minor GC的速度比使用Serial收集器快得多。处理老年代时Throughput收集器也能使用多线程方式。由于Thoughput收集器使用多线程也常常被称为Paraller收集器。Thoughput收集器在Minor GC和Full GC时会暂停所有的应用线程,同时在Full GC过程中会对老年代空间进行压缩整理。 - CMS收集器
CMS收集器在Full GC时不再暂停应用线程,而是使用肉若干个后台线程定期对老年嗲空间进行扫描,及时回收其中不再使用的对象。这种算法帮助CMS成为一个低延迟的收集器:应用线程只在Minor GC以及后台线程扫描老年代时发生极其短暂的停顿。应用程序线程停顿的总时长与使用Throughput收集器比起来短得多
额外付出的代价时更高的CPU使用:必须有足够的CPU资源用于运行后台的垃圾收集线程,在应用程序线程运行的同时扫描堆的使用情况。除此之外,后台线程不再进行任何压缩整理的工作,这意味着堆会逐渐变得碎片化。如果CMS的后台线程无法获得完成他们任务所需的CPU资源,或者如果堆变得过度碎片化以至于无法找到连续空间分配对象,CMS就蜕化到Serial收集器的行为:暂停所有应用线程,使用单线程回收、整理老年代空间,这之后又恢复到并发运行,再次启动后台线程。
通过-XX:UseConvMarkSweepGC、-XXUseParNewGC标志启用CMS垃圾收集器。 - G1垃圾收集器
G1垃圾收集算法将堆划分为若干个区域,不过它依旧属于分代收集器。这些区域中的一部分包含新生代,新生代的垃圾收集仍然使用采用暂停所有应用线程的方式,将存活对象移动到老年代或者Survivor空间。同其他的收集算法一样,这些操作也利用多线程的方式完成。
G1收集器属于Concurrent收集器:老年代的垃圾收集工作由后台线程完成,大多数的工作不需要暂停应用线程。由于老年代被划分到不同的区域,G1收集器通过将对象从一个区域复制到另一个区域,完成对象的清理工作,这也意味着在正常的处理过程中,G1收集器实现了堆的压缩整理。因此,使用G1收集器的堆不太容易发碎片化。通过标志-XX:UseG1GC启用G1垃圾收集器。
GC调优
虽然处理堆时各种GC算法有所差异,但是他们的基本配置参数时一致的。
调整堆的大小
与其他的性能问题一样,选择堆的大小其实时一种平衡。如果分配的堆过于小,程序大部分时间可能都消耗在GC上,没有足够的时间去运行应用程序的逻辑。但是,简单粗暴地设置一个特别大的堆也不是解决问题的办法。GC停顿消耗的时间取决于堆的大小,如果增大堆的空间,停顿的持续时间也会变长。这种情况下,停顿的频率会变得更少,但是它们持续的时间会让程序的整体性能变慢。
调整堆大小时首要的原则就是永远不要将堆的容量设置得比机器的物理内存还打,除此之外,还需要为jvm自身一级机器上其他应用程序预留一部分的内存空间
堆的大小由2个参数值空之:分别是初始值(-Xms)和最大值(-Xmx),可以将堆的初始值和最大值直接设置为一样的数值。这种设置能稍微提高GC的运行效率,因为它不再需要估算堆是否需要调整大小了。
代空间的调整
一旦堆的大小确定下来,jvm就需要决定分配多少堆给新生代空间,多少给老年代空间。如果新生代分配得比较大,垃圾收集发生的频率就比较低,从新生代晋升到老年代的对象也更少,老年代相对比较小,比较容易被填满,会更频繁地触发Full GC。
不同的GC算法尝试使用不同的方法来解决这些平衡问题。虽然方法不同,不过所有的GC方法都使用了同一套标志来设置代的大小
所有用于调整代空间的命令行标志调整的都是新生代空间,新生代空间剩下的所有空间都被老年代占用
-XX:NewRatio=N 设置新生代与老年代的空间占用比率。初始新生代大小 = 初始堆大小 / (1 + NewRatio)
-XX:NewSize=N 设置新生代空间的初始大小
-XX:MaxNewSize=N 设置新生代空间的最大大小
-XmnN 将NewSize和MaxNewSize设定为同一个值的快捷方法
永久代和元空间的调整
jvm载入类的时候,需要记录这些类的元数据。这部分数据被保存在一个单独的堆空间中。在Java 8称为元空间(Metaspace),Java 8之前称为永久代
永久代和元空间并不完全一样。永久代保存了一些与数据无关的杂项对象,这些对象在Java 8中移到了普通的堆空间内。除此之外,Java 8从根本上改变了保存在这个特殊区域内的元数据类型。使用元空间替换掉永久代的优势之一时不再需要对其进行调整
控制并发
除Serial收集器之外几乎所有的垃圾收集器使用的算法都基于多线程。启动的线程数由-XX:ParallelGCThreads=N参数控制
几乎所有的垃圾收集算法中基本的垃圾回收线程数都依据机器上的CPU数目计算得出,多个jvm运行于同一台物理机上时,计算得出的线程数可能过高,需进行优化
垃圾回收工具
多开启GC日志,使用-verbose:gc或-XX:+PrintGC这两个标志中的任意一个能创建基本的GC日志。使用-XX:+PringGCDetails标志会创建更详细的GC日志,使用-XX:+PrintGCTimeStamps或-XX:PrintGCDateStamps,便于更精确地判断GC操作之间的时间,使用-Xloggc:filename标志修改输出到某个文件,使用日志循环标志可以限制保存在GC日志中的数据量。通过-XX:+UseGCLogfileRotaion -XX:NumberOfGCLogFiles=N -XX:GCLogfilesSize=N标志控制日志文件的循环
GC Histogram能够读入GC日志,根据日志文件中的数据生成对应的图表和表格。
使用jconsole可以实时监控堆的使用情况
使用jstat监控应用程序的垃圾回收过程