4.JVM垃圾回收机制
4.1.新生代的GC
4.1.1.串行GC(SerialGC)
4.1.2.并行回收GC(Parallel Scavenge)
4.1.3.并行GC(ParNew)
4.2.GC(Minor GC、FullGC)
4.2.1.Minor GC
4.2.2.FullGC
4.3.GC日志
JVM分别对新生代和老年代采用不同的垃圾回收机制。
新生代通常存活时间较短,因此基于复制算法来进行回收,所谓复制算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和其中一个Survivor,复制到另一个之间Survivor空间中,然后清理掉原来就是在Eden和其中一个Survivor中的对象。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到 survivor,最后到老年代。
用java visualVM来查看,能明显观察到新生代满了后,会把对象转移到旧生代,然后清空继续装在,当旧生代也满了后,就会报OutOfMemory的异常。
在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)。
在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
与旧生代的并发GC配合使用。
旧生代的GC:
旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行 GC(SerialMSC)、并行GC(parallelMSC)和并发GC(CMS),具体算法细节还有待进一步深入研究。
GC分为两种:Minor GC、FullGC(或称为Major GC)。
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为Minor GC。
当发生Minor GC事件的时候,有一些有趣的地方需要注意到:
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
质疑常规的认知,所有的 Minor GC都会触发”stop-the-world”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就是,大部分 Eden区中的对象都能被认为是垃圾,永远也不会被复制到Survivor区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
每次Minor GC会清理年轻代的内存。
Minor GC是发生在新生代中的垃圾收集动作,所采用的复制算法。
当一个对象被判定为”死亡”的时候,GC就有责任来回收掉这部分对象的内存空间。新生代是GC收集垃圾的频繁区域。
当对象在Eden (包括一个 Survivor 区域,这里假设是 from 区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳(上面已经假设为 from 区域,这里应为to 区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另外一块Survivor区域(即to区域)中,然后清理所使用过的Eden以及Survivor区域(即from区域),并且将这些对象的年龄设置为1,以后对象在Survivor 区每熬过一次 Minor GC,就将对象的年龄+ 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold来设定),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象(即需要分配一块较大的连续内存空间)则是直接进入到老年代。
Full GC是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。
现实的生活中,老年代的人通常会比新生代的人”早死”。堆内存中的老年代(Old)不同于这个老年代里面的对象几乎个个都是在Survivor区域中熬过来的,它们是不会那么容易就”死掉”了的。
因此,Full GC发生的次数不会有Minor GC那么频繁,并且做一次Full GC要比进行一次Minor GC的时间更长。
另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片(即不连续的内存空间),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次GC的收集动作。
案例
package com.toto.jvm;
public class Demo {
public static void main(String[] args) {
Object obj = new Object();
System.gc();
System.out.println();
obj = new Object();
obj = new Object();
System.gc();
System.out.println();
}
}
设置VM arguments:-verbose:gc -XX:+PrintGCDetails
System.gc()可以触发Full GC
[GC (System.gc()) [PSYoungGen: 5201K->752K(151552K)] 5201K->760K(498688K), 0.0012719 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 752K->0K(151552K)] [ParOldGen: 8K->536K(347136K)] 760K->536K(498688K), [Metaspace: 2587K->2587K(1056768K)], 0.0088355 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[GC (System.gc()) [PSYoungGen: 2600K->96K(151552K)] 3137K->632K(498688K), 0.0005806 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 96K->0K(151552K)] [ParOldGen: 536K->535K(347136K)] 632K->535K(498688K), [Metaspace: 2588K->2588K(1056768K)], 0.0064823 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
Heap
PSYoungGen total 151552K, used 2601K [0x0000000716d00000, 0x0000000721600000, 0x00000007c0000000)
eden space 130048K, 2% used [0x0000000716d00000,0x0000000716f8a578,0x000000071ec00000)
from space 21504K, 0% used [0x0000000720100000,0x0000000720100000,0x0000000721600000)
to space 21504K, 0% used [0x000000071ec00000,0x000000071ec00000,0x0000000720100000)
ParOldGen total 347136K, used 535K [0x00000005c4600000, 0x00000005d9900000, 0x0000000716d00000)
object space 347136K, 0% used [0x00000005c4600000,0x00000005c4685f60,0x00000005d9900000)
Metaspace used 2594K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
设置JVM参数为-verbose:gc -XX:+PrintGCDetails,使得控制台能够显示GC相关的日志信息,执行上面代码,下面是其中一次执行的结果。
jstate -gc -t 4235 1s
下面只列举其中的几个常用和容易掌握的配置选项
-Xms | 初始堆大小。如:-Xms256m |
-Xmx | 最大堆大小。如:-Xmx512m |
-Xmn | 新生代大小。通常为Xmx的1/3 或1/4。新生代 = Eden + 2个Survivor空间。实际可用空间为 = Eden + 1 个Survivor,即90% |
-Xss | JDK1.5+每个线程堆栈大小为1M,一般来说如果栈不是很深的话,1M是绝对够用了的。 |
-XX:NewRatio | 新生代与老年代的比例,如–XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio | 新生代中Eden与Survivor 的比值。默认值为8。即 Eden占新生代空间的8/10,另外两个 Survivor 各占 1/10 |
-XX:PermSize | 永久代(方法区)的初始大小 |
-XX:MaxPermSize | 永久代(方法区)的最大值 |
-XX:+PrintGCDetails | 打印GC信息 |
-XX:+HeapDumpOnOutOfMemoryError | 让虚拟机在发生内存溢出时Dump出当前的内存堆转储快照,以便分析用。 |
-XX:Newratio: 设置Old和Yong的比例,比如值为2,则Old Generation是 Yong Generation的2倍,即Yong Generation占据内存的1/3
-XX:Newsize : 设置Yong Generation的初始值大小
-XX:Maxnewsize:设置Yong Generation的最大值大小
-XX:Surviorratio : 设置Eden和一个Suivior的比例,比如值为5,即Eden是To(S2)的比例是5,(From和To是一样大的),此时Eden占据Yong Generation的5/7
一般情况下,不允许-XX:Newratio值小于1,即Old要比Yong大。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小受本地内存限制。