一文了解类加载机制

类的生命周期

- 类的生命周期,有7个阶段.从类被加载到虚拟机内存中,到卸载出内存的整个过程.

- 整个完整的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段.

- 其中验证、准备、解析3个部分统称为连接.

- 类加载的过程不包括最后的使用和卸载阶段,只包括前5个阶段.

类加载器的作用

负责将生成的.class文件加载到内存中,并生成对应的Class对象

类加载过程的示意图

类加载各阶段

1)加载

- 将class文件读入内存,并为之创建一个java.lang.Class对象,即程序中使用任何类时,系统都会为之建立一个java.lang.Class对象,系统中所有的类都是java.lang.Class的实例.

- 类的加载由类加载器完成,jvm提供的类加载器叫做系统类加载器,此外还可以通过继承ClassLoader基类来自定义加载器.

- 通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源.

① 从本地文件系统加载class文件,这是大部分示例程序的类加载方式.

② 从JAR包加载class文件,这种方式也很常见,比如JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件.

③ 通过网络加载class文件.

④ 把一个Java源文件动态编译,并执行加载.

2)验证

当类被加载之后,系统会为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中.连接阶段的第一个部分就是验证.验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致.验证的目的在于确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身安全.其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证.

3)准备

准备阶段负责为类的静态变量分配内存,并设置默认初始值.

4)解析

将类的二进制数据中的符号引用替换成直接引用.说明一下,符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行.布局和内存无关.直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄,该引用是和内存中的布局有关的.

5)初始化

初始化是为类的静态变量赋予一个初始值.准备阶段和初始化阶段是不一样的,比如一个类中有这样的语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行连接的验证步骤,验证通过后进入准备阶段,给a分配内存,此时a的默认初始值为0,然后到解析阶段,再到初始化阶段,最后才把a真正的值赋给了a,即a=10.

类加载器的分类

1.从虚拟机的角度考虑,存在两种不同的类加载器

启动类加载器:是虚拟机自身的一部分.

所有其它类加载器:这些类加载器都由java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存之后才能去加载其它的类.

2.从开发的角度考虑,存在三种不同的类加载器

启动类加载器:Bootstrap ClassLoader,它负责加载存放在JDK安装目录\jre\lib下,或被

-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(比如rt.jar,所有的java.*开头的类都能被Bootstrap ClassLoader加载).启动类加载器是无法被Java程序直接引用的.它是虚拟机的一部分.

扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK安装目录\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器.

应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器.

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器.

三种常见类加载器解释

1.根类加载器:bootstrap class loader,也叫引导类加载器/启动类加载器.负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class.不属于java类库,由C++实现.它的加载路径是<JAVA_HOME>\lib

下面程序可以获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:

public class ClassLoaderTest {
	public static void main(String[] args) {
		
		URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
		for(URL url : urls){
			System.out.println(url.toExternalForm());
		}
	}
}

2.扩展类加载器:extensions class loader,属于sun.misc.Launcher的一个内部类.负责加载JRE的扩展目录,..\lib\ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类.由Java语言实现.它的加载路径是<JAVA_HOME>\lib\ext

3.应用类加载器:也称系统类加载器,该类加载器由sun.misc.Launcher$AppClassLoader来实现.它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者用户类路径(ClassPath)所指定的类.程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器.如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器.由Java语言实现,它的父类加载器为ExtClassLoader.

ps:根类加载器对于开发人员而言就是一个概念性的东西,无法通过代码获取,它属于jvm层面.

类加载器的层次关系

这种层次关系称为类加载器的双亲委派模型.我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码.该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式.

双亲委派模型的工作流程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类(比如虚拟机无法识别的一些类库).

双亲委派原则的好处

1.可以避免重复加载,也就是说,父类已经加载了,子类就不需要再次加载.再通俗一点,一个类要么是通过父类加载的要么是通过子类加载的.

2.更加安全,用户不可以随意定义类加载器来加载核心api.比如通过网络传递一个名java.lang.Integer的类,通过双亲委托模型传递到启动类加载器,而启动类加载器在核心Java API发现了这个名字的类,发现的该类会被加载,此时并不会重新加载网络传递过来的java.lang.Integer类,而直接返回已加载过的Integer.class,这样就可以防止核心API库被随意篡改.

类加载时机

1)创建类的实例,也就是new一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4)反射(Class.forName("..."))

5)初始化一个类的子类(会首先初始化子类的父类)

6)JVM启动时标明的启动类,即文件名和类名相同的那个类

JVM的类加载机制

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class一并由该类加载器负责载入,除非显示使用另外一个类加载器来载入.

双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类.通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载完成.

缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中.这就是为什么修改了Class后,必须重启JVM程序所做的修改才会生效的原因.

自定义类加载器的两种方式

1.遵守双亲委派模型:继承ClassLoader,重写findClass()方法.

2.破坏双亲委派模型:继承ClassLoader,重写loadClass()方法.

ps:通常推荐采用第一种方法自定义类加载器.

类加载的全过程

1.加载:将class文件的字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构(二进制),在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口.这个过程需要类加载器参与.最后外部可以通过Class对象作为操作类的入口.

2.连接:将java类的二进制代码合并到jvm的运行状态之中的过程.

• 验证

– 确保加载的类信息符合JVM规范,没有安全方面的问题.

• 准备

– 正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配.

• 解析

– 虚拟机常量池内的符号引用替换为直接引用的过程.【符号引用比较抽象,直接引用比较具体】

3.初始化

• 初始化阶段是执行类构造器<clinit>()方法的过程.类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的.

• 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先触发其父类的初始化.

• 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步(保证线程安全).

ps:静态变量也称静态域、静态属性.

类的主动引用(一定会发生类的初始化)

– new一个类的对象

– 调用类的静态成员(除了final常量)和静态方法

– 使用java.lang.reflect包的方法对类进行反射调用

Class.forName("类的全路径名称");

– 当虚拟机启动,则一定会初始化类.说白了就是启动main方法所在的类.

– 当初始化一个类,如果其父类没有被初始化,则先会初始化他的父类

类的被动引用(不会发生类的初始化)

– 当访问一个静态域时,只有真正声明这个域的类才会被初始化(调用的这个静态域真正所属的类)

• 通过子类引用父类的静态变量,不会导致子类初始化

– 定义该类型的数组,不会触发此类的初始化

– 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)

测试一:

package com.loser.test;

public class Demo01 {
	static {
		System.out.println("Demo01 静态块");
	}
	public static void main(String[] args) {
		System.out.println("Demo01...");
//		A a=new A();
//		System.out.println(A.width);//主动引用
//		A a2=new A();
		System.out.println(A.MAX);//被动引用
	}
}
class A extends A_F{
	public static int width=100;//静态非常量
	public static final int MAX=666;//静态常量
	static {
		System.out.println("A 静态块");
		System.out.println(width);
		width=200;
	}
	public A() {
		System.out.println("类A的无参构造方法");
	}
}
class A_F{
	static {
		System.out.println("A_F 静态块");
	}
}

ps:当调用静态非常量的时候,会去加载类;当调用静态常量的时候,不会去加载类。

测试二:

package com.loser.test;

public class Demo01 {
	static {
		System.out.println("Demo01 静态块");
	}
	public static void main(String[] args) throws ClassNotFoundException {
		System.out.println("Demo01...");
		System.out.println(B.width);//类B不加载,width属性继承自类A,类A才会加载
	}
}
class B extends A{
//	public static int width=2;//取消注释,类B会加载
	static {
		System.out.println("B 静态块");
	}
}
class A extends A_F{
	public static int width=100;//静态非常量
	public static final int MAX=666;//静态常量
	static {
		System.out.println("A 静态块");
		System.out.println(width);
		width=200;
	}
	public A() {
		System.out.println("类A的无参构造方法");
	}
}
class A_F{
	static {
		System.out.println("A_F 静态块");
	}
}

猜你喜欢

转载自blog.csdn.net/wxd772113786/article/details/104433184