一、概述
Java垃圾回收器实现内存的自动分配和回收,这两个操作都发生在Java堆上(还包括方法区,即永久代)。垃圾回收操作不是实时的发生(对象死亡不会立即释放),当内存消耗完或者是达到某一指标(threshold,使用内存占总内存的比列,比如0.75)时,触发垃圾回收操作。有一个对象死亡的例外,java.lang.Thread类型的对象即使没有引用,只要线程还在运行,就不会被回收。major collection会引发minor collection。
JVM中,程序计数器、虚拟机栈、本地方法栈有与线程是相同的生命周期。栈帧随着方法的进入和退出做入栈和出栈操作,从而实现内存的自动清理。因此,内存垃圾回收主要集中于堆和方法区。
对象存活判断方式
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,但无法解决对象相互循环引用的问题。
- 可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。即为不可达对象。
在Java语言中,GC Roots包括:- 虚拟机栈中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
二、回收的算法
引用计数(reference counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用
问题。
标记-清除(mark-sweep)
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用
,而且会产生内存碎片
。
复制(copying)
此算法把内存空间划为两个相等的区域
,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理 正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。此算法的缺点,就是需要两倍内存空间
。
标记-整理(mark-compact)
此算法结合了“标记-清除”和“复制”两个算法的优点。是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
分代(generational collecting)
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
分区(G1)
三、回收的机制
依统计分析,大多数对象生命周期都是短暂的,所以把Java内存分代管理。
四、回收的性能指标(Performance Metrics)
吞吐量(Throughput) 在一个较长的周期内,非回收时间占总时间的比率。
暂停时间(Pause time) Java虚拟机在回收垃圾的时候,有的算法会暂停所有应用线程的执行。
五、回收的区域
年轻代(Young Generation)
年轻代被划分为三个区域,伊甸区(eden)和两个小的幸存区(survivor),两个存活区按功能分为From和To。绝大多数的对象都在eden区分配,超过一个垃圾回收操作仍然存活的对象放到幸存区。当Eden区满时,还存活的对象将被复制到survivor区(两个中的一个),当这个survivor区满时,此区的存活对象将被复制到另外一个survivor区,当这个survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(tenured)”。需要注意,survivor的两个区是对称的,没先后关系,survivor区总有一个是空的。
年老代(Old Generation)
主要存储年轻代中经过多个回收周期仍然存活从而升级的对象,对于一些大的内存分配,可以直接分配到永久代。
永久代(Permanent Generation)
存储类、方法以及它们的描述信息。通常不需要调节该参数,但是有些应用可能动态生成或者调用一些class, 这时需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
六、回收的类型
- minor GC
当新对象生成,在eden申请空间失败时,就会触发minor GC,对eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到survivor区。然后整理survivor的两个区。 - full GC
对整个堆进行整理,包括Young、Tenured和Perm。有如下原因可能导致Full GC:
- Tenured被写满
- Perm域被写满
- System.gc()被显示调用
- 上一次GC之后heap的各域分配策略动态变化
七、垃圾回收器类型
串行收集器: 使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。使用-XX:+UseSerialGC打开。
-
并行收集器: 对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC打开。并行收集器在J2SE5.0第6更新上引入,在Java SE6.0中进行了增强--可以对年老代进行并行收集。如果年老代不使用并发收集的话,是使用单线程进行垃圾回收,因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。
- 使用-XX:ParallelGCThreads设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。
- 此收集器可以进行如下配置:
§ 最大垃圾回收暂停: 指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis指定。为毫秒。如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。
§ 吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=来设定,公式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。
并发收集器:可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。
1.主要减少年老代的暂停时间,在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
2. 并发收集器使用多处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。
3. 在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。
4. 浮动垃圾:由于在应用运行的同时进行垃圾回收,所以会有些垃圾在垃圾回收完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
5. Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。
6. 启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction指定还有多少剩余堆时开始执行并发收集。
八、具体的垃圾回收器
新生代
1 serial收集器
serial收集器是Hotspot运行在client模式下的默认新生代收集器, 它的特点是只用一个CPU/一条收集线程去完成GC工作, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称STW)。
虽然是单线程收集, 但简单而高效, 在VM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内.
2 ParNew收集器
ParNew收集器是前面serial的多线程版本, 除使用多个线程进行GC外, 包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与serial完全一样(也是VM启用CMS收集器-XX: +UseConcMarkSweepGC的默认新生代收集器).
由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数).
3 Parallel Scavenge收集器
与ParNew类似,Parallel Scavenge也是使用复制算法,也是并行多线程收集器。 但与其他收集器关注尽可能缩短垃圾收集时间
不同, Parallel Scavenge更关注系统吞吐量
。
系统吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适用于用户交互的程序--良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务--可以最高效率地利用CPU时间,尽快地完成程序的运算任务。
老年代
1 Serial Old收集器
Serial old是serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法。
2 Parallel Old收集器
Parallel old是Parallel Scavenge收老年代版本, 使用多线程和“标记-整理”算法。 吞吐量优先,主要与Parallel Scavenge配合在注重吞吐量 及 CPU资源敏感系统内使用。
3 CMS收集器
CMS(Concurrent Mark Sweep)收集器是真正意义上的并发收集器,虽然已经有了理论上表现更好的G1收集器。
CMS以获取最短回收停顿时间
为目标的收集器,基于”标记-清除”算法实现,整个GC过程分为以下4个步骤:
- 初始标记
- 并发标记(GC Roots Tracing过程)
- 重新标记
- 并发清除(CMS concurrent sweep: 已死象会就地释放,注意: 此处没有压缩)
其中初始标记和重新标记仍需STW。但初始标记仅只标记一下GC Roots能直接关联到的对象, 速度很快。重新标记是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,虽然一般比初始标记阶段稍长,但要远小于并发标记时间。
(由于整个GC过程耗时最长的并发标记和并发清除阶段的GC线程可与用户线程一起工作, 所以总体上CMS的GC过程是与用户线程一起并发地执行的。
由于CMS收集器将整个GC过程进行了更细粒度的划分, 因此可以实现并发收集、低停顿
的优势,但它也并非十分完美,其存在缺点及解决策略如下:
- CMS默认启动的回收线程数=(CPU数目+3)4
当CPU数>4时,GC线程最多占用不超过25%的CPU资源,但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低。 -
无法处理浮动垃圾
, 可能出现Promotion Failure、Concurrent Mode Failure而导致另一次Full GC的产生。
浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾。由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用,导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集。 因此CMS提供了-XX:CMSInitiatingOccupancyFraction参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly来启用该触发百分比),当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%)。 但当CMS运行期间预留的内存无法满足程序需要,就会出现Promotion Failure等失败, 这时VM将启动后备预案:临时启用serial old收集器来重新执行full GC(CMS通常配合大内存使用,一旦大内存转入串行的serial GC, 那停顿的时间就是大家都不愿看到的了)。
最后,由于CMS采用”标记-清除
”算法实现, 可能会产生大量内存碎片
。内存碎片过多可能会导致无法分配大对象而提前触发Full GC。因此CMS提供了-XX:+UseCMSCompactAtFullCollection参数,用于在Full GC后再执行一个碎片整理过程。 但内存整理是无法并发的,内存碎片问题虽然没有了,但停顿时间也因此变长了,因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后,跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理)。
4 G1收集器
HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:
- 空间整合,G1收集器采用
标记-整理
算法,不产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。 -
可预测停顿
,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型
,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1将Java堆划分为多个大小相等的独立区域(region),虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分region(不需要连续)的集合。
每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收。而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多。 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的regiorn(这也是G1命名的来源)。 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率。
新生代收集
G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果存活时间达到阀值, 这部分对象就会被提升到老年代。
G1的新生代收集特点如下:
- 一整块堆内存被分为多个Regions。
- 存活对象被拷贝到新的Survivor区或老年代。
- 年轻代内存由一组不连续的heap区组成,这种方法使得可以动态调整各代区域尺寸。
- Young GCs会有STW事件, 进行时所有应用程序线程都会被暂停,
- 多线程并发GC。
老年代收集
G1老年代GC会执行以下阶段:
注: 以下有些阶段也是年轻代垃圾收集的一部分。
index | Phase | Description |
---|---|---|
(1) | 初始标记 (Initial Mark: Stop the World ) | 在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions。 |
(2) | 扫描根区域 (Root Region Scanning: 与应用程序并发执行) | 扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完。 |
(3) | 并发标记 (Concurrent Marking : 与应用程序并发执行) | 在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断。 |
(4) | 重新标记 (Remark : Stop the World ) | 完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空region直接被移除并回收,并计算所有区域的活跃度)。 |
(5) | 清理 (Cleanup : Stop the World and Concurrent) | 见下 5-1、2、3 |
5-1 (Stop the world) | 在含有存活对象和完全空闲的区域上进行统计。 | |
5-2 (Stop the world) | 擦除Remembered Sets。 | |
5-3 (Concurrent) | 重置空regions并将他们返还给空闲列表(free list)。 | |
(*) | Copying/Cleanup (Stop the World ) | 选择”活跃度”最低的区域(这些区域可以最快的完成回收)。拷贝/转移存活的对象到新的尚未使用的regions。 该阶段会被记录到gc-log(只发生年轻代[GC pause (young)], 与老年代一起执行则被记录为[GC Pause (mixed)]。 |
详细步骤可参考 Oracle官方文档-The G1 Garbage Collector Step by Step.
G1老年代GC特点如下:
- 并发标记阶段(index 3)
- 在与应用程序并发执行的过程中会计算活跃度信息。
- 这些活跃度信息标识出那些regions最适合在STW期间回收(which regions will be best to reclaim during an evacuation pause)。
- 不像CMS有清理阶段。
- 再次标记阶段(index 4)
- 使用Snapshot-at-the-Beginning(SATB)算法比CMS快得多。
- 空region直接被回收。
- 拷贝/清理阶段(Copying/Cleanup Phase)
- 年轻代与老年代同时回收。
- 老年代内存回收会基于他的活跃度信息。
补充: 关于Remembered Set
G1收集器中, region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用都是使用remembered set来避免扫描全堆。G1中每个Region都有一个与之对应的Remembered Set,VM发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中(在分代例子中就是检查是否老年代中的对象引用了新生代的对象), 如果是, 便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。
常用的收集器组合
建议:如果系统花费很多的时间收集垃圾,请减小堆大小。full gc不应该超过 3-5 秒.
thread不要设的太高,一般一个cpu200个,多了的话,线程切换反而导致性能下降。
参考
JVM GC参数以及GC算法的应用
GC算法 垃圾收集器
深入理解G1垃圾收集器
JVM初探- 内存分配、GC原理与垃圾收集器