【JVM】类加载机制

类加载机制作为Java虚拟机最基础的部分,在学习Java语言是不容忽略的一部分,懂得类加载机制才是深入理解JVM的敲门砖,加油ヽ(.◕ฺˇд ˇ◕ฺ;)ノ


什么是类加载机制

类加载机制是指将类的.class文件的二进制数据读取到运行时的数据区的方法区内,然后在堆区内创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,类的加载的最终产品就是位于堆中的Class对象,对象向用户提供了访问方法区内的数据结构的接口。


类加载机制的过程

2d57R.png

类的加载过程分为了五个过程:加载->验证->准备->解析->初始化,其中(验证准备和解析)三个阶段也被成为连接阶段,这几个阶段是按顺序开始,而并不是按顺序完成,往往这些阶段会交叉的混合运行,一般在某个阶段运行过程中去调用激活另一个阶段,这里需要注意的是这五个阶段中有四个阶段的发生顺序是确定的(加载->验证->准备->初始化),但是解析并非一定在顺序中,它有时会在初始化过程之后,这样的目的是为了支持Java语言的动态绑定。接下来我们分别来看一个类加载过程中的每个阶段的内容:

加载(查找并获取字节流

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

这几个步骤是JVM需要在这个阶段完成的三件事情,相比于其他几个阶段来说,加载阶段(准确的说应该是加载获取类的二进制字节流的动作)是可控性最强的阶段,因为在这个阶段,开发人员可以利用系统提供的类加载器来加载,也可以使用自己定义的类加载器进行加载。加载阶段完成后JVM外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,并且创建了一个对应的java.lang.Class对象,用来访问这些数据。加载的具体方式和类加载器在后面的模块中解释。

验证(确保类加载的正确性

  1. 文件格式验证:验证字节流是否符合文件的规范;
  2. 元数据验证:对字节码的描述的信息进行语义分析,以保证其描述的信息符合语言规范的要求;
  3. 字节码验证:通过数据流和控制流分析,保证程序的语义合法逻辑;
  4. 符号引用验证:确保解析动作能够正确执行。

验证是连接阶段的第一步,这一步是为了确认Class文件字节流符合当前JVM的需求,并且不会危害到JVM本身的安全,所以进行了如上四个方面的验证动作,值得注意的是,验证并不是必需的阶段,它不会影响到程序的运行,所以如果类进行了反复的验证,那么完全可以通过参数Xverifynone来关闭大部分的验证措施,从而缩短虚拟机加载类的时长。

准备(为静态变量分配内存和初始化

准备阶段是为类的静态变量分配内存,并将其初始化为默认值,但是要注意:

  1. 这里分配内存仅是为类的静态变量,并不包括类中的实例变量,这个将在以后的对象实例化中分配到堆中
  2. 这里所说的初始化为默认值是指将对应的变量类型赋予对应的0值,如0、null、false等,而不是初始化为用户设置的变量值,将变量赋值为用户定义数值是在初始化阶段才做的工作;
  3. 全局变量如果没有显示的对其赋值,那么在使用时将会用到准备阶段为其赋予的0值,当然局部变量必须显示的赋值,否则编译是不通过的;
  4. 对于引用类型来说,如数组的引用或对象的引用,如果都没有对其进行显示的赋值而使用,那么都将用到系统为其赋予的0值null;
  5. 同时被static与final修饰的值将在此阶段就被赋予显示的初始值,也就是在编译期间就已经将其结果放到了调用它的类的常量池中了。

解析(符号引用转为直接引用

解析阶段是虚拟机将常量池中的符号引用(主要是类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7种)转化为直接引用的过程。其中直接引用就是一个直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

初始化(为类的静态变量赋予真正的初始值

JVM对静态变量初始化的两种方式:

  1. 声明类变量是指定初始值;
  2. 使用静态代码块为类变量指定初始值。

JVM初始化的步骤为:

  1. 若初始化的这个类还没有进行加载和连接,那么先将类进行加载和连接;
  2. 若初始化的这个类的父类还没有进行初始化,则先去初始化父类;
  3. 若初始化的这个类中有初始化语句,则系统依次执行这些初始化语句。

类初始化的时机:(对类进行主动调用的时候)

  1. 创建类的实例,及new一个对象;
  2. 访问某个类或接口的静态变量或对静态变量进行赋值时;
  3. 调用某个类的静态方法时;
  4. 某个类的子类被初始化时;
  5. 使用反射方式强制创建某个类或接口对应的java.lang.Class对象;
  6. JVM启动时被标明为启动类的类;

初始化总结:

初始化是类加载机制的最后一个阶段,本阶段才真正意义上执行了class内部代码,但是其实这也只是执行类构造器()方法,所以说,执行代码也只是一个开端。


类加载器

定义及一些分类与关系

提到类加载机制就不得不提到类加载器,,它是用在类加载机制的第一个阶段-加载,一个类在JVM中的唯一性依赖于类与其类加载器,即使两个类来源于同一个class文件,但是由于类加载器的不同也让两个类在JVM中是两个不同的存在。并且在加载阶段类加载器获取二进制字节流并非只在Class文件中,还可以从jar包中以及网络和其他文件生成。其种类可分为如下:

以虚拟机的角度

  1. 启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
  2. 所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

以开发人员的角度:

  1. 启动类加载器:Bootstrap-ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap-ClassLoader加载),并且启动类加载器是无法被Java程序直接引用的。
  2. 扩展类加载器:Extension-ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器:Application-ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

一般应用程序的加载都是由这三种类加载器互相协作完成的,当然也可以加入自定义的ClassLoader,因为JVM自带的ClassLoader只是在本地文件系统加载标准的java class文件。其实在使用Applet时就用到了自定义的ClassLoader,因为它需要加载网络上的Java class文件,并且可以做到:

  1. 在执行非置信代码之前,自动验证数字签名。
  2. 动态地创建符合用户特定需要的定制化构建类。
  3. 从特定的场所取得java class,例如数据库中和网络中。

这样可以检验相关的安全信息,应用服务器也大都使用了自定义ClassLoader技术。

几种类加载器的层级关系

2smPO.png
这种层级关系称为双亲委派模型,每一层类加载器的上一层为其父加载器,但是,它们的这种关系不是通过继承实现的,而是通过组合关系来复用父加载器中的代码的。这就不得不提到JVM类加载的3种机制了:

  1. 父类委托:当收到类加载请求时,先让父类进行加载,也就是一直到顶层的启动类加载器向下,只有当父类无法加载时,子类类加载器才会尝试从自己的类路径中加载;
  2. 全盘负责:当一个类加载器负责加载某个类时,该类的所依赖和引用的其他类也将由该类加载器全权负责,除非显示的使用另一个类加载器;
  3. 缓存机制:该机制会保证所有加载过的类都会被缓存,当需要加载某个类时,类加载器会先从缓存区中寻找是否有该类,若无,系统才会加载该类并放入缓存区。这也解释了为什么修改了类之后需要重新启动JVM新的类才会生效。

双亲委派机制:

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

这里给出双亲委派机制的源码以便于更好的理解双亲委派机制的作用原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}

protected synchronized 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 {//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {// 如果无父类加载器并且启动类加载器都不能加载,则调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

这里给出几个ClassLoader加载的Demo:

1
2
3
4
5
6
7
8
9
10
11
12
package First;

public class Demo {

public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
loader.loadClass("First.Text"); //loader.loadClass()的方式加载类 默认不执行初始化块
Class.forName("First.Text"); //Class.forNmae()的方式加载类 默认执行初始化块
Class.forName("First.Text", false, loader); //Class.forNmae()指定类加载器的方式 默认不执行初始化块(false)
}
}
1
2
3
4
5
6
7
8
package First;

public class Text {

static {
System.out.println("执行静态代码块");
}
}

运行结果:

1
2
jdk.internal.loader.ClassLoaders$AppClassLoader@3a71f4dd
执行静态代码块

这里记录一个坑

在普通的使用loader.loadClass(ClassNmae)或Class.forName(ClassName)或指定加载器的加载类时若ClassName只填写类名称将抛出ClassNotFoundException异常。
解决方法:同一个包中的类虽然可以直接引用但是由于类名前的包名已由编译器加载上去了,所以在通过这几个方法加载类时,如果不加包名则会默认在default中寻找,自然也就找不到了,所以在ClassName前应该加上包名,问题就解决了。

三种类加载方式的不同
1 . ClassLoader.loadClass(ClassName)方式默认只是将其加载到JVM中而不会去加载类的静态代码块,只有等到new Instance时才会去执行;
2 . Class.forName(ClassName)方法是将类的.class文件加载到JVM中并对类进行解释,执行类的静态代码块;
3 . Class.forNmae(ClassName, Initialize, Loader)这种类加载方式是使用指定的类加载器进行加载类,并且可以指定是否执行类的静态代码块。


结束语

本篇对于类加载机制通过累加载机制的过程将5个阶段的任务以及相关的机制原理以及所用到的一些外部实体做了解释,我们可以看到,在整个类加载机制的过程中,只有类加载阶段可以由用户控制,即使用指定的类加载器进行加载,其余的阶段都是由JVM所控制的,并且类加载机制的结束仅仅是代码运行的一个开端,所以说,类加载机制也仅仅是将类文件加载到JVM的内存中,只有在类加载结束后,才真正的开始执行字节码的操作。

参考:
《深入理解JAVA虚拟机》
https://www.cnblogs.com/ityouknow/p/5603287.html

如果觉得还不错的话,把它分享给朋友们吧(ง •̀_•́)ง