JVM 永久代和元空间

简单介绍

永久代是JDK7及以前对方法区的一个JVM实现。主要存放的是类信息、字符串常量、静态常量等(PS:JDK7之后比如字符串常量池等信息移到了堆内,不在永久代存储了)

元空间是JDK8之后才有,目的是取代永久代。

区别

对比项永久代PermGen元空间Metaspace
支持JDK版本JDK7及以前JDK8及以后
JVM参数-XX:PermSize 初始永久代大小
-XX:MaxPermSize : 最大永久代大小。默认32位64M,64位82M(指针膨胀)
注意:MaxPermSize设置,jvm启动的时候就会申请MaxPermSize大小的内存。
-XX:MetaspaceSize,初始空间配额,单位bytes
-XX:MaxMetaspaceSize,分配的最大空间。默认是没有限制的。
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比。
注意:MaxMetaspaceSize设置,jvm启动的时候不会申请这么大一块内存,与MaxPermSize是有区别的
OOM受限于默认值大小,易产生OOM。虽然有参数可以指定永久代大小,但是因为存储内容较多,比较难预估出来一个合适的值。受限于本机内存大小,OOM概率小。
GC永久代和老年代GC绑定到一起,二者其一满了之后便可触发永久代和老年代的垃圾回收
JDK7以后,如果使用G1垃圾回收器,永久代的垃圾回收只有在FullGC的时候才会触发。G1仅仅在PermGen满了或者应用分配内存的速度比G1并发垃圾收集速度快的时候才触发FullGC (可参见Oracle博客说明 及文末引用地址说明)
CMS回收器下,可以使用-XX:+CMSClassUnloadingEnabled 参数来让永久代的垃圾回收可以在CMC并发回收阶段处理掉。这点和G1不同。
当metaspace使用达到最大值的时候,会自动触发GC。但不影响堆上GC。
归属不属于堆内存,但内存空间和堆内存相连(存疑不属于堆内存,属于Native Memory,受限于服务器内存
补充虽然可能OOM但不至于导致服务器出现问题(因为有永久代大小限制)内存默认无上限,受限于服务器内存,所以可能会导致服务器出现问题,需要监控Metaspace的使用情况

移除永久代原因

  1. 减少永久代OOM的情况。

    永久代存放的一些类信息等,在动态生成类之前基本是足够的,但是随着动态类生成的技术的发展,原本的类信息量及大小变的开始不再那么永久不变更了。另外最开始的永久代里还存放字符串常量池,这块的增长也不可控,所以也就可能出现OOM的情况。(PS:JDK7已经开始了字符串常量池、符号引用、静态变量从永久代的迁出)

  2. 降低GC复杂度

    永久代和老年代的GC绑定到一起,但是永久代的内容被GC的可能性要小于老年代,所以,对GC而言,一方面效率不高,另外一方面,复杂度也高了很多(metaspace上gc不需要做压缩和扫描)。

  3. 商业合并

    这个应该是最主要的原因。 现有主流的JVM实现中,除了Hotspot之外,基本没有永久代这个概念,但仍旧运行的很好,如 JRockit。之后Oracle收购了Jrockit之后,便开始了Hotspot和JRockit的合并(非代码合并,而是取了JRockit优点做开发)。于是,在JDK8之后,便取消掉了永久代。

PS:

方法区 不等同于 永久代。 永久代是对方法区的一种实现。

永久代和堆逻辑上是分开的。 一般我们称永久代叫“非堆”,但物理上内存是相连的。

关于永久代和堆的关系
  1. 永久代在不在堆里?

    各有各的说法,各有各的理论。 我理解是内存相连,但逻辑分开。

    推荐看几篇文章了解下各自观点

    方法区的Class信息,又称为永久代,是否属于Java堆? - 毛海山的回答 - 知乎 https://www.zhihu.com/question/49044988/answer/113961406

    https://stackoverflow.com/questions/41358895/permgen-is-part-of-heap-or-not

    https://stackoverflow.com/questions/2129044/java-heap-terminology-young-old-and-permanent-generations/2129294#2129294

    https://stackoverflow.com/questions/1279449/what-is-perm-space

    https://stackoverflow.com/questions/4848669/perm-space-vs-heap-space

  2. Xmx不包含永久代大小

测试

测试代码:

public class PermGenTest {

    private final static int ONE_MB = 1024*1024;

    /**
     * JVM参数: -xmx20m -xms20m -xmn10m -XX:MaxPermSize=30m -XX:PermSize=30m -XX:PretenureSizeThreshold=100
     * @param args
     */
    public static void main(String[] args) {


        //连续申请3M内存
        for (int i =0 ; i < 100; i ++){
            allocateMemory(3);
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


        try {
            Thread.currentThread().join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


    private static void allocateMemory(int size){

        for (int i =0 ; i < size ; i++){
            byte[] bytes = new byte[ONE_MB];
        }

    }

}

1、测试老年代和永久代任意一方满了之后,会触发永久代垃圾回收。

测试环境: JDK1.6/1.7 + CMS垃圾回收 (以JDK1.6为例,1.7的结果与下面类似)

关于代码的说明:

使用-XX:PretenureSizeThreshold=100限定只要对象大小超过100字节,就会直接分配到老年代。代码中,我们连续申请3M大小内存,所以很快会触发CMS的老年代垃圾回收。

JVM参数如下:

/opt/soft/jdk/jdk1.6.0_45/bin/java -verbose:gc -Xloggc:gc.l -XX:+PrintGCDetails -Djava.rmi.server.hostname=192.168.1.x -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=33306 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Xmx20m -Xmn10m -Xms20m -XX:MaxPermSize=30m -XX:PermSize=30m -XX:PretenureSizeThreshold=100 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC  PermGenTest

产生的GC结果:

image-20200430180500797

可以看到,当老年代触发CMS垃圾回收的时候,永久代也会被回收(PS:图中永久代没有可被回收的东西,所以数值没变化)

如果我们把-XX:PretenureSizeThreshold调整成5M,则对象不会进入到老年代,这时候年轻代GC,并没有同时触发对永久代的GC。(如下图,可以看到没有Perm的回收日志)

image-20200430181002059

Ps:

-XX:MaxTenuringThreshold只对串行回收器和ParNew有效,对ParallGC无效

2、测试G1和CMS的永久代垃圾回收策略

测试环境: JDK1.7 + G1垃圾回收 (CMS的已在上面做过测试,此处查看G1和CMS触发垃圾回收场景)

代码不变,JVM参数调整为G1垃圾回收器

JVM参数:

/opt/soft/jdk/jdk1.7.0_80/bin/java -verbose:gc -Xloggc:gc.l -XX:+PrintGCDetails -Djava.rmi.server.hostname=192.168.1.x -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=33306 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Xmx20m -Xmn10m -Xms20m -XX:PretenureSizeThreshold=100 -XX:+UseG1GC  PermGenTest

PS: G1下不建议设置Xmn。此处为了测试和CMS的对比,保留xmn参数。

image-20200506200156598

整个的GC过程,并没有对永久代进行回收。

但如果FullGC时,则会回收永久代

image-20200506200305600

所以也验证了前面所提到的永久代GC在不同的垃圾回收器下的不同表现。

参考

https://juejin.im/post/5df5fde36fb9a0162c486c71

https://www.cnblogs.com/paddix/p/5309550.html

https://cloud.tencent.com/developer/article/1415205

https://www.baeldung.com/java-permgen-metaspace

关于G1的Permgen说明:https://blogs.oracle.com/poonam/about-g1-garbage-collector%2c-permanent-generation-and-metaspace

https://segmentfault.com/a/1190000005036183

https://www.sczyh30.com/posts/Java/jvm-metaspace/

番外:

关于HotSpot和JRockit: https://www.zhihu.com/question/29265430/answer/43818804

关于JVM的问题,推荐到 https://blogs.oracle.com/ 查看,基本上是官方的人写的博客,可信度高。当然能力强的可以看JDK源码