2、关于Minor GC,Major GC与Full GC
1) Minor GC:即新生代的GC,指发生在新生代的垃圾收集动作。当新生代的Eden区内存不足时,就会触发Minor GC。由于对象创建时,都会在Eden区分配内存,因此通过日志可以看到Minor GC动作执行相当频繁;同时,由于新生代对象朝生夕亡的特性,每次Minor GC的效果都十分理想。此外,Minor GC的效率也是非常高的。
2)Major GC与Full GC:我们可以认为Major GC与Full GC是一个概念,是指发生在老年代的引发了STW的GC。出现Full GC时,经常会伴随着至少一次的Minor GC,是由于老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。一般Full GC会比Minor GC速度慢10倍以上。
3、垃圾收集算法以及垃圾收集器
3.1 如何判断对象存活以及永久代可回收的判断
在进行垃圾收集之前,需要做的一件事情就是判断对象对象是否存活。如何判断对象存活,一般有两种方法:
1、引用计数法:引用计数法实现方式很简单,首先给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当每有一个地方引用失效时,计数器减1;当到达零时,表示没有任何地方有对这个对象的引用了,该对象即为垃圾回收的目标对象。可是目前没有商业的JVM使用引用计数法去判断对象是否存活,原因是因为它有一个弊端,即当某两个对象存在循环引用时,引用计数器永远都不能到零,因此也不会通知垃圾收集器进行回收。
2、可达性分析算法:目前商业虚拟机的主流实现中,都通过该种方式判断对象是否可回收。主要思路是定义一系列叫做“GC Root”的根节点,从这些根节点往下搜索,走过的路径称为引用链,当一个对象到“GC Root”没有任何引用链相连的时候,则证明该对象是不可用的。如下图所示,obj5与obj6即为可回收的对象。由此就可以很好的解决循环引用的问题。
通过上面的方法即可以判断出新生代以及年老代的对象是否可以回收,但是还有一个比较特殊的区域,那就是永久代。永久代一样是需要进行垃圾回收的,但是在永久代中进行垃圾回收的效果可能会比新生代以及老年代差了许多。永久代中保存的主要是已被虚拟机加载的类信息以及常量信息,因此永久代中垃圾回收主要包括两个部分:废弃的常量和无用的类。
1、废弃的常量:判断常量是否已废弃的方法与判断对象是否存活的方式类似,即发现如果没有任何对象引用常量池中的常量时,即可断定该常量是可以被回收的。
2、无用的类:判断一个类是否无用会比较复杂一点,需要从以下几个方面进行判断:
-
该类所有的实例已经被回收,也就是java堆中不存在该类的任何实例
-
加载该类的ClassLoader已经被回收
-
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射机制访问该类的方法
3.2 垃圾收集算法思想
1、复制算法: 现代商业虚拟机都使用这种方法来回收新生代。该算法的主要思路是:将内存分为两块,对象创建都在某一块上分配内存,当该块内存快满时触发垃圾回收,回收时将该块内存中存活的对象全部复制到另外一块内存中,然后将这块内存全部回收掉。这种方法的好处是由于是整块内存的回收,因此不会产生内存碎片。 现代商业虚拟机都是将内存分为三块,就是我们平时所说的1个Eden区以及2个Survivor区,由于新生代对象一般都是朝生夕亡(98%亡),所以一般默认比例8:1:1。垃圾回收时,将Eden区以及使用的1个Survivor区上存活的对象复制到另一个Survivor区上,然后清空Eden与之前的那个Survivor区。
2、标记清除算法:该算法的主要思路是:先标记出所有需要被回收的对象,在标记完成后统一回收被标记的对象,标记过程就是前面介绍的判断对象不存活了就标记。 该方法缺点也很明显,就是会产生大量的内存碎片,当有大对象进来时,可能总体空间是足够的,但是确找不到这样一块连续的内存空间而出现OOM异常。
3、标记整理/标记压缩算法: 该算法的主要思路是:先标记出所有需要被回收的对象,标记过程与上面一致,但是不是标记完就回收,而是让没有被标记的对象(存活的对象)全部向一端移动,最后清理掉边界以外的内存。该算法常被用来进行老年代的垃圾回收。
3.3 HotSpot虚拟机垃圾收集器
1、新生代收集器
-
Serial收集器:最基本的,历史最悠久的收集器。单线程收集器,在进行垃圾回收时必须暂停掉所有的用户线程,即Stop The World。但是它也有一个优点就是简单高效。采用的是复制算法,通过-XX:+UseSerialGC配置。
-
ParNew收集器:其实就是Serial收集器的多线程版本,在单CPU的情况下效果不一定会比Serial好。但是他的优势是可以配合CMS收集器进行工作,采用的是复制算法。通过-XX:+UseParNewGc配置
-
Parallel Scavenge收集器:Parallel Scavenge也是一款多线程收集器,与ParNew的不同之处在于关注点不一样,其他收集器主要关注尽量降低STW的时间,而它主要关注在吞吐量,采用的是复制算法。通过-XX:+UseParallelGC配置
2、老年代收集器
-
Serial Old收集器:是Serial收集器的老年代版本,采用的是“标记整理/标记压缩算法”,通过-XX:+UseSerialOldGC配置
-
Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,采用多线程以及“标记整理/标记压缩算法”。通过-XX:+UseParallelOldGC配置。
-
CMS 收集器:这时一款以获取最短回收停顿为目标的收集器,CMS是Concurrent Mark Sweep的缩写,从名字可以看到,这时一款使用“标记-清理”算法的并发收集器。主要分为:初始标记(CMS initial mark),并发标记(CMS concurrent mark),重新标记(CMS remark),并发清除(CMS concurrent sweep)4步,其中初始标记与重新标记两步仍然会导致‘Stop The World’,但是时间会比之前的收集器短许多。通过-XX:+UseConcMarkSweepGC配置
3、G1(GarbageFirst)收集器:当前收集器发展的最前沿的成果之一,能充分利用多CPU的硬件优势,来缩短STW的时间,可以不需要其他收集器的配合就可以管理整个堆内存,它最大的一个优势就是可预测停顿。
4、关于直接内存(Direct Memory)的GC
Direct Memory垃圾回收机制
:DirectByteBuffer所占内存是在堆内存之外的,因此一台机器堆内存分配的越多,会导致可用的堆外内存空间越少。Direct Memory的GC不能像新生代与老年代的GC一样,发现空间不足了就通知收集器进行垃圾回收,他只能等待老年代满了以后进行的Full GC顺便把他的废弃的内存回收掉,否则它只能等到抛出内存溢出的异常时,进入catch分支执行System.gc了,特别是如果设置了-XX:+DisableExplicit的话System.gc也会失效,注意,System.gc()执行的效果如下:
1
2
3
4
5
6
|
[Full GC [PSYoungGen: 0K->0K(306176K)] [ParOldGen: 526K->526K(699392K)] 526K->526K(1005568K) [PSPermGen: 2636K->2636K(262144K)] , 0.0045990 secs] [Times: user= 0.01 sys= 0.00 , real= 0.00 secs] |
也就是说,System.gc()执行的是一次Full GC。
下面证明Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。
-
堆外内存的配置
堆外内存使用的配置如下:
-XX:MaxDirectMemorySize=40M
下面通过实例来看下堆外内存的回收机制。
A、设置-verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=40M,执行如下代码:
1
2
3
|
while ( true ) { ByteBuffer buffer = ByteBuffer.allocate( 10 * 1024 * 1024 ); } |
执行结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[GC [PSYoungGen: 256266K->668K(306176K)] 256266K->676K(1005568K), 0.0019780 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 262030K->620K(306176K)] 262038K->636K(1005568K), 0.0011910 secs] [Times: user= 0.01 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 260143K->620K(306176K)] 260159K->636K(1005568K), 0.0014380 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 257778K->588K(306176K)] 257794K->604K(1005568K), 0.0011710 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 257349K->572K(306176K)] 257365K->588K(1005568K), 0.0012910 secs] [Times: user= 0.01 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 257073K->588K(348672K)] 257089K->604K(1048064K), 0.0014530 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 338944K->32K(348672K)] 338960K->593K(1048064K), 0.0012450 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 338238K->32K(348672K)] 338799K->593K(1048064K), 0.0004050 secs] [Times: user= 0.01 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 338140K->32K(348672K)] 338701K->593K(1048064K), 0.0004250 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 338075K->32K(348672K)] 338636K->593K(1048064K), 0.0004440 secs] [Times: user= 0.00 sys= 0.00 , real= 0.01 secs] [GC [PSYoungGen: 338033K->32K(348672K)] 338594K->593K(1048064K), 0.0004340 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 338005K->32K(348672K)] 338566K->593K(1048064K), 0.0004750 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 337987K->32K(348672K)] 338548K->593K(1048064K), 0.0004450 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 337975K->32K(348672K)] 338536K->593K(1048064K), 0.0004340 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] [GC [PSYoungGen: 337967K->32K(348672K)] 338528K->593K(1048064K), 0.0004620 secs] [Times: user= 0.00 sys= 0.00 , real= 0.01 secs] [GC [PSYoungGen: 337962K->32K(348672K)] 338523K->593K(1048064K), 0.0003790 secs] [Times: user= 0.00 sys= 0.00 , real= 0.00 secs] |
运行这段代码会发现:程序可以一直运行下去,不会报OutOfMemoryError。如果使用了-verbose:gc -XX:+PrintGCDetails,会发现程序频繁的进行垃圾回收活动。
B、重新设置启动参数为:-verbose:gc -XX:+PrintGCDetails -XX:+DisableExplicitGC -XX:MaxDirectMemorySize=40M, 与之前的JVM启动参数相比,增加了-XX:+DisableExplicitGC,这个参数作用是禁止代码中显示调用GC。代码如何显示调用GC呢,通过System.gc()函数调用。如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果,相当于是没有这行代码一样。执行同样代码,执行效果如下:
显然堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险。
5、使用jstat命令查看线上堆内存以及GC情况
-
命令格式:jstat -gcutil LVMID 间隔时间 执行次数( 通过jps命令可拿到LVMID)
说明: S0
表示Survivor0使用的比例,有时表示From,有时表示To
S1
表示Survivor1使用的比例,有时表示From,有时表示To
E
表示Eden区使用的比例
O
表示Old区即老年代使用的比例
P
表示Perm区即永久代或者方法区已使用的比例
YGC
表示程序启动以来发生的Minor GC(Young GC)的总次数
YGCT
表示Minor GC的总耗时
FGC
表示程序启动以来发生的Full GC的总次数
FGCT
表示Full GC的总耗时
GCT
表示GC总耗时
附:jstat命令其他选项
1
2
3
4
5
6
7
8
9
|
-class 监视类装载、卸载数量,总空间以及装载类的耗时 -gc 监视整个java堆的状况,包括Eden区、两个Survivor区以及新生代与老年代的容量,已用空间以及GC总耗时等信息。 -gcutil 与上面gc类似,但是gcutil主要关注百分比 -gccause 与gcutil类似,但是增加了导致上次gc发生的原因 -gcnew 监视新生代GC的状况 -gcnewcapacity 与gcnew类似,输出主要关注使用到的最大、最小空间 -gcold 监视老年代GC的状况 -gcoldcapacity 与gcold类似,输出主要关注使用到的最大、最小空间 -gcpermcapacity 输出永久代使用到的最大、最小空间 |