Java源文件(.java文件)被编译器编译后变为字节码形式的类文件(.class文件),Java类加载的过程就是JVM加载.class的二进制文件并且放到内存中,将数据放到方法区,并且在堆区构造一个java.lang.class对象,并且完成类的初始化的过程。
1.类的加载机制
为了减少码字,本段直接引用自 Java的类加载机制是什么?
Java的类加载机制主要分为三个过程:加载、连接和初始化。这三个过程的顺序是固定的,但是每个过程中的细节却是不同的。下面我们来详细介绍一下这三个过程。
1.1 加载
Java的类加载器会根据类的全限定名来加载类,当需要使用某个类时,如果该类还未被加载进内存,则需要执行一下步骤进行加载:
1.1.1. 通过类的全限定名找到对应的class文件,这里的class文件可以是.java文件经过编译之后生成的.class文件,也可以是通过其他方式生成的.class文件。
1.1.2 将class文件中的二进制数据读取到内存中,并将其转换为方法区的运行时数据结构。
1.1.3 创建由该类所属的java.lang.Class对象。该对象可以理解为,是对类的各种数据(如名称、访问修饰符、方法、成员变量等)的封装。
在加载类时,类加载器除了加载某个具体的类外,还需要将这个类所依赖的类也加入到内存中。这种依赖性是多层级的,也就是说,被依赖的类又可能会去依赖其他类,所以在加载一个类时,通常需要将其类图中所有的类都加载进来。
1.2 连接
Java虚拟机在加载类之后,需要对类进行连接,连接分为三个步骤:验证、准备和解析。
1.2.1. 验证:在这个步骤中,Java虚拟机主要确保所加载的类的正确性。验证过程主要包括文件格式验证、元数据验证、字节码验证和符号引用验证等。其目的在于确保目标.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机运行时环境安全。
1.2.2. 准备:在准备阶段,Java虚拟机为类的静态变量分配内存,并设置变量的初始值。这里需要注意的是,在这个阶段中分配的内存并不包含那些用户自定义的初始化值,这些值在初始化阶段中进行设置。
1.2.3. 解析:Java在这个阶段中将常量池中的符号引用转为直接引用。通过符号引用,虚拟机得知该类访问其他的类或者类中的字段、方法等,但在类初始化时,需要缓存这些直接引用,以便于直接调用。
1.3 初始化
在类的准备阶段,Java虚拟机已经为静态变量分配了内存并设置了初值,但是这些静态变量”赋初值“的动作并没有完成。初始化阶段,会为静态变量设置用户自定义的初始化值,并执行类构造器<clinit>()方法,以执行初始化操作。
此时,类的准备和初始化阶段已经执行结束,Java的类加载机制总的过程也就结束了
//引用结束
2.静态代码块和静态变量初始化
从上述类的加载机制可以看出,静态代码的初始化分为两步,连接的准备阶段包含初始化,最后又有一个初始化步骤,两者并不重叠,前者是给静态成员变量分配内存并且设置类型的初始值,后者是给静态成员变量设置用户指定的初始值。
这段话有点拗口,代码来说明更清晰。
1、父类 parent.java文件
package com.example.zhangzk.classinit;
public class Parent {
static {
System.out.println("Parent static block 1.");
parentStaticIntVar = 3;
}
static Integer parentStaticIntVar = 2;
static {
System.out.println("Parent static block 2.");
System.out.println("parentStaticIntVar=" + parentStaticIntVar);
parentStaticIntVar = 4;
}
{
System.out.println("Parent not static block 1.");
parentIntVar = 30;
}
Integer parentIntVar = 20;
public Parent(){
System.out.println("Parent construct method .");
System.out.println("parentIntVar=" + parentIntVar);
}
public void f(){
System.out.println("parent f().");
}
{
System.out.println("Parent not static block 2.");
parentIntVar = 40;
}
}
2、子类 Sub.java文件
package com.example.zhangzk.classinit;
public class Sub extends Parent {
static {
System.out.println("Sub static block 1.");
subStaticIntVar = 3;
}
static Integer subStaticIntVar = 2;
static {
System.out.println("Sub static block 2.");
System.out.println("subStaticIntVar=" + subStaticIntVar);
subStaticIntVar = 4;
}
{
System.out.println("Sub not static block 1.");
subIntVar = 30;
}
Integer subIntVar = 20;
public Sub(){
System.out.println("Sub construct method .");
System.out.println("subIntVar=" + subIntVar);
}
public void f(){
System.out.println("Sub f().");
}
{
System.out.println("Sub not static block 2.");
subIntVar = 40;
}
}
3、TestMain.java文件
package com.example.zhangzk.classinit;
public class TestMain {
public static void main(String[] args) {
System.out.println("-----class-----");
System.out.println(">>>subStaticIntVar=" + Sub.subStaticIntVar);
System.out.println(">>>parentStaticIntVar=" + Sub.parentStaticIntVar);
System.out.println("-----instance-----");
Sub s = new Sub();
s.f();
}
}
4、输出结果
-----class-----
Parent static block 1.
Parent static block 2.
parentStaticIntVar=2
Sub static block 1.
Sub static block 2.
subStaticIntVar=2
>>>subStaticIntVar=4
>>>parentStaticIntVar=4
-----instance-----
Parent not static block 1.
Parent not static block 2.
Parent construct method .
parentIntVar=40
Sub not static block 1.
Sub not static block 2.
Sub construct method .
subIntVar=40
Sub f().
解读一下后可以知道静态代码块和静态变量遵循如下规则:
1、静态代码块先于构造方法执行;
2、静态代码块可以给静态成员变量赋值;
3、静态代码块之间按照先后顺序执行;
4、父类的静态代码块先于子类的静态代码块执行;
5、静态代码块先于非静态代码块执行;
6、静态代码块在第一次使用这个类的时候执行,并且只执行一次;
7、静态变量的显式赋值和静态代码块的按照先后顺序执行;
实际上每个Java源文件由编辑器编译后,会自动给类加载器追加一个类初始化方法:<clinit>(),一个类只有一个,包含静态变量的显式赋值代码和静态代码块的代码,在源文件中看起来是一个一个独立的代码块,实际上编译后都放到一个这个类初始化方法中去了。
非静态的代码块和变量的规则和上述类似。
3.类加载的方式
Java的类加载时延迟加载模式,用到的时候再加载,比如new一个类的实例或者调用类的静态变量或者静态方法的时候就会自动完成类加载,在加载子类的之前要优先把子类依赖的父类进行加载。类加载和实例化是两个不同的步骤,除了默认的类加载方式,还有两种主动加载的方式,可以精细化的控制类加载方式。
3.1、Class.forName
Class<?> c = Class.forName("com.example.zhangzk.classinit.Sub");
这种方式加载类,则加载类,并且完成初始化,即静态变量会初始化,静态代码块也会执行。
还是用上面的Parent.java 、Sub.java类来上代码看输出才是王道。
测试类TestMain1.java的代码如下:
package com.example.zhangzk.classinit;
public class TestMain1 {
public static void main(String[] args) {
try {
String className = "com.example.zhangzk.classinit.Sub";
Class<?> c = Class.forName(className);
System.out.println(c.getClassLoader().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出如下:
Parent static block 1.
Parent static block 2.
parentStaticIntVar=2
Sub static block 1.
Sub static block 2.
subStaticIntVar=2
app
3.2、ClassLoader.loadClass
Class<?> c = Thread.currentThread().getContextClassLoader().loadClass("com.example.zhangzk.classinit.Sub");
这种方式来加载类,则只加载类,但不进行初始化,即静态变量不会初始化,静态代码块也不会执行。
继续用上面的Parent.java 、Sub.java类来上代码看输出才是王道。
测试类TestMain2.java的代码如下:
package com.example.zhangzk.classinit;
public class TestMain2 {
public static void main(String[] args) {
try {
String className = "com.example.zhangzk.classinit.Sub";
Class<?> c = Thread.currentThread().getContextClassLoader().loadClass(className);
System.out.println(c.getClassLoader().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出:
app
4.类加载器有哪些
4.1启动类加载器
C++编写的,进入Java世界的大门,负责加载存放在$JAVA_HOME\jre\lib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。
4.2扩展类加载器
Java编写的,父加载器为启动类加载器,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载$JAVA_HOME\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。
4.3应用程序类加载器
Java编写的,父加载器为扩展类加载器,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
4.4自定义类加载器
Java编写的,父加载器为应用程序类加载器,典型代表是Tomcat,为了在一个TOMCAT进程下部署多个JAVA应用程序必须要自定义类加载器,进行应用隔离。
5.双亲委托模型
每个类加载器需要加载类的时候,先请求父类加载器来加载,这样逐层向父类加载器委托,一直会持续委托到启动类加载器,启动类加载器是没有父类加载器的,层层父类加载器在自己的搜索路径找不到要加载的类才由最开始委托的那个类加载器来自行加载。想象一下,假如自己定义一个 java.lang.Object的话会发生什么?当然是加载失败了,层层委托的时候会发现启动类已经加载了该类,这样底层的安全才有保障,要不谁都可以来伪造一把。
类加载器的关系就像一棵竹子,竹子的根是启动类加载器,往上一节是扩展类加载器,再往上一节是应用程序类加载器,应用程序类加载器之上就可以长出许多的竹枝了,竹枝也可以再长出竹节。
双亲委派模型的名字有点蛋痛,除启动类加载器没有父类加载器外,其他的类加载器只有一个父类加载器,没有谁有两个或者更多的父加载器,其实可以改个名字叫“单亲委托模型”更贴切。
类加载器其实就干两件事,一个是找类,一个是加载类,找类是在指定的路径下找,加载就是先默认向父类加载器发起加载,不行就自己自己加载。有时候,需要打破双亲委托模型,其实就是不要默认向父类加载器委托就可以了,自己的地盘自己来做主就好了。
Tomcat 下可以同时部署多个WEB应用,各应用有共享的部分,也有需要隔离的部分,所以就需要定义自己的类加载器来进行精确控制,以Tomcat的类加载器为例,可以细品双亲委托模型和被打破的双亲委托模型。
要想搞清楚Spring Boot的启动流程,必须要要知道上述区别,只有充分利用好上述差异才能精准的控制加载和初始化的过程,Spring中这些都用的出神入化了。