jvm对大对象分配内存的特殊处理

    前段日子在和leader交流技术的时候,偶然听到jvm在分配内存空间给大对象时,如果young区空间不足会直接在old区切一块过去。对于这个结论很好奇,也比较怀疑,所以就上网搜了下,发现还真有这么回事。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。以下给出具体代码来说明:首先定义好jvm内存各个区域的大小。我设定的是eden区8M,from和to各1M,old区10M,总共20M的空间,参数如下:

  1. -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8  

    紧接着,开始写程序。很简单,就是初始化一个9M的程序,然后用jstat命令看jdk的内存使用情况。

public class App {  
    private static final int _1MB = 1024 * 1024;  
  
    /** 
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 
     * -XX:PretenureSizeThreshold=3145728 
     */  
    public static void main(String[] args) {  
        byte[] allocation = new byte[9*_1MB];  
        while(true){  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  

    然后打成jar,执行。结果如下:

S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT     
  0.00   0.00  18.04  90.00  23.08      0    0.000    20    0.027    0.027  

    果然,当对象大小大于eden区的时候会直接扔到old区。但我还不满足与此,于是将对象改大了些,改成了11M。再次尝试发现结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space  
    at com.taobao.jdkmem.App.main(App.java:17)  

    到这里结束了么?当然没有:)这个是一个大的完整的对象,当大对象本身是由一连串的小对象组成的时候,会不会不再OOM呢?于是改了代码再次尝试:

public class App {  
    private static final int _1MB = 1024 * 1024;  
  
    /** 
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 
     * -XX:PretenureSizeThreshold=3145728 
     */  
    public static void main(String[] args) {  
        byte[][] allocation;  
        allocation = new byte[11][_1MB]; // 直接分配在老年代中  
        while(true){  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  

    再次运行,结果如下:

S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT     
  0.00  38.06  67.68  60.02  23.10      1    0.007    14    0.012    0.019  

果然,这次居然又被jvm给生吃下去了。不过这次并非所有的都在old区,而是有一部分还在young区里活着。看来jvm还是足够彪悍的。

由此可见,当出现大对象的时候,jvm用牺牲部分宝贵的old区的方式来保证了整个jvm的正常运转。所以,程序中尽量要避免大对象,如果实在不行,就让大对象活的尽量久些,莫要new一个然后gc掉再new一个再gc,这么爆jvm可不太友好。

到这里结束了吧?你猜对了,还没有:P既然知道jvm会对大对象申请内存做特殊处理,那么就在琢磨程序员有没有方法干预这个过程呢?答案是有的,就是使用这个参数-XX:PretenureSizeThreshold。这个参数的单位是Byte,其作用是当新对象申请的内存空间大于这个参数值的时候,直接扔到old区。做个试验就证明了:

public class App {  
    private static final int _1MB = 1024 * 1024;  
  
    /** 
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 
     * -XX:PretenureSizeThreshold=3145728 
     */  
    public static void main(String[] args) {  
        byte[] allocation = new byte[4*_1MB];  
        while(true){  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  

    运行命令如下:

  1. java -jar -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 memtest-1.0-SNAPSHOT.jar   

    我设置的阈值是3M,意味着超过3M的对象会被直接扔到old区。结果是皆大欢喜,新对象直接被扔到了old区:

  1. S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT     
  2.   0.00   0.00  18.04  40.00  23.08      0    0.000     0    0.000    0.000  

    试验有了结果,自然而然心情愉悦。但这个参数使用时需要慎重,因为fullgc的代价很高,因此old区就显得非常宝贵。除非你真的清楚你在干什么,否则莫要轻易玩这个参数,万一搞个频繁fullgc就玩大了。ok,到此打完收工。

猜你喜欢

转载自blog.csdn.net/qq_15037231/article/details/84137220