第一句子网 - 唯美句子、句子迷、好句子大全
第一句子网 > GC工作原理和日志

GC工作原理和日志

时间:2024-05-20 10:11:28

相关推荐

GC工作原理和日志

GC的工作原理

JVM分别对新生代和旧生代采用不同的垃圾回收机制

新生代的GC:

新生代通常存活时间较短,因此基于Copying算法来进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到survivor,最后到旧生代,

用javavisualVM来查看,能明显观察到新生代满了后,会把对象转移到旧生代,然后清空继续装载,当旧生代也满了后,就会报outofmemory的异常,如下图所示:

在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)

串行GC

在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定

并行回收GC

在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数

并行GC

与旧生代的并发GC配合使用

旧生代的GC:

旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。

在执行机制上JVM提供了串行GC(SerialMSC)、并行GC(parallelMSC)和并发GC(CMS),具体算法细节还有待进一步深入研究。

以上各种GC机制是需要组合使用的,指定方式由下表所示:

GC日志

见图

YongGC

FullGC

基本垃圾回收算法

可以从不同的的角度去划分垃圾回收算法:

按照基本回收策略分

引用计数(Reference Counting)

比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的

问题。

标记-清除(Mark-Sweep)

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。 此算法需要暂停整个应用,同时,会产生内存碎片。

复制(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。 此算法每次只处理正在使用中的对

象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

标记-整理(Mark-Compact)

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆

的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

按分区对待的方

增量收集(Incremental Collecting)

实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因 JDK5.0 中的收集器没有使用这种算法的。

分代收集(Generational Collecting)

基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从 J2SE1.2 开始)都是使用此算法的。

按系统线程分

串行收集

串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M 左右)情况下的多处理器机器上。

并行收集

并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上 CPU数目越多,越能体现出并行收集器的优势并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且

暂停时间会因为堆越大而越长。

垃圾回收面临的问题

上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计数为 0 的对象即可。但是这种方法无法解决循环引用。所以,后来实现的垃圾判断算法中,都是从程序运行的根节点出发, 遍历整个对象引用,查找存活的对象。那么

在这种方式的实现中, 垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的。上面分析的堆和栈的区别,其中栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从 Java 栈开始。 同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。

同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。 这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的

引用,这种引用逐步扩展,最终以 null 引用或者基本类型结束, 这样就形成了一颗以 Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他

剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。因此, 垃圾回收的起点是一些根对象(java 栈, 静态变量, 寄存器…) 。而最简单的 Java栈就是 Java 程序执行的 main 函数。这种回收方式,也是上面提到的“标记-清除”的回收方

如何处理碎片

由于不同 Java 对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。 所以,在上面提到的基本垃圾回收算法中, “复制”方式和“标记

-整理”方式,都可以解决碎片的问题

如何解决同时存在的对象创建和对象回收问题

垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的, 一个回收内存,一个分配内存,从这点看,两者是矛盾的。 因此,在现有的垃圾回收方式中,要进行垃

圾回收前,一般都需要暂停整个应用(即:暂停内存的分配), 然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,而且最有效的解决二者矛盾的方式。

但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,

比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个 G 时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决

了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时, “碎片”问题将会比较难解决

分代垃圾回收详述

为什么要分代

分代的垃圾回收策略,是基于这样一个事实: 不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

在 Java 程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http 请求中的 Session 对象、线程、 Socket 连接,这类对象跟业务直接挂钩,因此生命周

期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如: String 对象,由于其不变类的特性,系统会产生大量的这些对象,有些对

象甚至只用一次即可回收。

试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周

期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,

不同代上采用最适合它的垃圾回收方式进行回收。

如何分代

虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。 其中持久代主要存放的是 Java 类的类信息,与垃

圾收集要收集的 Java 对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

年轻代:

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个 Eden 区,两个 Survivor 区(一般而言)。大部分

对象在 Eden 区中生成。当 Eden 区满时,还存活的对象将被复制到 Survivor 区(两个中的一个), 当这个 Survivor 区满时,此区的存活对象将被复制到另外一个 Survivor 区,当这

