2

深度剖析 | 【JVM深层系列】[HotSpotVM研究系列] JVM调优的"标准参数"的各...

 1 year ago
source link: https://www.cnblogs.com/liboware/p/16986641.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【易错问题】Major GC和Full GC的区别是什么?触发条件呢?

相信大多数人的理解是Major GC只针对老年代,Full GC会先触发一次Minor GC,不知对否?我参考了R大的分析和介绍,总结了一下相关的说明和分析结论。

在基于HotSpotVM的基础角度

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

Partial GC(部分回收模式)

Partial GC代表着并不收集整个GC堆的模式

  • Young Generation GC(新生代回收模式):它主要是进行回收新生代范围内的内存对象的GC回收器。
  • Old/Tenured Generation GC(老年代回收模式):它主要是针对于回收老年代Old/Tenured Generation范围内的GC垃圾回收器(CMS的Concurrent Collection是这个模式)。
  • Mixed Generation GC(混合代回收模式):收集整个young gen以及部分old gen的GC。只有G1有这个模式

Full GC(全体回收模式)

Full GC代表着收集整个JVM的运行时堆+方法区+直接堆外内存的总体范围内。(甚至可以理解为JVM进程范围内的绝大部分范围的数据区域)。

它会涵盖了所有的模式和区域包含:Young Gen(新生代)、Tenured Gen(老生代)、Perm/Meta Gen(元空间)(JDK8前后的版本)等全局范围的GC垃圾回收模式。

在一般情况下Major GC通常是跟Full GC是等价的,收集整个GC堆。但如果从HotSpot VM底层的细节出发,如果再有人说“Major GC”的时候一定要问清楚他想要指的是上面的Full GC还是Old/Tenured GC。

基于最简单的分代式GC策略

触发条件是:Young GC

按HotSpot VM的Serial GC的实现来看,当Young gen中的Eden区分达到阈值(属于一定的百分比进行控制)的时候触发。

注意:Young GC中有部分存活对象会晋升到Old/Tenured Gen,所以Young GC后Old Gen的占用量通常会有所升高

触发条件是:Full GC
  1. 当准备要触发一次Young GC时,如果发现统计数据说之前Young Old/Tenured Gen剩余的空间大,则不会触发Young GC,而是转为触发Full GC(因为HotSpot VM的GC里,除了CMS的Concurrent collection之外,其它能收集Old/Tenured Gen的GC都会同时收集整个GC堆,包括Young gen,所以不需要事先触发一次单独的Young GC);

  2. 如果有Perm/Meta gen的话,要在Perm/Meta gen分配空间但已经没有足够空间时,也要触发一次full GC。

  3. System.gc()方法或者Heap Dump自带的GC,默认也是触发Full GC。HotSpot VM里其它非并发GC的触发条件复杂一些,不过大致的原理与上面说的其实一样。

注意:Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发Full GC前先执行一次Young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低Full GC的暂停时间(因为young GC会尽量清理了Young Gen的垃圾对象,减少了Full GC的扫描工作量)。控制这个行为的VM参数是-XX:+ScavengeBeforeFullGC。

触发条件是:Concurrent GC

Concurrent GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查Old Gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对Old gen做并发收集。

GC回收器对应的GC模式列举

在Hotspot JVM实现的Serial GC, Parallel GC, CMS, G1 GC中大致可以对应到某个Young GC和Old GC算法组合;

  • Serial GC算法:Serial Young GC + Serial Old GC (实际上它是全局范围的Full GC);
  • Parallel GC算法:Parallel Young GC + 非并行的PS MarkSweep GC / 并行的Parallel Old GC(这俩实际上也是全局范围的Full GC),选PS MarkSweep GC 还是 Parallel Old GC 由参数UseParallelOldGC来控制;
  • CMS算法:ParNew(Young)GC + CMS(Old)GC (piggyback on ParNew的结果/老生代存活下来的object只做记录,不做compaction)+ Full GC for CMS算法(应对核心的CMS GC某些时候的不赶趟,开销很大);
  • G1 GC:Young GC + mixed GC(新生代,再加上部分老生代)+ Full GC for G1 GC算法(应对G1 GC算法某些时候的不赶趟,开销很大);

