7.6. 方法区的演进细节
- 首先明确:只有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java 虚拟机规范》管束,并不要求统一
- Hotspot 中方法区的变化:
7.6.1. 为什么永久代要被元空间替代?
官网地址:JEP 122: Remove the Permanent Generation (java.net)
随着 Java8 的到来,HotSpot VM 中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
这项改动是很有必要的,原因有:
为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生 Perm 区的 oom。比如某个实际 Web 工 程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
"Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space"
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。
对永久代进行调优是很困难的。
有些人认为方法区(如 HotSpot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK 11 时期的 ZGC 收集器就不支持类卸载)。 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
7.6.2. StringTable 为什么要调整位置?
jdk7 中将 StringTable 放到了堆空间中。因为永久代的回收效率很低,在 full gc 的时候才会触发。而 full gc 是老年代的空间不足、永久代不足时才会触发。
这就导致 StringTable 回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
7.6.3. 静态变量存放在那里?
示例1
/** * 静态引用对应的对象实体始终都存在堆空间 * jdk7: * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails * jdk8: * -Xms200m -Xmx200m-XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails */ public class StaticFieldTest { private static byte[] arr = new byte[1024 * 1024 * 100]; public static void main(String[] args) { System.out.println(StaticFieldTest.arr); try { Thread.sleep(1000000); } catch (InterruptedException e){ e.printStackTrace(); } } }
结论:静态引用对应的对象实体始终都存在堆空间
- 示例2
/** * 《深入理解Java虚拟机》中的案例: * staticObj、instanceObj、localObj存放在哪里? * @author shkstart shkstart@126.com * @create 2020 11:39 */ 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(); } }
使用 JHSDB 工具进行分析,这里细节略掉
**三个变量的引用:**staticobj 随着 Test 的类型信息存放在方法区,instanceobj 随着 Test 的对象实例存放在 Java 堆,localobject 则是存放在 foo()方法栈帧的局部变量表中。
测试发现:三个对象的数据在内存中的地址都落在 Eden 区范围内,所以结论:只要是对象实例必然会在 Java 堆中分配。
接着,找到了一个引用该 staticobj 对象的地方,是在一个 java.lang.Class 的实例里,并且给出了这个实例的地址,通过 Inspector 查看该对象实例,可以清楚看到这确实是一个 java.lang.Class 类型的对象实例,里面有一个名为 staticobj 的实例字段:
从《Java 虚拟机规范》所定义的概念模型来看,所有 Class 相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java 虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK7 及其以后版本的 HotSpot 虚拟机选择把静态变量与类型在 Java 语言一端的映射 class 对象存放在一起,存储于 Java 堆之中,从我们的实验中也明确验证了这一点
备注:前面已经说过了,没有栈上分配这码事,如果未逃逸,做标量替换,把一个对象分解为多个成员变量,也还是存在堆上。
7.7. 方法区的垃圾回收
有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 zGC 收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近 Java 语言层次的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
回收废弃常量与回收 Java 堆中的对象非常类似。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息
在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 以及 OSGi 这类频繁自定义类加载器的场景中,通常都需要 Java 虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
总结
关于MiniorGC和Full GC的区别. MinorGC是老年代的GC,FullGC是整个堆的GC
栈帧中的动态链接指向的是方法区中的方法,代表当前操作的是哪个方法
常见面试题
百度:
说一下 JVM 内存模型吧,有哪些区?分别干什么的?
蚂蚁金服:
Java8 的内存分代改进 JVM 内存分哪几个区,每个区的作用是什么?
一面:JVM 内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个 survivor 区?
二面:Eden 和 survior 的比例分配
小米:
jvm 内存分区,为什么要有新生代和老年代
字节跳动:
二面:Java 的内存分区
二面:讲讲 vm 运行时数据库区 什么时候对象会进入老年代?
京东:
JVM 的内存结构,Eden 和 Survivor 比例。
JVM 内存为什么要分成新生代,老年代,持久代。
新生代中为什么要分为 Eden 和 survivor。
天猫:
一面:Jvm 内存模型以及分区,需要详细到每个区放什么。
一面:JVM 的内存模型,Java8 做了什么改
拼多多:
JVM 内存分哪几个区,每个区的作用是什么?
美团:
java 内存分配 jvm 的永久代中会发生垃圾回收吗?
一面:jvm 内存分区,为什么要有新生代和老年代?