Java 虚拟机基础(一) 类的加载机制

发布于 2020-05-08  186 次阅读


Java 虚拟机 (Java Virtual Machine ,JVM)学这东西有啥用? 装13啊!不然呢?面试么?

找打???

一、啥是 JVM

想当年,编程语言被两大家瓜分,一是编译语言,一是解释语言。

编译语言就是 编写之后由编译器编译成系统可以识别的运行文件,这样就可以直接运行,特点就是运行快,但却受系统限制,因为在各个系统上的写法可能都不一样,所以跨平台能力差。典型的就是  c ,c++ ,当然还有现在的 golang。

解释语言是由解释器充当系统的翻译,这样,无论你在哪个系统,只要这个翻译在,就能得到同样的效果。所以解释语言的特点就是 跨平台能力强,但运行速度慢。 典型就是 当年的 python 等。

那 Java 呢? Java是取其二者之中间,称之为字节码语言,它有一个自己的虚拟机,代码编译完成后并不是直接在系统上跑,而是在 虚拟机 上跑。这样,只要系统安装了 虚拟机 就可以运行代码,完美的跨平台,而且不用解释器拖慢速度。

所以 JVM 就是一个小小的系统,一个读 Java字节码的系统? Java 字节码是啥?Java字节码是 由 编译器 通过编译 Java 代码所生成的系统指令。通常是 .class 文件。

其运行过程如下图

Java代码运行过程

二、类的加载过程

了解了 java 代码的大概流程后,那我们还要明白,我们所写的代码是如何到 虚拟机 里的,虚拟机又是如何运行他们的呢?其过程如下

类的加载过程

2.1 加载(Load)

如果想要加,那首先要有工具,也就是类加载器。而且还要知道类在哪,而这就要用到类的全限定名了。

    2.1.1 类加载器 (ClassLoader)

    类加载器,顾名思义,就是用来加载类的工具。但是有趣的是,类加载器不只一个,而是有四个!

类加载器分类

其中 Bootstrap ClassLoader 级别最高,主要用于加载最主要的类,比如常用的 java.lang.String 等。

上图中,由上到下,优先级由高到低,什么意思呢?

在加载类的时候,加载器会先看自己的上级是否已经加载过了,如果已经加载过了,那就不在加载。所以 自己写的  java.lang.String 是无法替换真正的 java.lang.String,因为自己写的类是由 App ClassLoader 加载的,它在加载时会先看其的上级是否已经加载过了,Extension ClassLoader 也会看自己的上级 Bootstrap ClassLoader 是否加载,而 java.lang.String已经在 Bootstrap ClassLoader 中加载过了,所以就不会在进行加载了。

那如何查看 类加载器呢?

如段代码的执行结果是什么呢?

ClassLoader

其执行结果如下:

执行结果

从前两个输出可以看到 AppClassLoader 和 ExtClassLoader但是为啥到 Bootstrap ClassLoader 的时候却变成了 null ?

因为 Bootstrap ClassLoader 是底层,是由 C 语言实现的,自然在 Java 层面是看不到的。

而最底级的 Custom ClassLoader 是干嘛的呢? 其实它是用来给想要自定义 ClassLoader 的项目留的。通过继承 java.lang.ClassLoader 类就来实现。比如 tomcat 就自定义了自己的 ClassLoader。

    2.1.2 类的全限定名

    在写代码的时候,我们代码的第一行往往定义了个文件所在的包。如:

package com.olcoat.demo

打包成 jar 之后我们可以用 解压工具 看到包内的目录结构,而上面的包路径,也正是文件夹的路径,com/olocat/demo 这下面的文件正是 com.olocat.demo 包内的类所编译后的 .class 文件

而每个类加上其包名,就成了它的全限定名 如: com.olocat.demo.MyClass

    2.1.3 类的加载

了解了类全限定名和类加载器后我们回归主题,那什么时候我们才会加载一个类呢?
在JVM中规定了四条必需加载该类的时刻。(注:前提是该类还未被加载)