GC回收模式的触发总结

  • 搞清楚了上面这些组合,我们再来看看各类GC算法的触发条件。简单说,触发条件就是某GC算法对应区域满了,或是预测快满了。比如,
    • 各种Young GC的触发原因都是eden区满了;
    • Serial Old GC/PS MarkSweep GC/Parallel Old GC的触发则是在要执行Young GC时候预测其promote的object的总size超过老生代剩余size;
    • CMS GC的initial marking的触发条件是老生代使用比率超过某值;
    • G1 GC的initial marking的触发条件是Heap使用比率超过某值;
    • Full GC for CMS算法和Full GC for G1 GC算法的触发原因很明显,就是4.3 和 4.4 的fancy算法不赶趟了,只能全局范围大搞一次GC了(相信我,这很慢!这很慢!这很慢!);

【坑点与坑点】-XX:+DisableExplicitGC 与 NIO的direct memory的关系

很多人都见过JVM调优建议里使用这个参数,对吧?但是为什么要用它,什么时候应该用而什么时候用了会掉坑里呢?

  1. 首先,要了解的是这个参数的作用。在Oracle/Sun JDK这个具体实现上,System.gc()的默认效果是引发一次stop-the-world的Full GC,由上面所知就是针对于整个GC堆做内存垃圾收集。

  2. 再次,如果采用了用了-XX:+DisableExplicitGC参数后,System.gc()的调用就会变成一个空调用,完全不会触发任何GC(但是“函数调用”本身的开销还是存在的哦~)。

    • 为啥要用这个参数呢?最主要的原因是为了防止某些小白同学在代码里到处写System.gc()的调用而干扰了程序的正常运行吧。
      1. 有些应用程序本来可能正常跑一天也不会出一次Full GC,但就是因为有人在代码里调用了System.gc()而不得不间歇性被暂停。
      2. 有些时候这些调用是在某些库或框架里写的,改不了它们的代码但又不想被这些调用干扰也会用这参数。

-XX:+DisableExplicitGC看起来这参数应该总是开着嘛。有啥坑呢?

下述三个条件同时满足时会发生的

  1. 应用本身在GC堆内的对象行为良好,正常情况下很久都不发生Full GC。
  2. 应用大量使用了NIO的direct memory,经常、反复的申请DirectByteBuffer。
  3. 使用了-XX:+DisableExplicitGC。

能观察到的现象是:

highlighter- code-theme-dark CSS
java.lang.OutOfMemoryError: Direct buffer memory  
    at java.nio.Bits.reserveMemory(Bits.java:633)  
    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:98)  
    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)  
用一个案例来分析这现象:
java
import java.nio.*;  
public class DisableExplicitGCDemo {  
  public static void main(String[] args) {  
    for (int i = 0; i < 100000; i++) {  
      ByteBuffer.allocateDirect(128);  
    }  
    System.out.println("Done");  
  }  
}  

然后编译、运行。

highlighter- code-theme-dark Bash
$ java -version  
java version "1.6.0_25"  
Java(TM) SE Runtime Environment (build 1.6.0_25-b06)  
Java HotSpot(TM) 64-Bit Server VM (build 20.0-b11, mixed mode)  
$ javac DisableExplicitGCDemo.java   
$ java -XX:MaxDirectMemorySize=10m -XX:+PrintGC -XX:+DisableExplicitGC DisableExplicitGCDemo
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory  
    at java.nio.Bits.reserveMemory(Bits.java:633)  
    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:98)  
    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)  
    at DisableExplicitGCDemo.main(DisableExplicitGCDemo.java:6)  
$ java -XX:MaxDirectMemorySize=10m -XX:+PrintGC DisableExplicitGCDemo  
[GC 10996K->10480K(120704K), 0.0433980 secs]  
[Full GC 10480K->10415K(120704K), 0.0359420 secs]  
Done  
  • 可以看到,同样的程序,不带-XX:+DisableExplicitGC时能正常完成运行,而带上这个参数后却出现了OOM。-XX:MaxDirectMemorySize=10m限制了DirectByteBuffer能分配的空间的限额,以便问题更容易展现出来。不用这个参数就得多跑一会儿了。

  • 循环不断申请DirectByteBuffer但并没有引用,所以这些DirectByteBuffer应该刚创建出来就已经满足被GC的条件,等下次GC运行的时候就应该可以被回收。

  • 实际上却没这么简单。DirectByteBuffer是种典型的“冰山”对象,也就是说它的Java对象虽然很小很无辜,但它背后却会关联着一定量的native memory资源,而这些资源并不在GC的控制之下,需要自己注意控制好。

