看图学 JVM 目录 https://blog.csdn.net/weixin_39340061/article/details/106611873
一、内容结构
二、方法区基本知识
1. 官方介绍
2. 认识方法区
- 运行时数据区整体结构图
- 从线程共享的角度看
3. 核心知识
- 与堆一样,也是线程共享的
- 在 JVM 启动时创建,结束时销毁
- 大小可以选择固定或可扩展
- 空间不足时会抛出 OOM
三、方法区演进
1. 方法区在不同 JDK 版本中的演进
- JDK 1.6 及以前版本中, 采用永久代实现,静态变量存放在永久代
- JDK 1.7 ,将静态变量和 StringTable 移出永久代,放置到堆空间
- JDK 1.8,方法区被移动到与堆不相连的本地内存,名称也更改为:元空间(Metaspace)。
JDK 1.8 中,StringTable 和静态变量仍存放在堆区
2. 永久代为什么要替换成元空间?
- 永久代的空间大小难以确定
- 在某些场景下,如果动态加载的类过多,容易产生 Perm 区的 OOM。比如一个 Web 工程中,因为功能多,运行时不断动态加载很多类。经常出现 OOM 这样的致命错误。
- 元空间与永久代最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间大小仅受本地内存限制。
- 对永久代进行调优很困难
3. StringTable 为什么要调整?
JDK 7 之前, StringTable 存放在永久代,但永久代在 Full GC 时候才会回收,Full GC 又只在老年代或永久代空间不足是才会触发,从而导致回收效率不高。我们开发中会有大量的字符串被创建, 如果回收效率低,会导致永久代空间不足,抛出 OOM 等致命异常。所以从 JDK 7 开始,StringTable 被移动到堆空间。
四、方法区设置
1. JDK 1.7 及以前设置方法区
- -XX:PermSize=xxM,设置永久代初始大小,默认值是 20.75 M
- -XX:MaxPermaSize=xxx, 设置最大空间大小,32 位系统默认值是 64 M,64 位是 82 M。 如果 JVM 加载的信息超过该值,就会抛出 OutOfMemoryError:PermGen Space
2. JDK 1.8 设置方法区
- -XX:MetaspaceSize,元空间初始大小,也称为高水平线。当方法区内容触及该值时,会触发 Full GC。
- -XX:MaxMetaspaceSize,元空间最大内存大小,默认值为 -1 (没有限制, 或者说受限于系统可用内存)。如果方法区加载信息超过该值就会抛出:OutOfMemoryError:Metaspace
3. 示例:借助 CGLib 使方法区溢出
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OomObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method,
Object[] objects,
MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
- 添加依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>
五、方法区内部结构
1. 方法区结构概览
《深入理解 Java 虚拟机》书中对方法区存储内容描述如下:
它用于存储已被虚拟机加载的类型信息、常量、静态变量、及时编译器编译后的代码缓存等。
2.方法区存储的内容
- 类型信息
对每个加载的类型(类、接口、枚举、注解等), JVM 必须在方法区中存储以下类型信息:- 完整有效的名称(包名.类名)
- 直接父类的完整有效名称(对于 interface 或 java.lang.Object,都没有父类)
- 修饰符(public,abstract, final 等)
- 直接接口的一个有序列表
- 域(Filed)信息
JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序, 域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient 的某个子集) - 方法(Method)信息
JVM 必须保存所有方法的以下信息,同域信息一样包括声明顺序(可以按方法定义顺序记忆)- 方法修饰符
- 方法返回类型
- 方法名称
- 方法参数的数量和类型(按顺序)
- 异常表 (异常处理开始位置、结束位置、代码处理在程序计数器中偏移地址、被捕获的异常类的常量池索引等)
- 方法的字节码、操作数栈、局部变量表及大小
3. 运行时常量池
3.1 字节码文件常量池
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外。还包含一项信息那就是常量池(constant pool table),包括各种字面量和符号引用。 这部分内容将在类加载后存放到方法区运行时常量池中。官方说明
3.2 运行时常量池
- 运行时常量池方法区的一部分。在加载类和接口到虚拟机后,就会创建相应的运行时常量池
- JVM 为每个类型都维护一个常量池,池中的数据项和数组一样,都是通过索引来访问
- 运行时常量池中包含许多不同类型的常量, 包括:
- 编译期就已经明确的数值字符常量,
- 运行期解析后才能获得的方法和字段引用,此时不再是符号引用,已经更换成真实的地址。运行时常量池与 Class 文件常量池的另一个重要特征是具备了动态性
- 当创建运行时常量表时,如果构造所需内存空间超过了方法区能提供的最大值,则 JVM 会抛出 OOM
运行时常量池中存放的内容
- 字面量:比较接近 Java 语言层次的常量概念,如文本字符串、被声明为 final 的常量值
- 符号引用:属于编译原理方面的概念,包括以下三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
六、方法区垃圾回收
1. 方法区的垃圾回收
- 《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区实现垃圾回收。 事实上也确实存在未实现或未能完整实现方法区类型卸载的虚拟机
- 在方法区进行垃圾回收效果比较难以让人满意,尤其是类型的卸载条件相当苛刻。但对该区域进行垃圾回收又是必要的。以前 Sun 公司的 Bug 列表中出现过若干严重 Bug 就是由于低版本 HotSpot 虚拟机对此区域不能完全回收导致的内存泄漏
2. 方法区垃圾回收的两个主要部分
2.1 常量池废弃常量
- HotSpot 虚拟机的常量回收策略:只要常量池中常量没有任何地方引用,就可以被回收
- 回收废弃常量与回收堆内对象类似
2.2 不再使用的类型信息
判断类型不再使用的条件
- 实例:该类的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例
- 类加载器:加载该类的类加载器已经被回收,这个条件除非是精心设计过的可替换类加载器的场景:如OSGi,JSP 等的重加载等。否则很难达成。
- Class 对象:该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
不再使用类型的回收
- Java 虚拟机被允许满足上述三个条件的无用类进行回收,但和对象回收不同,并非没有引用就必然回收。
- HotSpot 虚拟机提供了 –Xnoclassgc 参数进行控制,还可以用 –verbose:class 以及 –XX:TraceClass-Loading、-XX:TraceClassUnloading 查看类加载和卸载信息
- 在大量使用反射、动态代理、CGLib等字节码框架,动态生成 JSP 或 OSGi 这类频繁自定义类加载器的场景。通常需要 JVM 具备类型卸载的能力,以保证不会对系统内存形成过大压力。
七、方法区、栈、堆交互
一个简单的新建对象,也会涉及到栈、堆和方法区:
- 首先会从方法区获取类型信息
- 在堆中创建对象实例
- 在栈的当前栈帧中创建引用变量,该引用指向堆中新建的实例
八、总结
一张运行时数据区全景图: