Java类加载那些事

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中这些都用的出神入化了。

你可能感兴趣的:(Java,java,开发语言)