1. 加载之前
自己编写一个My.java文件
在运行该文件时,首先通过javac My.java将该文件编译成My.calss(二进制文件,默认会放在当前目录下)。
通过类加载器将My.class文件加载到JVM中的运行时数据区,再通过执行引擎去执行将加载到运行时数据区的字节码文件。
2. 类加载过程
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
下面就一个一个去分析一下这几个过程。
1、加载
”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:
(1)通过一个类的全限定名来获取其定义的二进制字节流
(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。我们在最后一部分会详细介绍这个类加载器。在这里我们只需要知道类加载器的作用就是上面虚拟机需要完成的三件事,仅此而已就好了。
2、验证
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:
(1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。
(2)元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
(3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。
(4)符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。
3、准备
准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:
(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如
public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。
当然还有其他的默认值。
[图片上传失败...(image-b5768d-1589109242703)]
注意,在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。
4、解析
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
5、初始化
这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
创建类的实例,也就是new的方式访问某个类或接口的静态变量,或者对该静态变量赋值调用类的静态方法反射(如 Class.forName(“com.shengsiyuan.Test”))初始化某个类的子类,则其父类也会被初始化Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类好了,到目前为止就是类加载机制的整个过程,但是还有一个重要的概念,那就是类加载器。在加载阶段其实我们提到过类加载器,说是在后面详细说,在这就好好地介绍一下类加载器。
相关限制
3.1 下层的加载器能够看到上层加载器中的类,反之则不行
3.2 类加载器可以加载一个类,但是不能卸载一个类。但是类加载器可以被删除或者被创建
3. 类加载
- 加载器加载原理源码
首先判断当前类加载器是否有父类,如果有就调用父类加载器进行加载,否则就调用顶层类加载器Bootstarp类加载器进行加载,当Bootstrap类加载器进行加载失败时,就尝试调用子类进行加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); // 委托父类加载
} else {
c = findBootstrapClassOrNull(name); // 委托Bootstrap加载
}
} 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.
long t1 = System.nanoTime();
c = findClass(name); // 当前类加载器加载
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- 原理简述
当类加载器加载Myclass.class时,会自底向上检查该Myclass.class是否已经被加载过,假如在Extension class loader处发现,该Myclass.class已经被加载过,则Myclass.class将不会被再次加载,而是会使用已经加载过的Myclass.class。加入检查到Bootstrap class loader 处也没有发现该Myclass.class被加载过,此时则会自顶向下进行尝试加载,首先Bootstrap class loader尝试在java_home/lib目录下找Myclass.class是否存在,存在就会被加载并完成Myclass.class的加载行为,否则继续向下尝试加载,当找到Custom class loader都没有找到Myclass.class时,则会抛出ClassNoFoundException异常。
4. 双亲委派模型是什么?
- 如果一个类加载器收到一个加载某个类的请求,先去查找该类是否被已经加载过,如果被加载过就直接返回,否则该类加载器不会去加载该类,而是把该请求层层委托给父类加载器,每一层都是如此,直至递归到最顶层的类加载器(启动类加载器),只有当父类加载器在其搜索范围内无法找到所需的类时,子类加载器才会尝试去加载类。
5. JDBC为什么要破坏双亲委派模型?
- 因为类加载器受到加载范围受限,在某些情况下父类加载器无法加载到需要的文件,这时候就需要委托子类加载器去加载class文件。
JDBC的Driver接口定义在JDK中,其实现是由各个数据库的服务商来提供,比如MySql驱动包。DriverManager类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于$JAVA_HOME中的jre/lib/rt.jar包,由于BootStrap类加载器来进行加载,而其Driver接口的实现类是位于服务商提供的jar包中的,根据类加载器限制,当被装载的类引用另外一个类的时候,虚拟机就会使用装载第一个类的类加载器来加载被引用的类。也就是说BootStrap类加载器还要去加载jar包中的Driver接口的实现类。我们知道BootStrap类加载器默认只负责加载$JAVA_HOME中jre/lib/tr.jar中的class文件,所以Driver接口的实现类根本不在这个路径下,因此需要有子类加载器去加载Driver接口的具体实现类。这样就破坏了双亲委派模型。
那么父类加载器怎么通过子类加载器去加载类文件呢,线程上下文类加载器可以通过Thread.setContThread.setContextClassLoaser()方法设置,如果不设置默认会从父类继承,一般默认使用的是应用程序类加载器。通过
Thread.currentThread().getContextClassLoader()来获取子类类加载器