在前面的文章中《HotSpot JVM 「04」Runtime Data Areas - Heap》 我们介绍了 JVM 运行时数据区中最重要的一块区域,堆空间。在接下来的文章中,我们将介绍一下运行时数据区中的其他部分,包括线程间共享的方法区和线程间独占的栈空间和 PC 寄存器等。让我们一块学习下吧。
01-方法区与永久代、元空间
JVMS 中对方法区(Method Area)的定义是:线程间共享的一块内存区域,用于存放每个类结构,例如运行时常量池、域或方法数据、方法和构造器代码等,它类似操作系统中的 text 段。
逻辑上,方法区是堆的一部分,但实现上 JVMS 未作过多的要求。永久代(PermGen)是 HotSpot 中特有的概念,它是 HotSpot 堆 JVMS 中方法区的实现。Java 8 以后,永久代被元空间(Meta Space)取代,永久代也就逐渐退出了历史舞台。
Java 7之前,永久代的大小由虚拟机参数-XX:PermSize和-XX:MaxPermSize控制。Java 8之后,元空间的大小由虚拟机参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize控制。
01.1-方法区中存储的内容
虚拟机加载的类、接口等,其定义信息存放在方法区,具体包括:
- 全量限定名,例如
java.lang.Object; - 直接父类的全量限定名,接口或
Object类没有直接父类; - 访问限定符,例如
public/private/protected; - 类实现的直接父接口列表信息;
类中定义的域信息,具体包括:
- *.class 文件中定义的域(field)列表、顺序等信息;
- 域的相关信息,包括全量限定名、类型、访问修饰符等等;
类中定义的方法信息,具体包括:
- *.class 文件中定义的方法(method)列表;
- 每个方法的访问修饰符、方法签名(方法名、入参、返回值等)信息;
01.2-运行时常量池
每个类或接口的 .class 文件中都有一个 constant_pool 表,运行时常量池就是这个表的运行时表示。下图所示为 Main.class 通过 javap- v 显示的常量池内容。
注:这也意味着方法区中存在多个类的运行时常量池。
运行时常量池中的条目(Entry)分为两类:
- symbolic references,后续需要继续解析。
- static constants,后续无需再处理。
01.3-方法区的内存回收
在 Java 7之前,永久代也是通过 GC 来回收内存。Java 8之后,元空间使用的是直接内存,无法使用 GC 进行内存回收。从前面的描述中我们知道,方法区中存储的主要是常量池和类型信息,内存回收多发生在常量池回收或类型卸载时。
HotSpot 中对常量池的回收策略非常明确,只要常量池中的常量没有被任何地方引用,则可以被回收。而类型卸载发生在某个类不再被使用时。某个类型被卸载需要同时满足如下三个条件:
- 该类在堆中所有的实例均已被回收。
- 该类的类加载器已被回收。
- 该类的类对象(
java.lang.Class对象)没有在任何地方引用,且无法在任何地方通过反射访问该类的方法。
满足上述条件的类可以被回收,但是是否回收还要基于虚拟机的配置情况。虚拟机参数-Xnoclassgc可以控制虚拟机卸载类时的行为。还可以通过虚拟机参数-verbose:class、-X:+TraceClassLoading 、-XX:+TraceClassUnLoading来观察虚拟机加载、卸载类的行为。
02-PC 寄存器、虚拟机栈和本地方法栈
02.1-PC 寄存器
前面几个章节介绍的都是线程间共享的内存空间。在本节中,我们将一起学习下每个线程独占的运行时数据区。
每个线程都拥有一个 PC 寄存器,它存储的是线程将要执行的下一条指令的内存地址。当程序执行本地方法时,该寄存器的值是未定义的。在多线程环境下,CPU 执行不同的线程就需要切换不同的上下文,PC 寄存器用来告诉 CPU 当前程序运行到什么地方。
PC 寄存器是一块非常小的内存空间(原因是它存储的是指令的地址),它的生命周期与线程的生命周期保持一致。程序的顺序执行、分支、循环、跳转或异常处理等都与该区域的内容相关。
02.2-栈
每个虚拟机线程都拥有一个栈空间,它保存着方法的局部变量、部分结果,并参与方法的调用和返回。栈中存储的是一种称为栈帧(frame)的数据结构。栈帧在方法调用是创建,在方法调用完成(正常返回或异常中断)时销毁。
栈帧的内部结构包含如下几部分内容:
- 局部变量表,这部分数据不存在线程安全问题,因为是线程独享的,与 ThreadLocal 有着类似的效果。
- 操作数栈,主要保存临时结果,是指令中操作数的主要来源之一。
- 指向当前方法所在类的运行时常量池引用,指向方法区中与当前方法对应的 method(参考方法区那一节)。
- 方法返回地址,正常返回时返回调用语句的下一条指令;异常返回时,通过异常表确定返回值。
- 其他的附加信息
到此为止,我们已经介绍过 JVM 运行时数据区中的主要内容了。运行时数据区与 Java 内存模型是比较容易混淆的两个概念,通过最近几篇文章的介绍,相信大家对这两个概念也有了基本的了解与区分。如果文章中有描写的不清楚或者不正确的地方,希望读者朋友可以指出来,大家一起交流学习下。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
历史文章推荐