JVM系列文章第二章-类文件到虚拟机

image.png

xiatian

第二章:类文件到虚拟机(类加载机制)

在了解了 class 文件的二进制格式后,那么接下来需要思考的就是,JVM 到底是如何运行 class 文件中我们所写的程序逻辑代码的呢?

JVM 在开始真正运行程序逻辑之前,第一步需要做的就是将 class 文件加载到 JVM 内存中,所以我们首先需要了解 Java 的类加载机制是如何的?

类加载机制(先看官网介绍)

docs.oracle.com/javase/spec…

Chapter 5. Loading, Linking, and Initializing

The Java Virtual Machine dynamically loads, links and initializes classes and interfaces. Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation. Linking is the process of taking a class or interface and combining it into the run-time state of the Java Virtual Machine so that it can be executed. Initialization of a class or interface consists of executing the class or interface initialization method (§2.9.2).

从官方文档中得知,Java 类加载分为三步进行:Loading(加载)、Linking(链接)、Initizlizing(初始化),先不做深入理解,先看下面内容,在深入了解类加载机制之前,我们要先对 JVM 的运行时数据区有一个了解,看下面内容。

初步认识 Runtime Data Areas(运行时数据区)

照常惯例,先看官网是如何介绍的:

docs.oracle.com/javase/spec…

2.5. Run-Time Data Areas

The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

结合官网介绍和参考以下官方图片,JVM 运行时数据区(Runtime Data Areas),可以划分为以下 5个区域,先做大致了解:

  • Method Area:方法区 -->存储已被虚拟机加载的类信息、常量、静态变量
  • Heap:堆内存 -->存放对象实例
  • Java Virtual Machine Stacks:Java 虚拟机栈 -->用于存储局部变量表
  • Program Counter Registers:程序计数器 -->当前线程所执行的字节码的行号指示器
  • Native Method Statcks:本地方法栈 -->为虚拟机使用到的 Native 方法

Slide1

Method Area:方法区(线程共享内存)

元空间:Metaspce >=JDK1.8

永久代:Permanent >=JDK1.7

用于存储已被虚拟机加载的类信息(模板信息)、常量、静态变量、即编译器编译后的代码等数据

  • 例如:(形象的理解一下)
    • 类描述信息:public class HelloWorld
    • 成员变量信息:private Integer a
    • 方法描述信息:public void test()

每个 Java 进程只有一个,被所有 Java 线程所共享,这里面的数据就是线程非安全

方法区不够存储了,就会抛出 OOM

运行时常量池(Run-Time Constant Pool) --小插曲

2.5.5. Run-Time Constant Pool

A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file (§4.4).

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

Heap:堆内存(线程共享内存)

主要存储类的实例信息

  • 例如:(形象的理解一下)
    • new User()
    • Class clazz = class.forName("")

Java 堆是被所有线程共享的一块内存区域

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OOM

 Runtime Data Areas

结合以上两块内存区:

  • 1)方法区用于存储类信息、常量、静态变量等
  • 2)堆内存用于存储类的实例信息

那么一个 class 文件里面的大部分字节码信息,通过类加载机制都可以被加载到内存中去了,现在需要思考另外一个问题,就是 JVM 是如何运行起来的呢?如何运行代码中的方法的呢?(接下来看 Java 虚拟机栈)

Java Virtual Machine Stacks:Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

来看一段伪代码:

public static void main(String args[]) {
  a();
}
static void a() {
  b();
}
static void b() {
  c();
}
static void c(){
  System.out.println("hello");
}
复制代码

Java 虚拟机栈

补充知识:

栈是一种先进后出的数据结构

上图说明了这段代码在执行过程中,Java 虚拟机栈的压栈和出栈的整个过程:

  • 【压栈】先将入口方法 main()方法栈帧压入
  • 【压栈】main 方法调研 a方法:再将 a()方法栈帧压入
  • 【压栈】依次进行......
  • 【出栈】当 c()方法执行完成返回后:将 c()方法栈帧进行出栈
  • 【出栈】依次进行......
  • 【出栈】直至 main()方法执行完成

栈帧

官网地址:docs.oracle.com/javase/spec…

栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。

每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。

  • 局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中
    • 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使 用。
  • 操作数栈:以压栈和出栈的方式存储操作数的
  • 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
  • 方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。

栈帧

Program Counter Registers:程序计数器

我们都知道一个 JVM 进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据 CPU 调度来的。

假如线程 A正在执行到某个地方,突然失去了 CPU 的执行权,切换到线程 B了,然后当线程 A再获得 CPU 执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。

如果线程正在执行 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;

如果正在执行的是 Native 方法,则这个计数器为空。

JVM 程序计数器

Native Method Statcks:本地方法栈

如果当前线程执行的方法是 Native 类型的,这些方法就会在本地方法栈中执行。

那如果在 Java 方法执行的时候调用 native 的方法呢?

先来看一个本地方法:(native 的本地方法,并不是用 Java 语言实现的)

Object:
public native int hashCode();
复制代码

本地方法栈

再来看类加载机制

所谓类加载机制就是:

  • 虚拟机把 class 文件加载到内存
  • 并对数据进行校验,转换解析和初始化
  • 形成可以虚拟机直接使用的 Java 类型,即 java.lang.Class

