HotSpot JVM 「05」Runtime Data Areas - stack & other areas

117 阅读5分钟

在前面的文章中《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 显示的常量池内容。

Untitled.png

注:这也意味着方法区中存在多个类的运行时常量池。

运行时常量池中的条目(Entry)分为两类:

  1. symbolic references,后续需要继续解析。
  2. static constants,后续无需再处理。

01.3-方法区的内存回收

在 Java 7之前,永久代也是通过 GC 来回收内存。Java 8之后,元空间使用的是直接内存,无法使用 GC 进行内存回收。从前面的描述中我们知道,方法区中存储的主要是常量池和类型信息,内存回收多发生在常量池回收或类型卸载时。

HotSpot 中对常量池的回收策略非常明确,只要常量池中的常量没有被任何地方引用,则可以被回收。而类型卸载发生在某个类不再被使用时。某个类型被卸载需要同时满足如下三个条件:

  1. 该类在堆中所有的实例均已被回收。
  2. 该类的类加载器已被回收。
  3. 该类的类对象(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 内存模型是比较容易混淆的两个概念,通过最近几篇文章的介绍,相信大家对这两个概念也有了基本的了解与区分。如果文章中有描写的不清楚或者不正确的地方,希望读者朋友可以指出来,大家一起交流学习下。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿


历史文章推荐