个 Survivor 去也满了的时候,从第一个 Survivor 区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。 需要注意, Survivor 的两个区是对称的,没先后关系, 所以同一

个区中可能同时存在从 Eden 复制过来 对象,和从前一个 Survivor 复制过来的对象,而复制到年老区的只有从第一个 Survivor 去过来的对象。 而且, Survivor 区总有一个是空的。同时,根据程序需要, Survivor 区是可以配置为多个的(多于两个),这样可以增加对象在

年轻代中的存在时间,减少被放到年老代的可能。

年老代:

在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

持久代:

用于存放静态文件,如今 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较

大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

什么情况下触发垃圾回收

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。 GC 有两种类型:

Scavenge GCFull GC

Scavenge GC

一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对 Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后

整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出

来。

Full GC

对整个堆进行整理,包括 Young、 Tenured 和 Perm。 Full GC 因为需要对整个对进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。 在对 JVM 调优的

过程中,很大一部分工作就是对于 FullGC 的调节。有如下原因可能导致 Full GC:

年老代(Tenured)被写满持久代(Perm)被写满System.gc()被显示调用上一次 GC 之后 Heap 的各域分配策略动态变化

选择合适的垃圾收集算法

串行收集器(SerialMSC)

用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M 左右)情况下的多处理器机器上。可以使用-XX:+UseSerialGC 打开。

并行收集器(parallelMSC)

对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-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%的时间用于垃圾回收

并发收集器(CMS)

可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适

合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC 打开。

并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收

线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器 会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多

个线程同时进行垃圾回收工作。并发收集器使用处理器换来短暂的停顿时间。在一个 N 个处理器的系统上,并发收集部

分使用 K/N 个可用处理器进行回收,一般情况下 1<=K<=N/4。在只有一个处理器的主机上使用并发收集器,设置为 incremental mode 模式也可获得较短的停顿时间。

浮动垃圾: 由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完

成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回

收掉。所以,并发收集器一般需要 20%的预留空间用于这些浮动垃圾。

Concurrent Mode Failure: 并发收集器在应用运行时进行收集,所以需要保证堆在垃圾

回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种

情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。

启动并发收集器: 因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有

足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置

-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆时开始执行并发收集

配置示例