对JVM如何使用native memory不熟悉的同学可以研究一下这篇演讲,“Where Does All the Native Memory Go”。

【盲点问题】DirectByteBuffer的回收问题

Oracle/Sun JDK的实现里,DirectByteBuffer有几处值得注意的地方。

  1. DirectByteBuffer没有finalizer,它的native memory的清理工作是通过sun.misc.Cleaner自动完成的。
  2. sun.misc.Cleaner是一种基于PhantomReference的清理工具,比普通的finalizer轻量些。

"A cleaner tracks a referent object and encapsulates a thunk of arbitrary cleanup code. Some time after the GC detects that a cleaner's referent has become phantom-reachable, the reference-handler thread will run the cleaner."

java
/** 
 * General-purpose phantom-reference-based cleaners. 
 * 
 * <p> Cleaners are a lightweight and more robust alternative to finalization. 
 * They are lightweight because they are not created by the VM and thus do not 
 * require a JNI upcall to be created, and because their cleanup code is 
 * invoked directly by the reference-handler thread rather than by the 
 * finalizer thread.  They are more robust because they use phantom references, 
 * the weakest type of reference object, thereby avoiding the nasty ordering 
 * problems inherent to finalization. 
 * 
 * <p> A cleaner tracks a referent object and encapsulates a thunk of arbitrary 
 * cleanup code.  Some time after the GC detects that a cleaner's referent has 
 * become phantom-reachable, the reference-handler thread will run the cleaner. 
 * Cleaners may also be invoked directly; they are thread safe and ensure that 
 * they run their thunks at most once. 
 * 
 * <p> Cleaners are not a replacement for finalization.  They should be used 
 * only when the cleanup code is extremely simple and straightforward. 
 * Nontrivial cleaners are inadvisable since they risk blocking the 
 * reference-handler thread and delaying further cleanup and finalization. 
 * 
 * 
 * @author Mark Reinhold 
 * @version %I%, %E% 
 */  

Oracle/Sun JDK中的HotSpot VM只会在Old Gen GC(Full GC/Major GC或者Concurrent GC都算)的时候才会对Old Gen中的对象做Reference Processing,而在Young GC/Minor GC时只会对Young Gen里的对象做Reference processing。Full GC会对Old Gen做Reference processing,进而能触发Cleaner对已死的DirectByteBuffer对象做清理工作。

  • 如果很长一段时间里没做过GC或者只做了Young GC的话则不会在Old Gen触发Cleaner的工作,那么就可能让本来已经死了的、但已经晋升到Old Gen的DirectByteBuffer关联的Native Memory得不到及时释放

  • 为DirectByteBuffer分配空间过程中会显式调用System.gc(),以通过Full GC来强迫已经无用的DirectByteBuffer对象释放掉它们关联的native memory

java
// These methods should be called whenever direct memory is allocated or  
// freed.  They allow the user to control the amount of direct memory  
// which a process may access.  All sizes are specified in bytes.  
static void reserveMemory(long size) {   
    synchronized (Bits.class) {  
        if (!memoryLimitSet && VM.isBooted()) {  
            maxMemory = VM.maxDirectMemory();  
            memoryLimitSet = true;  
        }  
        if (size <= maxMemory - reservedMemory) {  
            reservedMemory += size;  
            return;  
        }  
    }  
    System.gc();  
    try {  
        Thread.sleep(100);  
    } catch (InterruptedException x) {  
        // Restore interrupt status  
        Thread.currentThread().interrupt();  
    }  
    synchronized (Bits.class) {  
        if (reservedMemory + size > maxMemory)  
            throw new OutOfMemoryError("Direct buffer memory");  
           reservedMemory += size;
    }  
}  

这几个实现特征使得Oracle/Sun JDK依赖于System.gc()触发GC来保证DirectByteMemory的清理工作能及时完成。

如果打开了-XX:+DisableExplicitGC,清理工作就可能得不到及时完成,于是就有机会见到direct memory的OOM,也就是上面的例子演示的情况。我们这边在实际生产环境中确实遇到过这样的问题。

如果你在使用Oracle/Sun JDK,应用里有任何地方用了direct memory,那么使用-XX:+DisableExplicitGC要小心。如果用了该参数而且遇到direct memory的OOM,可以尝试去掉该参数看是否能避开这种OOM。如果担心System.gc()调用造成Full GC频繁,可以尝试下面提到 -XX:+ExplicitGCInvokesConcurrent 参数

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK