概念
在了解java类的加载机制之前,我们需要先了解一下几个基本概念
字节码
我们知道,计算机只认识0和1的机器码,所以不管任何语言最终都需要被编译成计算机所能理解的机器码,才可以被执行。
而java类(.java)被编译器(javac.exe)编译之后会产生字节码文件(.class),然后才能被JVM所加载和执行
那么字节码和机器码之间又有什么关系呢?为什么java类被编译成class文件 就可以被执行了呢?
字节码是一种中间状态的(中间码)的二进制代码(文件)
字节码(Bytecode)是一种包含执行程序、由一序列 op 代码/数据对 组成的二进制文件。需要直译器转译后才能成为机器码;
**
也就是说,字节码是一种二进制代码,不可以直接被计算机所执行,必须经过直译器去转译成机器码才可以。
如下图所示,我们可以认为JVM就是一种特殊的直译器,可以将class文件从字节码(二进制代码)转换成机器码并被机器执行**。
我们看一下字节码文件是怎样的,有类ClassTest如下
package com.java;
/**
* @description
* @date: 2021-01-19 15:22
*/
public class ClassTest {
final static int MAX_VALUE = 100;
/**
* 类变量
*/
static int i;
/**
* 实例变量
*/
int k;
public static void main(String[] args) {
System.out.println(i);
System.out.println("亿点点小测试");
}
/**
* 普通方法
*/
public void commonMethod(){
System.out.println("普通方法");
}
}
编译后生成的ClassTest.class文件
使用javap命令查看class文件结构:
D:\IDEAWorkSpace\JAVALearn\CodeSource\target\classes\com\java>javap -c ClassTest
警告: 二进制文件ClassTest包含com.java.ClassTest
Compiled from "ClassTest.java"
public class com.java.ClassTest {
static final int MAX_VALUE;
static int i;
int k;
public com.java.ClassTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #3 // Field i:I
6: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12: ldc #5 // String 亿点点小测试
14: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: return
public void commonMethod();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String 普通方法
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
使用SublimeText打开class文件,以16进制形式查看,内容如下:
cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
........篇幅问题,省略部分
其中cafe babe表示的是魔数,魔数就是用来区分文件类型的一种标志,也就是说,cafe babe是java文件的类型标识。
对象和类
类:是抽象的概念集合,表示的是一个共性的产物,类之中定义的是属性和行为(方法);
如上述定义的ClassTest则是一个类
对象:对象是一种个性的表示,表示一个独立的个体,每个对象拥有自己独立的属性,依靠属性来区分不同对象。
当调用new ClassTest()会构造一个对象。
类是对象的模板,对象是类的实例。类只有通过对象才可以使用,而在开发之中应该先产生类,之后再产生对象。类不能直接使用,对象是可以直接使用的
类加载器(ClassLoader)
类加载器负责加载所有的类到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,每个这样的实例用来代表一个java类,通过该实例的newInstance()方法可以创建出该java类的一个对象
一旦一个类被加载到JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识
在java中,类加载器分为四种**
启动类加载器(BootstrapClassLoader):顶级类加载器,用C++语言编写,在Java虚拟机启动后初始化,它主要负责加载
%JAVA_HOME%/jre/lib,-Xbootclasspath
参数指定的路径以及%JAVA_HOME%/jre/classes
中的类。扩展类加载器(ExtClassLoader):由BootstrapClassLoader加载,ExtClassLoader将BootstrapClassLoader设置为父类加载器,ExtClassLoader负责加载
${JAVA_HOME}/jre/lib/ext
目录下的jar包应用程序类加载器(AppClassLoader):负责装载classpath路径下的类文件
**
- 自定义类加载器:如果符合双亲委派模型,这个类加载器加载用户自定义classpath下的jar包, 例如
tomcat
的WEB-INF/class和WEB-INF/lib.
**
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
- 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式
- 从JAR包加载class文件,这种方式也是很常见的
- 通过网络加载class文件。
- 把一个Java源文件动态编译,并执行加载。
类的生命周期
类的生命周期从类被加载到虚拟机内存中开始,到卸载内存为止
共经历了七个阶段,分别是:加载—验证—准备—解析—初始化—使用—卸载
整个顺序并不是完全固定的,其中解析阶段可以在初始化之后再开始,这样便可以实现Java的运行时绑定(动态绑定)机制
类加载过程
类的加载过程只涉及类的生命周期钱五个步骤,也就是:
加载—>验证—>准备—>解析—>初始化
**
加载(Loading)
ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个Class对象实例**。**
步骤如下:
1、通过一个类的完整限定名来获取定义此类的二进制字节流(字节流的来源非常灵活)
2、将这个字节流所代表的静态储存结构转换成为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
注意:类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类
验证(Verification)
目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全**,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
文件格式的验证:验证字节流是否符合Class文件格式的规范(比如说是否以 cafe bebe 开头),并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
元数据验证:对类的元数据信息进行语义校验(对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
准备(Preparation)
为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值**
这里不包含final修饰的static变量 ,因为final在编译的时候就已经分配了。这里也不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。
**
如static int i = 5
;
此时i会被赋值为0而不是5,初始化为5是在初始化阶段执行的,即便没有=5,一样会赋值为0
初始值由数据类型决定,如果是Integer则赋值为null
解析(Resolution)
这里主要的任务是把常量池中的符号引用替换成直接引用
符号引用:符号引用是以一组符号来描述所引用的目标**,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。 比如 com.java.ClassTest类引用了 com.java.Demo类,编译时 ClassTest类并不知道 Demo类的实际内存地址,因此只能使用符号 com.java.Demo,这就是符号引用的一种。
直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄,通过对符号引用进行解析,找到引用的实际内存地址。该引用是和内存中的布局有关的,并且一定加载进来的。
解析阶段主要包含以下动作:
(1)类或接口的解析
会先判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用
(2)字段解析
会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;
如果没有,则会按照继承关系从下往上递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从下往上递归搜索其父类,直至查找结束
(3)类方法解析
对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口
(4)接口方法解析
与类方法解析步骤类似,接口不会有父类,因此,只递归向上搜索父接口就行了。
JVM常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用总结起来则包括了下面三类常量:
- 类和接口的全限定名(即带有包名的Class名,如:com.java.ClassTest)
- 字段的名称和描述符(private、static等描述符)
- 方法的名称和描述符(private、static等描述符)
初始化(Initialization)
类加载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。
前面已经对static 初始化了默认值,这里我们对它进行赋值【即i=5】,成员变量也将被初始化【k=0】
换句话说,初始化阶段是执行类构造器方法的过程**。
个人理解的整个过程可以使用如下图示,如有错误,还请指出
类加载机制
全盘负责: 当一个类加载器负责加载某个类的时候,该类所依赖和引用的其它的类也由该类加载器负责加载,除非显式地使用另外一个类加载器来载入。
双亲委派:加载一个类时,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
缓存机制: 保证所有加载过的类会被缓存,当程序中需要使用某个类时,类加载器先从缓存中搜寻该类,如果缓存区不存在该类的对象时,系统才会去加载这个类。
双亲委派机制—-保证系统内一个类只会被一个类加载器所加载
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
这样做的好处是,Java类随着它的类加载器一起具备了一种带有优先级的层次关系,这主要是为了防止同名的类出现混乱,也是一种隔离性的体现。
举个例子,比如java.lang.Object这个类,无论哪个类加载器加载时,最终都会委派给Bootstrap加载器去加载,这就保证了整个系统运行过程中的Object都是同一个类。
总结
1、字节码是一种中间状态的(中间码)的二进制代码(文件),字节码(Bytecode)是一种包含执行程序、由一序列 op 代码/数据对 组成的二进制文件。需要直译器转译后才能成为机器码;
2、可以认为JVM就是一种特殊的直译器,可以将class文件从字节码(二进制代码)转换成机器码并被机器执行**。
3、类加载器负责加载所有的类到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例
4、类的生命周期从类被加载到虚拟机内存中开始,到卸载内存为止。共经历了七个阶段,分别是:加载—验证—准备—解析—初始化—使用—卸载
5、类的加载过程只涉及类的生命周期钱五个步骤,也就是:加载—>验证—>准备—>解析—>初始化
6、加载(Loading)阶段:ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个Class对象实例
7、验证(Verification)阶段:目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
8、准备(Preparation)阶段:为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值
9、解析(Resolution)阶段:这里主要的任务是把常量池中的符号引用替换成直接引用
10、初始化(Initialization)阶段:类加载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。
11、类加载机制涉及三种模型:全盘负责、双亲委派、缓存机制
12、双亲委派机制指的是加载一个类时,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。这样做的好处是,Java类随着它的类加载器一起具备了一种带有优先级的层次关系,这主要是为了防止同名的类出现混乱,也是一种隔离性的体现。
思考
什么时候会触发类的加载?
虚拟机严格规定,有且仅有 5 种情况必须对类进行加载
1、遇到 new、getstatic、putstatic、invokestatic
这四条字节码指令时,如果类还没进行加载,则需要先触发其加载。
2、使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类还没进行加载,则需要先触发其加载。
3、当加载一个类的时候,如果发现其父类还没进行加载,则需要先触发其父类的加载。
4、当虚拟机启动时,用户需要指定一个执行的主类,即调用其 #main(String[] args) 方法,虚拟机则会先加载该主类。
5、当使用 JDK7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过加载,则需要先触发其加载。
这 5 种场景中的行为称为对一个类进行主动引用,除此之外,其它所有引用类的方式都不会触发加载,称为被动引用。