JVM运行时数据区---方法区(演变和垃圾回收)

方法区演进细节与垃圾回收

方法区演进细节

永久代演进过程:

  1. 首先明确:只有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。
  2. Hotspot 中方法区的变化如下图:
    在这里插入图片描述
    JDK6 :方法区由永久代实现,使用 JVM 虚拟机内存(虚拟的内存)。
    在这里插入图片描述
    JDK7 :方法区由永久代实现,使用 JVM 虚拟机内存。
    在这里插入图片描述
    JDK8 :方法区由元空间实现,使用物理机本地内存。
    在这里插入图片描述

永久代为什么要被元空间替代?

官方文档:http://openjdk.java.net/jeps/122

  1. 随着 Java8 的到来,HotSpot VM 中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。
  2. 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
  3. 这项改动是很有必要的,原因有:
  • 为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生 Perm 区的 OOM 。比如某个实际 Web 工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。如:Exception in thread ‘dubbo client x.x connector’ java.lang .OutOfMemoryError:PermGen space,而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。

  • 对永久代进行调优是很困难的。方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再用的类型,方法区的调优主要是为了降低 Full GC。

字符串常量池

字符串常量池 StringTable 为什么要调整位置?

  • JDK7 中将 StringTable 放到了堆空间中。因为永久代的回收效率很低,在 Full GC 的时候才会执行永久代的垃圾回收,而 Full GC 是老年代的空间不足、永久代不足时才会触发;
  • 这就导致StringTable回收效率不高,而开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放到堆里,能及时回收内存。

静态变量放在哪里

对象实体在哪里放着?

/**
 * 结论:
 * 1、静态引用对应的对象实体(也就是这个new byte[1024 * 1024 * 100])始终都存在堆空间,
 * 2、只是那个变量(相当于下面的arr变量名)在 JDK6,JDK7,JDK8 存放位置中有所变化
 *
 * jdk7:
 * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 * jdk 8:
 * -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 */
public class StaticFieldTest {
    
    
    private static byte[] arr = new byte[1024 * 1024 * 100];//100MB

    public static void main(String[] args) {
    
    
        System.out.println(StaticFieldTest.arr);
    }
}

JDK6 环境下:
在这里插入图片描述
JDK7 环境下:
在这里插入图片描述
JDK8 环境下:
在这里插入图片描述
变量(名)存放在哪里?

用JHSDB工具来进行分析:

public class StaticObjTest {
    
    
    static class Test {
    
    
        static ObjectHolder staticObj = new ObjectHolder();
        ObjectHolder instanceObj = new ObjectHolder();

        void foo() {
    
    
            ObjectHolder localObj = new ObjectHolder();
            System.out.println("done");
        }
    }

    private static class ObjectHolder {
    
    
    }

    public static void main(String[] args) {
    
    
        Test test = new StaticObjTest.Test();
        test.foo();
    }
}

JDK6 环境下:

  1. staticObj 随着 Test 的类型信息存放在方法区;

  2. instanceObj 随着 Test 的对象实例存放在Java堆;

  3. localObject (局部变量)则是存放在 foo() 方法栈帧的局部变量表中;

  4. 测试发现:三个对象的数据在内存中的地址都落在 Eden 区范围内,所以结论:只要是对象实例必然会在Java堆中分配。
    在这里插入图片描述

  1. 0x00007f32c7800000(Eden区的起始地址) —- 0x00007f32c7b50000(Eden区的终止地址),
  2. 可以发现三个变量都在这个范围内,
  3. 所以可以得到上面结论。
  1. 接着,找到了一个引用该 staticObj 对象的地方,是在一个 java.lang.Class 的实例里,并且给出了这个实例的地址,通过 Inspector 查看该对象实例,可以清楚看到这确实是一个 java.lang.Class 类型的对象实例,里面有一个名为 staticobj 的实例字段:
    作
  • 从《Java虚拟机规范》所定义的概念模型来看,所有 Class 相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK7 及其以后版本的 HotSpot 虚拟机选择把静态变量与类型在 Java 语言一端的映射 Class 对象存放在一起,存储于 Java 堆之中,从实验中也明确验证了这一点。

方法区的垃圾回收

  1. .有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的ZGC 收集器就不支持类卸载)。

  2. .一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。

  3. .方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

    1.先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:

    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

    2.HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。

    3.回收废弃常量与回收 Java 堆中的对象非常类似。(关于常量的回收比较简单,重点是类的回收)

方法区的类卸载:

  1. 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

    • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例;
    • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP 的重加载等,否则通常是很难达成的;
    • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  2. Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了-Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClass-Loading、-XX: +TraceClassUnLoading 查看类加载和卸载信息。

  3. 在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 以及 OSGi 这类频繁自定义类加载器的场景中,通常都需要 Java 虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

运行时数据区总结

在这里插入图片描述

常见面试题

  1. 百度
  • 三面:说一下JVM内存模型吧,有哪些区?分别干什么的?
  1. 蚂蚁金服:
  • Java8的内存分代改进
  • JVM内存分哪几个区,每个区的作用是什么?
  • 一面:JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区?
  • 二面:Eden和survior的比例分配。
  1. 小米:
  • jvm内存分区,为什么要有新生代和老年代?
  1. 字节跳动:
  • 二面:Java的内存分区
  • 二面:讲讲vm运行时数据库区。
  • 什么时候对象会进入老年代?
  1. 京东:
  • JVM的内存结构,Eden和Survivor比例。
  • JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和survivor。
  1. 天猫:
  • 一面:Jvm内存模型以及分区,需要详细到每个区放什么。
  • 一面:JVM的内存模型,Java8做了什么改变?
  1. 拼多多:
  • JVM内存分哪几个区,每个区的作用是什么?
  1. 美团:
  • java内存分配
  • jvm的永久代中会发生垃圾回收吗?
  • 一面:jvm内存分区,为什么要有新生代和老年代?

猜你喜欢

转载自blog.csdn.net/qq_33626996/article/details/114547495