目录
4.6 CMS(Concurrent Mark Sweep)收集器:
1. Java垃圾回收概况
Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,其实不是Java的特有技术,早在1959年,John McCarthy就发明了垃圾收集,以简化Lisp中的手动内存管理。只是作为受众最广的编程语言之一,Java将其发扬光大。Java优于早期高级语言霸主C++/C的主要优点之一,就在于使用Java,不再需要程序员象C++/C一样专门编写代码,进行手动内存回收和垃圾清理,以避免内存溢出错误(Out Of Memory Error,OOM)。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。 该机制对JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,并尽量防止出现内存泄露和溢出问题。
既然JVM已经自动为我们完成了上述问题,那么学习Java GC原理,其意义何在呢?通过学习Java GC机制,其作用有二:1,进行排错。虽然Java GC可以进行自动内存回收,但实际中经常出现OOM。通过学习,可以帮助我们在日常工作中排查各种内存溢出或泄露问题;
2,进行性能调优。解决程序性能瓶颈,达到更高的并发量,从而写出更高效的程序。
注意:考虑到广泛性,本文所讨论的虚拟机为默认JDK所使用的HotSpot虚拟机。
2. 堆内存分配和回收介绍
2.1 堆内存分配
这里所说的内存分配,主要指的是在堆上的分配。一般的,对象的内存分配都是在堆上进行。Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。如下图:
我们在程序中new出来的对象一般情况下都会在新生代里的Eden区里面分配空间,如果存活时间足够长将会进入Survivor区,进而如果存活时间再长,还会被提升分配到老年代里面。持久代里面存放的是Class类元数据、方法描述等。具体如下
- 年轻代(Young Generation):
对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。
年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再贴切不过)和两个存活区(Survivor 0 、Survivor 1)。在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。
- 年老代(Old Generation):
对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。 可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。 如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。
这里可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。
用-XX: PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
- 永久代(Permanent Generation):
永久代就是我们所说的方法区,而方法区经常被称为Non-Heap(非堆)。仅仅在HotSpot虚拟机的实现中才将GC分代收集扩展至方法区,或者说使用永久代来实现方法区,对于其他的虚拟机是不存在永久代这个概念的。这个区域会存储包括类定义、结构、字段、方法(数据及代码)以及常量在内的类相关数据。
它可以通过-XX:PermSize及-XX:MaxPermSize来进行调节。如果它的空间用完了,会导致java.lang.OutOfMemoryError: PermGenspace的异常。
注意:JDK8开始,持久代已经被彻底删除了,取代它的是另一个内存区域也被称为元空间。
2.2 堆内存回收
通常我们把发生在新生代的垃圾回收称为Minor GC,而把发生在老年代的垃圾回收称为Major GC,而FullGC是指整个堆内存的垃圾回收,包括对新生代、老年代和持久代的回收。一般情况下应用程序发生Minor GC的次数要远远大于Major GC和Full GC的次数。
- 新生代:
在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor,具体过程如下。
- 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
- 最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
- 下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;
- 将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;
- 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。
这里也可以发现,停止复制算法中,用来复制的两部分并不总是相等的(传统的停止复制算法两部分内存相等,但新生代中使用1个大的Eden区和2个小的Survivor区来避免这个问题)
由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到 老年代。用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,代表Eden:Survivor1:Survivor2=8:1:1.
这里需要明确一点,就是在新生代采用的停止复制算法中,“停 止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所有线程的执行。这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。
- 老年代:
老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。
在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。
- 永久代:
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
- 类的所有实例都已经被回收
- 加载类的ClassLoader已经被回收
- 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。
3. 分代回收算法
我们一般讨论的垃圾回收主要针对Java堆内存中的新生代和老年代,也正因为新生代和老年代结构上的不同,所以产生了分代回收算法,即新生代的垃圾回收和老年代的垃圾回收采用的是不同的回收算法。针对新生代,主要采用复制算法,而针对老年代,通常采用标记-清除算法或者标记-整理算法来进行回收。
3.1 复制算法
复制算法的思想是将内存分成大小相等的两块区域,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块区域上,然后对该块进行内存回收。示例图如下所示:
这个算法实现简单,并且也相对高效,但是代价就是需要将牺牲一半的内存空间用于进行复制。有研究表明,新生代中的对象98%存活期很短,所以并不需要以1:1的比例来划分整个新生代,通常的做法是将新生代内存空间划分成一块较大的Eden区和两块较小的Survivor区,两块Survivor区域的大小保持一致。每次使用Eden和其中一块Survivor区,当回收的时候,将还存活的对象复制到另外一块Survivor空间上,最后清除Eden区和一开始使用的Survivor区。假如用于复制的Survivor区放不下存活的对象,那么会将对象存到老年代。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1:1,也就是说新生代中牺牲掉10%的空间而不是一半的空间。
3.2 标记-清除算法
标记-清除(Mark-Sweep)算法分为两个阶段:标记和清除
在标记阶段将标记出需要回收的对象空间,然后在下一个阶段清除阶段里面,将这些标记出来的对象空间回收掉。这种算法有两个主要问题:一个是标记和清除的效率不高,另一个问题是在清理之后会产生大量不连续的内存碎片,这样会导致在分配大对象时候无法找到足够的连续内存而触发另一次垃圾收集动作。标记-清除算法示例图如下所示:
3.3 标记-整理算法
标记-整理(Mark-Compact)算法有效预防了标记-清除算法中可能产生过多内存碎片的问题。在标记需要回收的对象以后,它会将所有存活的对象空间挪到一起,然后再执行清理。示例图如下所示:
标记-整理通常会在标记-清除算法里面作为备选方案,为了防止标记-清除后产生大量内存碎片而无法为大对象分配足够内存的情况,如后面所讲的Serial Old收集器(基于标记-整理算法实现)将会作为CMS收集器(基于标记-清除算法实现)的备选方案。
4. 垃圾收集器
在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,HotSpot 1.7版使用的垃圾收集器如下图(图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用):
这里要注意的是,在讲解GC的时候会涉及到并行和并发两个概念。在这里,并行指的是多个GC收集线程之间并行进行垃圾回收。而并发指的是多个GC收集线程与所有的用户线程能够交替执行。
4.1 Serial收集器:
新生代收集器,使用停止复制算法,使用一个线程进行GC,串行,在回收的过程中会挂起所有的用户线程(Stop The World)。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)
4.2 ParNew收集器:
新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
4.3 Parallel Scavenge 收集器:
新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效),用开关参数-XX:+UseAdaptiveSizePolicy可以进行动态控制,如自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等,这个参数在ParNew下没有。
4.4 Serial Old收集器:
老年代收集器,单线程收集器,串行,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。
4.5 Parallel Old收集器:
老年代收集器,多线程,并行,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。
使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。
4.6 CMS(Concurrent Mark Sweep)收集器:
老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。CMS只在初始化标记和重新标记阶段需要挂起用户线程,造成一定的应用停顿(STW),而其他阶段收集线程都可以与用户线程并发交替进行,不必挂起用户线程,所以并不会造成应用的停顿。CMS收集器可以最大程度地减少因垃圾回收而造成应用停顿的时间。
使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS,当用户线程内存不足时,采用备用方案Serial Old收集。
4.7 G1收集器:
JDK1.7开始使用,并行,多线程机制,使用标记整理,值得注意的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。使用-XX:+Useg1GC开关控制使用G1收集器进行收集。
5. JVM参数使用
5.1 堆内存相关
- -Xms 与 -Xmx
-Xms
用于指定Java应用使用的最小堆内存,如-Xms1024m
表示将Java应用最小堆设置为1024M。-Xmx
用于指定Java应用使用的最大堆内存,如-Xmx1024m
表示将Java应用最大堆设置为1024m。过小的堆内存可能会造成程序抛出OOM异常,所以正常发布的应用应该明确指定这两个参数。并且,一般会选择将-Xms
与-Xmx
设置成一样大小,防止JVM动态调整堆内存容量对程序造成性能影响。
- -Xmn
通过-Xmn
可以设置堆内存中新生代的容量,以此达到间接控制老年代容量的作用,因为没有JVM参数可以直接控制老年代的容量。如-Xmn256m
表示将新生代容量设置为256M。假如这个时候额外指定了-Xms1024m -Xmx1024m
,那么老年代的容量为768M(1024-256=768)。
- -XX:PermSize 与 -XX:MaxPermSize
-XX:PermSize
和-XX:MaxPermSize
分别用于设置永久代的容量和最大容量。如-XX:PermSize=64m -XX:MaxPermSize=128m
表示将永久代的初始容量设置为64M,最大容量设置为128M。
- -XX:SurvivorRatio
这个参数用于设置新生代中Eden区和Survivor(S0、S1)的容量比值。默认设置为-XX:SurvivorRatio=8
表示Eden区与Survivor的容量比例为8:1:1。假设-Xmn256m -XX
:
Survivor=8
,那么Eden区容量为204.8M(256M / 10 * 8),S0和S1区的容量大小均为25.6M(256M / 10 * 1)。
5.2 GC收集器相关
- -XX:+UseSerialGC
虚拟机运行在client模式下的默认值,使用这个参数表示虚拟机将使用Serial + Serial Old收集器组合进行垃圾回收。
-XX:+UseSerialGC表示使用这个设置,而-XX:-UseSerialGC表示禁用这个设置。
- -XX:+UseParNewGC
使用这个设置以后,虚拟机将使用ParNew + Serial Old收集器组合进行垃圾回收。
- -XX:+UseConcMarkSweepGC
使用这个设置以后,虚拟机将使用ParNew + CMS + Serial Old的收集器组合进行垃圾回收。注意Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器来进行回收(将会整理内存碎片)。
- -XX:+UseParallelGC
虚拟机运行在server模式下的默认值。使用这个设置,虚拟机将使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行垃圾回收。
- -XX:+UseParallelOldGC
使用这个设置以后,虚拟机将使用Parallel Scavengen + Parallel Old的收集器组合进行垃圾回收。
- -XX:PretenureSizeThreshold
设置直接晋升到老年代的对象大小,大于这个参数的对象将直接在老年代分配,而不是在新生代分配。注意这个值只能设置为字节,如-XX:PretenureSizeThreshold=3145728
表示超过3M的对象将直接在老年代分配。
- -XX:MaxTenuringThreshold
设置晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个值时就进入老年代。默认设置为-XX:MaxTenuringThreshold=15
。
- -XX:ParellelGCThreads
设置并行GC时进行内存回收的线程数。只有当采用的垃圾回收器是采用多线程模式,包括ParNew、Parallel Scavenge、Parallel Old、CMS,这个参数的设置才会有效。
- -XX:CMSInitiatingOccupancyFraction
设置CMS收集器在老年代空间被使用多少(百分比)后触发垃圾收集。默认设置-XX:CMSInitiatingOccupancyFraction=68
表示老年代空间使用比例达到68%时触发CMS垃圾收集。仅当老年代收集器设置为CMS时候这个参数才有效。
- -XX:+UseCMSCompactAtFullCollection
设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅当老年代收集器设置为CMS时候这个参数才有效。
- -XX:CMSFullGCsBeforeCompaction
设置CMS收集器在进行多少次垃圾收集后再进行一次内存碎片整理。如设置-XX:CMSFullGCsBeforeCompaction=2
表示CMS收集器进行了2次垃圾收集之后,进行一次内存碎片整理。仅当老年代收集器设置为CMS时候这个参数才有效。
5.3 GC日志相关
- -XX:+PrintGCDetails
表示输出GC的详细情况。
- -XX:+PrintGCDateStamps
指定输出GC时的时间格式,比指定-XX:+PrintGCTimeStamps
可读性更高。
- -Xloggc
指定gc日志的存放位置。如-Xloggc:/var/log/myapp-gc.log
表示将gc日志保存在磁盘/var/log/
目录,文件名为myapp-gc.log
。
参考资料
- Java GC 介绍 https://blog.csdn.net/d6619309/article/details/53358250
- Java系列笔记(3) - Java 内存区域和GC机制 https://www.cnblogs.com/zhguang/p/3257367.html
- 深入理解 Java G1 垃圾收集器 http://blog.jobbole.com/109170/