Java源文件(.java文件)被编译器编译后变为字节码形式的类文件(.class文件),Java类加载的过程就是JVM加载.class的二进制文件并且放到内存中,将数据放到方法区,并且在堆区构造一个java.lang.class对象,并且完成类的初始化的过程。
//下述片段引用自 Java的类加载机制是什么?
Java的类加载机制主要分为三个过程:加载、连接和初始化。
1.加载机制
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虚拟机已经为静态变量分配了内存并设置了初值,但是这些静态变量”赋初值“的动作并没有完成。初始化阶段,会为静态变量设置用户自定义的初始化值,并执行类构造器
此时,类的准备和初始化阶段已经执行结束,Java的类加载机制总的过程也就结束了
//引用结束
注意,静态代码的初始化分为两步,连接的准备阶段包含初始化,最后又有一个初始化步骤,两者并不重叠,前者是给静态成员变量分配内存并且设置类型的初始值,后者是给静态成员变量设置用户指定的初始值。这段话有点拗口,代码来说明更清晰。
1、父类 parent.java文件
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文件
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文件
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源文件由编辑器编译后,会自动给类加载器追加一个类初始化方法:
非静态的代码块和变量的规则和上述类似。
类加载的方法>>>
1、Class.forName("")
Class> c = Class.forName("com.example.zhangzk.reflect.TestServiceImpl");
加载类,并且完成初始化。
2、ClassLoader.loadClass("")
Class> c = Thread.currentThread().getContextClassLoader().loadClass("com.example.zhangzk.reflect.TestServiceImpl");
加载类,不初始化。
加载器有哪些>>>
启动类加载器,C++编写的,进入Java世界的大门,负责加载存放在$JAVA_HOME\jre\lib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。
扩展类加载器,Java编写的,父加载器为启动类加载器,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载$JAVA_HOME\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。
应用程序类加载器,Java编写的,父加载器为扩展类加载器,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器,Java编写的,父加载器为应用程序类加载器,典型代表是Tomcat,为了在一个TOMCAT进程下部署多个JAVA应用程序必须要自定义类加载器,进行应用隔离。
双亲委派模型>>>
每个类加载器需要加载类的时候,先请求父类加载器来加载,一直到启动类加载器,启动类加载器是没有父类加载器的,父类加载器找不到给类才由自己来加载。
要想搞清楚Spring Boot的启动流程,必须要要知道上述区别,只有充分利用好上述差异才能精准的控制加载和初始化的过程,Spring中这些都用的出神入化了。