$ java -versionjava version "1.7.0_15"Java(TM) SE Runtime Environment (build 1.7.0_15-b03)Java HotSpot(TM) 64-Bit Server VM (build 23.7-b01, mixed mode

ParNew-CMS

-Xmx5000m-Xms5000m-Xmn2000m-XX:PermSize=256m-server-verbose:gc-XX:+PrintGCDateStamps-XX:+PrintGCTimeStamps-XX:+PrintGCDetails-XX:+PrintTenuringDistribution-XX:+PrintCommandLineFlags-XX:+DisableExplicitGC //禁止代码中显示调用GC-XX:+UseConcMarkSweepGC //设置年老代为并发收集-XX:ParallelCMSThreads=2 //年轻代的并行收集线程数-XX:+CMSClassUnloadingEnabled //垃圾回收会清理持久代,移除不再使用的classes。这个参数只有在 UseConcMarkSweepGC 也启用的情况下才有用-XX:+UseCMSCompactAtFullCollection //打开对年老代的压缩。可能会影响性能,但是可以消除碎片-XX:CMSInitiatingOccupancyFraction=80 //设定CMS在对内存占用率达到80%的时候开始GC -Xloggc:logs/gc.log

gc日志

-11-30T11:20:49.760+0800: 260811.966: [GC 260811.966: [ParNewDesired survivor size 104857600 bytes, new threshold 6 (max 6)- age 1: 3656 bytes, 3656 total- age 2: 2519408 bytes, 22653064 total- age 3: 1912488 bytes, 24565552 total- age 4: 1833128 bytes, 26398680 total- age 5: 1837288 bytes, 28235968 total- age 6: 1835776 bytes, 30071744 total: 1674734K->38101K(1843200K), 0.0366340 secs] 2186395K->550621K(2867200K), 0.0368630 secs] [Times: user=0.13 sys=0.00, real=0.03 secs] --初始标记(STW initial mark) 暂停应用 -11-30T11:20:49.798+0800: 260812.004: [GC [1 CMS-initial-mark: 512519K(1024000K)] 551233K(2867200K), 0.0196820 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] --并发标记(Concurrent marking)-11-30T11:20:49.818+0800: 260812.024: [CMS-concurrent-mark-start]-11-30T11:20:49.895+0800: 260812.101: [CMS-concurrent-mark: 0.077/0.077 secs] [Times: user=0.18 sys=0.00, real=0.08 secs] --并发预清理-11-30T11:20:49.895+0800: 260812.101: [CMS-concurrent-preclean-start]-11-30T11:20:49.902+0800: 260812.109: [CMS-concurrent-preclean: 0.007/0.007 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] -11-30T11:20:49.902+0800: 260812.109: [CMS-concurrent-abortable-preclean-start]CMS: abort preclean due to time -11-30T11:20:55.041+0800: 260817.248: [CMS-concurrent-abortable-preclean: 4.436/5.139 secs] [Times: user=4.81 sys=0.00, real=5.14 secs] -11-30T11:20:55.042+0800: 260817.249: [GC[YG occupancy: 80661 K (1843200 K)]260817.249: [Rescan (parallel) , 0.0090570 secs]260817.258: [weak refs processing, 0.0462830 secs]260817.304: [class unloading, 0.0076340 secs]260817.312: [scrub symbol table, 0.0079340 secs]-- 重新标记(STW remark) *** 暂停应用260817.320: [scrub string table, 0.0011250 secs] [1 CMS-remark: 512519K(1024000K)] 593180K(2867200K), 0.0814480 secs] [Times: user=0.12 sys=0.00, real=0.08 secs]-- 并发清理(Concurrent sweeping) -11-30T11:20:55.125+0800: 260817.332: [CMS-concurrent-sweep-start]-11-30T11:20:55.534+0800: 260817.740: [CMS-concurrent-sweep: 0.389/0.409 secs] [Times: user=0.59 sys=0.00, real=0.41 secs] -- 并发重置(Concurrent reset)-11-30T11:20:55.534+0800: 260817.740: [CMS-concurrent-reset-start]-11-30T11:20:55.537+0800: 260817.743: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。

并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。

重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。

并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

CSM执行过程:

PSYoungGen-ParOldGen

JAVA_OPTS="$JAVA_OPTS -server -XX:+PrintGCDateStamps-XX:+PrintGCTimeStamps \-XX:+PrintGCDetails \-XX:+PrintTenuringDistribution \-Xloggc:$HOME/logs/gc.log \-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$KIEV_HOME/logs \-Xmx5000M -Xms5000M -Xmn2000M -XX:PermSize=250M -XX:MaxPermSize=250M -Xss256K \

gc日志

-09-23T11:52:30.495+0800: 236340.374: [GCDesired survivor size 13107200 bytes, new threshold 1 (max 15)[PSYoungGen: 1686834K->8764K(1693760K)] 5098511K->3421071K(5107136K), 0.1114790 secs] [Times: user=0.34 sys=0.01, real=0.11 secs]-09-23T11:52:30.626+0800: 236340.504: [Full GC [PSYoungGen: 8764K->0K(1693760K)] [ParOldGen: 3412307K->749257K(3413376K)] 3421071K->749257K(5107136K) [PSPermGen: 40766K->40762K(256000K)], 1.6641540 secs] [Times:user=3.77 sys=0.01, real=1.66 secs]

小结

串行处理器:

适用情况:数据量比较小(100M 左右);单处理器下并且对响应时间无要求的应用。缺点:只能用于小型应用

并行处理器:

适用情况: “对吞吐量有高要求”,多 CPU、对应用响应时间无要求的中、大型应用。举例:

后台处理、科学计算。缺点:垃圾收集过程中应用响应时间可能加长

并发处理器:

适用情况: “对响应时间有高要求”,多 CPU、对应用响应时间有较高要求的中、大型应用。

举例: Web 服务器/应用服务器、电信交换、集成开发环境

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。