类加载的全过程:

应该知道的知识:

类加载过程是个及其复杂的过程,并非所有类都是按照如下的顺序严格的执行的,N 多的类是并行处理的

类加载过程

装载(Load)

装载就是,查找和导入 class 文件的过程

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口

装载(Load)

链接(Link)

链接的过程,又分为以下 3个小步进行,分别是:验证-->准备-->解析

验证(verify)

保证被加载类的正确性

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备(Prepare)

为类的静态变量分配内存,并初始化其为默认值

链接(Link)

解析(Resolve)

将常量池中的符号引用解析成直接引用

补充知识:

符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标就行,

符号引用与虚拟机的内存布局没有关系,引用的目标不一定需要已经加载到内存中。种种虚拟机的内存布局可以都不相同。但是他们能接受的符号引用必须是一致的。

符号引用的字面量形式明确定义在 JAVA 虚拟机规范的 class 文件中。

直接引用(Direct Reference):直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定在内存中存在。

解析(Resolve)

初始化(Initialize)

为类的静态变量,静态代码块执行初始化操作

初始化(Initialize)

类加载器 ClassLoader

在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载 class 文件的。

先看下 JVM 提供了哪些类加载器

Bootstrap ClassLoader

  • 负责加载$JAVA_HOME 中 jre/lib/rt.jar 里所有的 class 或 Xbootclassoath 选项指定的 jar 包。由 C++实现,不是 ClassLoader 子类。

Extension ClassLoader

  • 负责加载 java 平台中扩展功能的一些 jar 包,包括$JAVA_HOME 中 jre/lib/*.jar 或 -Djava.ext.dirs 指定目录下的 jar 包。

App ClassLoader

  • 负责加载 classpath 中指定的 jar 包及 Djava.class.path 所指定目录下的类和 jar 包。

Custom ClassLoader

  • 通过 java.lang.ClassLoader 的子类自定义加载 class,属于应用程序根据 自身需要自定义的 ClassLoader,如 tomcat、jboss 都会根据 j2ee 规范自行实现 ClassLoader。

类加载器(ClassLoader)

加载原则(双亲委派)

先看下 JDK 中CLassLoader 类的源码:

public Class<?> loadClass(String name) throws ClassNotFoundException {
  return loadClass(name, false);
}
//              -----??-----
protected Class<?> loadClass(String name, boolean resolve)
  throws ClassNotFoundException
{
  // 首先,检查是否已经被类加载器加载过
  Class<?> c = findLoadedClass(name);
  if (c == null) {
    try {
      // 存在父加载器,递归的交由父加载器
      if (parent != null) {
        c = parent.loadClass(name, false);
      } else {
        // 直到最上面的 Bootstrap 类加载器
        c = findBootstrapClassOrNull(name);
      }
    } catch (ClassNotFoundException e) {
      // ClassNotFoundException thrown if class not found
      // from the non-null parent class loader
    }

    if (c == null) {
      // If still not found, then invoke findClass in order
      // to find the class.
      c = findClass(name);
    }
  }
  return c;
}
复制代码

用图来理解下双亲委派机制:

双亲委派机制

从上图中我们就更容易理解了,当一个 HelloWorld.class 这样的文件要被加载时:

  • 首先会在 AppClassLoader 中检查是否加载过(Class<?> c = findLoadedClass(name);),如果有那就无需再加载了
  • 如果没有,那么会拿到父加载器,然后调用父加载器的 loadClass 方法,父类中同理也会先检查自己是否已经加载过,如果没有再往上
  • 注意这个类似递归的过程,直到到达 Bootstrap ClassLoader 之前,都是在检查是否加载过,并不会选择自己去加载
  • 直到 BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出 ClassNotFoundException

那么有人就有下面这种疑问了?

为什么要设计这种机制

这种设计有个好处是,如果有人想替换系统级别的类,比如:java.lang.String。篡改它的实现,在这种机制下这些系统的类已经被 Bootstrap ClassLoader 加载过了,所以其他类加载器并没有机会再去加载,保证了一个唯一的类(全限定名一样)JVM 只会加载一次。

破坏双亲委派

Tomcat 的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String 等),各个 web 应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给 commonClassLoader 走双亲委托。

有兴趣的同学,可以自行学习下,本章节不再详述,后续可以正对性的进行专题研究。

小结

从第一章《初始 JVM》的分析,我们了解了一个 Java 源文件需要首先编译成 class 文件后,JVM 虚拟机才能运行这段代码。从本章节的分析,我们了解了一个 class 文件是如何被加载到 JVM 的内存空间的,并且了解了 Runtime Data Areas 的各个内存分配,接下来我们将深入的对 JVM 内存模型进行分析,并深入分析 JVM 的垃圾回收机制。

敬请期待,JVM 系列文章第三章,《JVM 内存模型和垃圾回收机制》。

推荐阅读

初识 JVM(带你从不同的视角认识 JVM)

Elasticsearch系列之二选主7.x之后

Dapr 实战(一)

Dapr 实战(二)

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 [email protected]

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

image.png

Supongo que te gusta

Origin juejin.im/post/7075116826551648292
Recomendado
Clasificación