1)使用new关键字实例化对象

2)读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)

3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)

4)调用一个类的静态方法

如果想要加载某一个类,首先要有加载器,而 JVM 的类加载器正是大名顶顶的 ClassLoader 关于 ClassLoader 的细节就先不在这里介绍了。只需要了解它是用来加载类的工具即可。

加载器有了那我们还要找到要加载的类,而类的全限定名正是它的住址,可让 JVM 可以精准的找到它的所在。

找到 类的所在后,JVM 会将字节码文件由静态数据存储结构转为运行时数据结构。

然后在 Java堆 中生成一个代表这个类的 java.lang.Class 对象,作为访问这些数据的入口。

如果你不知道 Java 堆,和 运行进数据结构,那没有关系,下面会讲,你现在只用了解它现在进入内存了就行。

所以加载的过程如下:

1.通过类的全限定名找到其字节码文件所在。
2.将其加载到运行时数据结构中。

2.2 链接(Link)

如果说加载可以看名字就了解是什么意思,那链接就有点让人摸不到头脑了。链接到底是什么意思呢?

其实,链接并不是一个过程,而是三个过程:验证、准备、解析。

    2.2.1 验证

     验证很好理解,其主要目地就是为了保证被加载类的正确性。其通过四个方法进行验证、文件格式、元数据、字节码、符号引用,这四个办法对文件进行验证。以确保所加载的文件是正确的 .class 文件。

    2.2.2 准备

      该阶段就要是为静态变量分配内存空间,并对未赋值的变量 赋予初始值。如:

private static int i;

      对于局部变量而言,这样的写法是直接编译错误的,对于静态变量而言,这会被赋予初始值,对于 int 类型而言,自然就是 0 了。

      该阶段的重点在于给静态变量分配内存空间,而不是赋予初始值,所以要注意主次。

    2.2.3 解析

    对于解析而言,如果去看 oracle 的官方文档,那我们可以看到一段很有趣的解释。

Resolution is the process of dynamically determining concrete values from symbolic references in the run-time constant pool.Resolution of the symbolic reference of one occurrence of an invokedynamic instruction does not imply that the same symbolic reference is considered resolved for any other invokedynamic instruction.

这段话很难理解,大概可以理解为,把类中的符号引用转换为直接引用。
不像人话是吧。其实对于系统而言,它会在意你的变量名是 name 还是 a1 么? 或是说它会在意你的类名是 PersonController 还是 P1  么?

对于系统而言,无论是什么数据都不过是内存中的一块地址!所以它只在意地址,而不在意变量名到底是啥。那这些就很好解释了。

符号引用的意思就是 用一组字母或符号,它所代表一个引用(指针)。
直接引用其实就是指针本身。它是一组所指目标的地址。

所以解析的过程就是将你的 变量,类,变成其所在地址的值而已。

2.3 初始化(Initialize)

在官方文档中对初始化过程的解释非常简单。

Initialization of a class or interface consists of executing its class or interface initialization method

这句话很简单,初始化阶段就是执行这个类或接口的初始化方法。
一说到初始化方法,那第一个想到的肯定是构造方法啊。然而要注意构造方法的英文是constructor 那这个 initalization method 到底是不是构造方法呢?

其实官方文档在这句话的未尾放了链接,指向文档的  2.9 Special Methods 在这里面,解释了初始化方法到底是个啥。

At the level of the Java Virtual Machine, every constructor written in the Java programming language  appears as an instance initialization method that has the special name <init>. 

对 JVM 来说,每个Java代码的构造方法都会被添加一个 initialization method 实例  这个特殊方法被命名为 init 。

所以实际上,初始化方法就是 构造方法的实例,而初始化过程也就是执行构造方法的过程。

到此类的加载过程就算是结束了。禁止转载,如需转载请通过简信或评论联系作者。1人点赞JVM


欢迎来到欧喵的博客,喜欢就看看吧