首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深入理解Java类加载器(ClassLoader)

深入理解Java类加载器(ClassLoader)

作者头像
张哥编程
发布2024-12-13 16:44:38
发布2024-12-13 16:44:38
6980
举报
文章被收录于专栏:云计算linux云计算linux

1.什么是类的加载(类初始化)

JVM重要的一个领域,类加载 当程序主动使用某个类时,如果该类还没被加载到内存中,则JVM就会通过加载,连接,初始化三个步骤来对类进行初始化,如果没有意外,JVM将会连续完成三个步骤,所以有时也把这个三个步骤称为类的初始化或类加载。

而类加载必然涉及类加载器,下面我们先了解一下类的加载

代码语言:javascript
复制
类的加载(类初始化):
1.在java代码中,类型的加载,连接,初始化过程都是在程序运行期间完成的,类从磁盘加载到内存的三个阶段。
提供了更大的灵活性,提供了更多的可能性。

上面的第一句话虽然简短,但是包含了两个重要的概念。

代码语言:javascript
复制
类型:

定义的类,接口或枚举称为类型,而不涉及对象,在类的加载过程中,是一个创建对象之前的一些信息。

程序运行期间:

程序运行期间完成的典型例子就是动态代理,其实很多语言编译期间就完成了加载,也正因为这个特性给Java程序提供更大的灵活性,提供了更多的可能性

类加载注意事项:

代码语言:javascript
复制
类加载器并不需要等到某个类被首次主动使用时再加载它。
JVM规范允许类加载在预料到某个类将要被使用时,就预先加载它。
如果在预先加载的过程遇到.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(linkagerror),如果该类一直没有被程序主动使用,那么类加载就不会报告错误。

2.类的生命周期

从上图可知,类从被加载到虚拟机内存,到卸载出内存为止,类的生命周期是从加载,验证,准备,解析,初始化,使用,卸载七个阶段组成的。而验证,准备,解析统称为连接阶段。 加载,验证,准备,初始化和卸载这五个阶段的顺序都是固定确定的。类的加载过程必须按照这种顺序开始(注意是“开始”,而不是进行)。而解析阶段则不一定,它在某些情况下可以在初始化后再开始,这是为了支持java语言的运行时绑定,也就是java的动态绑定或者晚期绑定。 2.1 加载

在上面已经提到过,加载阶段是类加载的第一个阶段,类的加载过程就是从加载阶段开始的。 加载阶段指定是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象(JVM规范并未说明Class对象位于哪里,Hotspot虚拟机将其放在方法区中),用来封装类在方法区内的数据结构,类的加载的最终产品是位于堆区中Class对象。Class对象封装了类在方法区的数据结构,并且向Java程序提供了访问了方法区内的数据结构的接口。

代码语言:javascript
复制
Class对象位置(HotSpot虚拟机)
1.在jdk1.7是在方法区中或永久代中。
2.在jdk1.8是在方法区中或元空间中。
jdk1.8中移除了永久代,转而用元空间来实现方法区。

注意

代码语言:javascript
复制
方法区其实只是一个虚拟的概念,方法区具体的实现其实是永久代和元空间,1.7是永久代,1.8是元空间。
元空间和永久代最大的区别就是:JDK7的永久代放在堆中并且独立与堆,JDK8的元空间完全剥离虚拟机,存在于直接内存中。


编写一个新的Java类时,JVM会帮我们编译成class对象,存放在同名的.class文件中。在运行时,当需要生成这个类的对象时,JVM会帮我们检查该类是否已经加载到内存中,若是没有加载,则把.class文件加载到内存中,若是已经加载,则根据class文件生成实例对象。

怎么理解Class对象和new出来的对象的关系? 可以把Class对象看成一个模板,每个new出来的对象都是按照 Class对象这个模板参照出来的,为啥可以参照呢?因为Class对象提供了访问方法区内数据结构的入口。

总结: 加载阶段简单来说就是: .class文件(二进制数据)-——>读取到内存中-——>数据放到方法区-——>堆中创建对应的Class对象-——>并提供访问方法区的接口 相对于类加载的其他阶段而言,加载阶段(准确来说,是加载阶段获取类的二进制字节流的动作)是可控性最高的阶段。因为开发人员既可以使用系统提供的类加载器来完成加载,也可以定义自己的类加载来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象。这样便可以通过该对象访问方法区中的这些数据。 加载.class文件的方式: 类的加载由类加载器完成,类的加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础。JVM提供的这些类加载器通常被称为系统类加载器,除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。通过使用不同的类加载器,可以从不同来源加载类的二进制数据。二进制数据通常有以下几种来源。

代码语言:javascript
复制
从本地系统中直接加载。
通过网络下载.class文件。
从zip,jar包等归档文件中加载.class文件。
从专用数据库中提取.class文件。
将java源代码编译为.class文件。

2.2 验证

代码语言:javascript
复制
验证:确保被加载的类的正确性
关于验证大可不必深入但是了解类加载机制必须要知道有这么一个过程,以及知道验证就是为了确保class文件文件的字节流中包含的信息符合当前虚拟机的要求即可。
下面关于验证的内容作为了解即可。

验证是连接阶段的第一阶段,这一阶段的目的是确保class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害当前虚拟机自身的安全,验证阶段大致会完成四个阶段的检验操作。

代码语言:javascript
复制
文件格式验证:验证字节流是否符合Class文件格式的规范,例如:是否以0xCAFEBABE开头,主次版本号是否在当前虚拟机的处理范围之内,常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意,对比Javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求,例如这个类是否有父类,除了java.lang.Object这个类以外。
字节码验证:通过程序流和数据流分析,确保程序语言是合法的,符合逻辑的。
符合引用验证:确保解析操作能正确执行。
验证阶段是非常重要的,但不是必须的,他对程序运行期间没有任何影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,来缩短虚拟机加载类的时间。

2.3 准备(重点)

当完成字节码文件的校验之后,JVM便会开始为类变量分配内存并初始化,准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都在方法区分配。

代码语言:javascript
复制
这里有两个需要注意的关键点,即内存分配的对象以及初始化的类型。

内存分配的对象:要明白首先要知道Java中的变量有类变量和类成员变量两种类型。类变量指的是被static修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,JVM只会给类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始。

举个例子,例如下面的代码在准备阶段,只会给LeiBianLiang 属性分配内存,而不会为ChenYuanBL 属性分配内存。

public static int LeiBianLiang = 666; public String ChenYuanBL = "jvm";

**初始化的类型:**在准备阶段,JVM会为类变量分配内存,并为其初始化(JVM只会给类变量分配内存,而不会为类成员变量分配内存,类成员变量自然这个时候也不能初始化)。但是这里的初始化指的是为变量赋予Java语言中该类型的默认值。而不是用户代码里初始化的值。 例如下面的代码在准备阶段之后,LeiBianLiang的值将是0,而不是666。

public static int LeiBianLiang = 666;

注意: 但如果一个变量是常量(被static final修饰的)的化,那么准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,ChangLiang 的值将是666,而不是0。

public static final int ChangLiang = 666;

之所以被static final修饰的变量会直接赋值,而static修饰的变量会被赋予java语言类型的默认值,其实我们稍微思考一下就会明白。 两个语句的区别在于一个有final关键字修饰,另外一个没有,而final关键字在Java中代表不可改变的。意思就是说我们如果一旦对ChangLiang 赋值就不会再改变,既然一旦赋值就不会再改变,那么就必须一开始就要赋予用户希望的值。因此被final修饰的类变量再准备阶段就会被赋予想要的值,而没有被final修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。 2.4 解析

当通过准备阶段之后,就进入了解析阶段,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类符号引用进行,符号引用就是一组符号来描述目标,可以是任何的字面量。

直接引用就是直接指向目标的指针,相对偏移或一个间接定位到目标的句柄。 2.5 初始化(重点)

到了初始化阶段,用户定义的代码才真正开始执行

Java程序对类的使用方式分为两种,主动使用和被动使用。一般来说,只有当对类的首次主动使用才会导致类的初始化。所以主动使用又叫做类加载过程中初始化开始的时间。 那什么是主动使用呢?包括以下六种方式。

代码语言:javascript
复制
1.创建类的实例,也就是使用new关键字创建对象。
2.调用类的静态方法。
3.访问某个类或者接口的静态变量,或者对该静态变量赋值。被final和staic修饰的变量除外(是在编译器把结果放入常量池的静态字段)。
4.反射(例如Class.forName()和对象.class)。
5.初始化某个类的子类,则其父类也会被初始化。
6.Java虚拟机启动时被标明为启动类的类(JavaTest),还有就是Main方法的类会首先被初始化。

最后注意一点对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承,多态中最为明显。

2.6 使用

当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码,使用阶段大家了解一下即可。 2.7 卸载

当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存。 2.8 结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

代码语言:javascript
复制
执行了System.exit()方法。
程序正常执行结束。
程序在执行过程中遇到异常或者错误而异常终止。
由于操作系统出现错误导致Java虚拟机进程终止。

3.接口的加载过程

接口加载过程与类加载过程稍有不同。

当一个类在初始化时,要求其父类全部都已经初始化过了。但是一个接口在初始化时,并不要求其父接口全部都已经完成初始化。当真正用到父接口的时候才会初始化。

package com.jvm.classloader;public class ClassAndObjectLnitialize {public static void main(String[] args) {System.out.println("输出的打印语句");}public ClassAndObjectLnitialize(){System.out.println("构造方法");System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);}{System.out.println("普通代码块");}int ZhiShang = 250;static int QingShang = 666;static{System.out.println("静态代码块");} }

运行结果静态代码块输出的打印语句

下面我们简单分析一下,上面提到过触发初始化主动使用第六点,Java虚拟机启动时被标明为启动类的类(JavaTest),还有就是Main方法的类首先被初始化。

那么类的初始化顺序到底是怎么样的呢,在我们的代码中,我们只知道一个构造方法。但实际上Java代码编译成字节码之后,最开始是没有构造方法的概念,只有类初始化方法和对象初始化方法。

类初始化方法:编译器会按照其出现顺序,收集:类变量(static修饰的)的赋值语句,静态代码块最终组成类初始化方法。类初始化的方法一般在类初始化的时候执行。 所以上面的这个例子,类初始化方法就会执行下面这段代码。

static int QingShang = 666; //类变量(static变量)的赋值语句static //静态代码块{System.out.println("静态代码块");}

而不会执行普通代码块和普通赋值语句了。

对象初始化方法:编译器会按照其出现顺序,收集:成员变量的赋值语句,普通代码块,最后收集构造函数的代码,最终组成对象初始化的方法。值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化的方法,对象初始化方法一般在实例化对象的时候执行。

以上面的这个例子,对象初始化方法就会执行下面这段代码。

代码语言:javascript
复制
{                        System.out.println("普通代码块");    //普通代码块}int ZhiShang = 250;   //成员变量的赋值语句System.out.println("构造方法");  //最后收集构造函数的代码System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);

4.理解首次主动使用

我在上面提到过Java程序对类的使用方式分为两种,主动使用和被动使用。一般来说只有当对类首次主动使用的时候才会导致类的初始化,其中首次很重要,话不多说,上代码

package com.example.day07;class Father{public static int a = 1;static {System.out.println("父类静态代码块");} } class Son extends Father{public static int b = 2;static {System.out.println("子类静态代码块");} }public class Test2 {static {System.out.println("Main方法静态代码块");}public static void main(String[] args) {Father father;System.out.println("======");father=new Father();System.out.println("======");System.out.println(Father.a);System.out.println("======");System.out.println(Son.b);} } 分析:

代码语言:javascript
复制
首先根据主动使用概括的第六点,Main方法所在的类首先被初始化。所以最先执行Main方法所在类的静态代码块。
而Father father只是声明了一个引用,不会执行什么。
当运行到father=new Father();的时候,看到了关键字new并且将father引用指向了Father对象,说明主动使用了,所以父类Father将被初始化。因此打印了:父类静态代码块。之后执行了 System.out.println(Father.a);属于访问静态变量,也属于是主动使用,这个时候注意了,因为上面运行到father=new Father();的时候,已经触发了Father类主动使用并且初始化一次,这次不是首次主动使用了,所以Father不会在被初始化,自然它的静态代码块也不会在执行。所以直接打印静态常量值1。
而后面的 System.out.println(Son.b);同样也只是初始化自己,不回去初始化父类,只因为父类Father已经不再是首次主动调用了。

5.类加载器

所有的类被加载都是通过类加载器完成的。

类加载器负责加载所有的类,其为所有被载入内存的类生成一个java.lang.Class对象实例对象。一旦一个类被加载到JVM中,同一个类就不会被再次加载。正如一个对象有一个唯一标识一样。一个加载到JVM中的类也有一个唯一标识。

代码语言:javascript
复制
关于唯一标识符:
在java中,一个类用其全限定类名(包括包名和类名)作为标识。
但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中。然后转换为一个与目标类对应的java.lang.Class对象实例。在虚拟机提供了三种类加载器:启动(Bootstrap)类加载器,扩展(Extension)类加载器,系统(System)类加载器(也称问应用类加载器)。

111111.png
111111.png

站在Java开发人员的角度来看,类加载器大概可以分为以下几类。

代码语言:javascript
复制
启动类加载器(BootstrapClassLoader):启动类加载器主要加载的是JVM自身需要的类,这个类加载器是C++语言实现的,是虚拟机的一部分。负责加载存放在JDK\jre\lib(JDK代表的是我们安装JDK目录的位置,下面同理)下面的类,或者-Xbootclasspath参数指定的路径中,并且能被虚拟机识别的类库(如rt.jar,所有的java开头的类均被BootstrapClassLoader加载),启动类加载器是无法被java程序员直接引用的。
总结一句话:
启动类加载器加载java运行过程中的核心类库JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List…
扩展类加载器(ExtensionClassLoader):该加载器是由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录下的类,或者由java.ext.dirs系统变量指定的路径中所有类库(如javax开头的类),开发者可以直接使用扩展类加载器。
应用类加载器(ApplicationClassLoader):该加载器是由sun.misc.Launcher$AppClassLoader实现。它负责加载用户类路径(ClassPath)所指定的类。开发者可以直接使用该类加载器。如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
总结:
应用类加载器加载ClassPath变量指定路径下的类,即用户自己项目工程编写的类。
线程上下文类加载器:除了以上列举的三种类加载器,还有一种比较特殊的类加载器,就是线程上下文类加载器。类似Thread.currentThread().getContextClassLoader()获取线程上下文类加载器。线程上下文类加载器其实很重要,它违背(破坏)双亲委派模型。很好的打破了双亲委派模型的局限性。尽管我们在开发中很少用到,但是框架组件开发绝对要频繁使用到线程上下文类加载器如tomcat等。

在java程序的日常开发中,类的加载几乎是前面三种类加载相互配合执行的,在必要时,我们还可以自定义类加载器(ClassLoader),因为JVM自带的类加载只是懂得从本地文件系统加载标准的class文件,因此如果我们编写了自己的类加载器(ClassLoader),便可以做到如下几点:

代码语言:javascript
复制
在执行非置信代码之前,自动验证数字签名。
动态的创建符合用户特定需要的定制化构建类。
从特定的场所获得java class,例如数据库和网络中。

需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用到该类时,才会将它的class加载到内存中生成Class对象。而且加载某个类的Class文件时,Java虚拟机默认采用的是双亲委派模式即把请求交给父类处理。它是一种任务委派方式,下面将会详细说到。

下面我们看一个程序:如何获取父类加载器。

代码语言:javascript
复制

从上面的结果来看,并没有获取到ExtClassLoader的父加载器,原因是BootstrapClassLoader启动类加载器是C++语言实现的(这里仅限Hotspot虚拟机,也就是JDK1.5以后Java默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),找不到一个确定的返回父Loader的方式,于是就返回null,至于$符合是内部类的含义。 6.java虚拟机入口应用:sun.misc.Launcher

Launcher是什么?如果你没有接触过Launcher可能会觉得特别陌生,但是它又特别重要,为何说它重要呢?不知道你有没有想过为啥类加载器首先会去到应用程序类加载器ApplicationClassLoader。是的,我在介绍应用类加载器ApplicationClassLoader和扩展类加载器ExtensionClassLoader的时候就已经提到过这两个类加载器是sun.misc.Launcher实现的。为了更好的理解,我们查看一下Launcher的源码。

public class Launcher {private static Launcher launcher = new Launcher();private static String bootClassPath =System.getProperty("sun.boot.class.path");public static Launcher getLauncher() {return launcher;}private ClassLoader loader;public Launcher() {// Create the extension class loaderClassLoader extcl;try {extcl = ExtClassLoader.getExtClassLoader();} catch (IOException e) {throw new InternalError("Could not create extension class loader", e);}// Now create the class loader to use to launch the applicationtry {loader = AppClassLoader.getAppClassLoader(extcl);} catch (IOException e) {throw new InternalError("Could not create application class loader", e);}//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解Thread.currentThread().setContextClassLoader(loader);}/** Returns the class loader used to launch the main application./public ClassLoader getClassLoader() {return loader;}/** The class loader used for loading installed extensions./static class ExtClassLoader extends URLClassLoader {}/*** The class loader used for loading from java.class.path.* runs in a restricted security context.*/static class AppClassLoader extends URLClassLoader {}

通过以上源码我们可以看出:

代码语言:javascript
复制
Launcher初始化了AppClassLoader 和ExtClassLoader ,首先是创建了extcl扩展类加载器。
之后的App应用(系统)类加载器作为Launch的一个成员变量,至于为啥不把extcl扩展类加载器也作为成员变量的原因,大家想一想,是为啥呢,其实很简单,因为没必要,因为直接使用App系统类加载的getParent()即可得到extcl扩展类加载器。
Launcher中并没有看到BootstrapClassLoader,但通过System.getProperty("sun.boot.class.path");得到了字符串bootClassPath。这个应该就是BootstrapClassLoader加载jar包的路径。我们打印一下看看里面是什么内容,其实就是Jre目录下的jar包或者class文件。

System.out.println(System.getProperty("sun.boot.class.path"));

输出结果:

D:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar; D:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar; D:\Program Files\Java\jdk1.8.0_144\jre\lib\sunrsasign.jar; D:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar; D:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar; D:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar; D:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar; D:\Program Files\Java\jdk1.8.0_144\jre\classes

7.ExtClassLoader和AppClassLoader源码

如果想进一步了解其中的原理,可以看下源码。 7.1 ExtClassLoader源码

/** The class loader used for loading installed extensions./static class ExtClassLoader extends URLClassLoader {static {ClassLoader.registerAsParallelCapable();}/** create an ExtClassLoader. The ExtClassLoader is created* within a context that limits which files it can read*/public static ExtClassLoader getExtClassLoader() throws IOException{final File[] dirs = getExtDirs();try {// Prior implementations of this doPrivileged() block supplied// aa synthesized ACC via a call to the private method// ExtClassLoader.getContext().return AccessController.doPrivileged(new PrivilegedExceptionAction<ExtClassLoader>() {public ExtClassLoader run() throws IOException {int len = dirs.length;for (int i = 0; i < len; i++) {MetaIndex.registerDirectory(dirs[i]);}return new ExtClassLoader(dirs);}});} catch (java.security.PrivilegedActionException e) {throw (IOException) e.getException();}}private static File[] getExtDirs() {String s = System.getProperty("java.ext.dirs");File[] dirs;if (s != null) {StringTokenizer st =new StringTokenizer(s, File.pathSeparator);int count = st.countTokens();dirs = new File[count];for (int i = 0; i < count; i++) {dirs[i] = new File(st.nextToken());}} else {dirs = new File[0];}return dirs;}}

我们可以通过指定-D java.ext.dirs参数来添加或者改变ExtClassLoader的加载路径。 7.2 AppClassLoader源码

/*** The class loader used for loading from java.class.path.* runs in a restricted security context.*/static class AppClassLoader extends URLClassLoader {public static ClassLoader getAppClassLoader(final ClassLoader extcl)throws IOException{final String s = System.getProperty("java.class.path");final File[] path = (s == null) ? new File[0] : getClassPath(s);return AccessController.doPrivileged(new PrivilegedAction<AppClassLoader>() {public AppClassLoader run() {URL[] urls =(s == null) ? new URL[0] : pathToURLs(path);return new AppClassLoader(urls, extcl);}});} }

从源码中我们可以看到AppClassLoader加载的就是java.class.path下路径。 7.3 小结

从上面的源码可以看出,父加载器并不是指其父类,ExtClassLoader和AppClassLoader同样继承自URLClassLoader 。这个时候会有人问了,那为何调用AppClassLoader.getParent()方法会得到ExtClassLoader实例呢。 实际上URLClassLoader源码中也没有getParent()方法,这个方法是ClassLoader中的,源码如下:

public abstract class ClassLoader {// The parent class loader for delegation // Note: VM hardcoded the offset of this field, thus all new fields // must be added after it. private final ClassLoader parent; // The class loader for the system// @GuardedBy("ClassLoader.class") private static ClassLoader scl;private ClassLoader(Void unused, ClassLoader parent) {this.parent = parent;... } protected ClassLoader(ClassLoader parent) {this(checkCreateClassLoader(), parent); } protected ClassLoader() {this(checkCreateClassLoader(), getSystemClassLoader()); } public final ClassLoader getParent() {if (parent == null)return null;return parent; } public static ClassLoader getSystemClassLoader() {initSystemClassLoader();if (scl == null) {return null;}return scl; }private static synchronized void initSystemClassLoader() {if (!sclSet) {if (scl != null)throw new IllegalStateException("recursive invocation");sun.misc.Launcher l = sun.misc.Launcher.getLauncher();if (l != null) {Throwable oops = null;//通过Launcher获取ClassLoaderscl = l.getClassLoader();try {scl = AccessController.doPrivileged(new SystemClassLoaderAction(scl));} catch (PrivilegedActionException pae) {oops = pae.getCause();if (oops instanceof InvocationTargetException) {oops = oops.getCause();}}if (oops != null) {if (oops instanceof Error) {throw (Error) oops;} else {// wrap the exceptionthrow new Error(oops);}}}sclSet = true;} } }

从上面的源码可以看到getParent()方法实际上返回的是一个ClassLoader对象parent。parent的赋值是在ClassLoader对象的构造方法中,它又两个情况

代码语言:javascript
复制
由外部类创建ClassLoader时直接指定一个ClassLoader作为parent。
由getSystemClassLoader()方法生成,也就是在sun.misc.Launcher通过getClassLoader()获取,也就是AppClassLoader。简单来说就是,如果一个ClassLoader创建时没有指定parent,那么它parent默认的就是AppClassLoader。

8.关于命名空间

说到类加载器,就必须说一下命名空间这个概念,实际上类加载一个必不可少的前提就是命名空间。

代码语言:javascript
复制
命名空间概念:
每个类加载器都有自己的命名空间,命名空间由该类加载器及所有的父加载器所加载的类构成。

特别注意的是:
1.在同一个命名空间中,不会出现类的完整名字(包括包名)相同的两个类。
2.在不同的命名空间,有可能出现类的完整名字(包括包名)相同的两个类
3.由子加载器加载的类能看见父加载器加载的类,但是父加载器加载的类不能看见子加载器加载的类。

我们已经知道了每个类只能被加载一次,其实这样说是不够准确的,怎么样才算是准确的呢,那就涉及到命名空间的概念了,只有在相同的命名空间中,每一个类才能只被加载一次,反过来说,就是一个类不在同一个命名空间可以被加载多次,而被加载多次的Class对象是相互独立的。 9.JVM类加载机制

JVM的类加载机制主要有如下三种:

代码语言:javascript
复制
全盘负责:当一个类加载器负责加载某一个Class时,该Class所依赖的和引用的其他Class也将由该类加载负责载入,除非我们显示使用另外一个类加载来载入。
父类委托:先让父类加载器试图加载此类,只有当父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的说,子类们遇到事情都去找父类帮忙,也不管自己能不能解决,先去找父亲解决,一个个向上抛,直到抛到最顶级的父类启动类加载器,父类解决不了了子类才去解决。直到某个子类能做才做,之后的子类就直接返回。
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器会先从缓冲区寻找该Class,只有缓冲区不存在,系统才会读取该类的二进制数据。并将其转换成Class对象,存入缓冲区,这就是为什么修改了Class文件,需要重启JVM,程序的修改才会生效。

9.JVM类加载机制 10.双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜素范围中没有找到所需要的类时,即无法完成该加载,子加载器才会尝试去加载该类。

双亲委派机制:

代码语言:javascript
复制
1.当AppClassLoader去加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委托给父加载器ExtClassLoader去完成。
2.当ExtClassLoader去加载一个class时,它首先也不会去尝试加载这个类,而是把类加载请求委托给父加载器BootstrapClassLoader去完成。
3.如果BootstrapClass加载失败(例如在JAVA_HOME/jre/lib里未找到该class),就会使用ExtClassLoader来尝试加载。
4.如果使用ExtClassLoader加载失败(例如在JAVA_HOME/jre/lib/ext里未找到该class),就会使用AppClassLoader来尝试加载。如果AppClassLoader也加载失败,则会抛出异常ClassNotFoundException。
请添加链接描述
请添加链接描述

从代码层面了解几个Java中定义的类加载器及双亲委派模型的实现,他们的类图关系如下。

请添加链接描述
请添加链接描述

从图可以看出顶层的类加载器是抽象类abstract ClassLoader,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)。为了更好的理解双亲委派模型,ClassLoader源码中的loadClass(String)方法该方法加载指定名称(包括包名)的二进制类型,该方法在jdk1.2后不建议用户重写但是可以直接调用该方法。loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现。loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成Class对象的进行解析相关的操作,源码分析如下。

public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 先从缓存查找该class对象,找到就不用重新加载Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {//如果找不到,则委托给父类加载器去加载c = parent.loadClass(name, false);} else {//如果没有父类,则委托给启动加载器去加载c = findBootstrapClassOrNull(name);}} 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// 如果都没有找到,则通过自定义实现的findClass去查找并加载c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {//是否需要在加载时进行解析resolveClass(c);}return c;}}

既然存在这个双亲委派模型,那么就一定有着存在的意义,其意义主要是:Java类随着它的类加载器一起具备了一种带有优先级的层级关系,通过这种层级关系,可以避免类的重复加载。当父类已经加载了该类时,就没必要子类加载器ClassLoader再加载一次,其次是考虑到安全因素。Java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.String的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心api中发现了这个名字的类,发现该类已经被加载了,并不会重新加载网络传递过来的java.lang.String的类,而直接返回已经加载过的String.class。这样便可以防止核心api库被随意篡改。

代码语言:javascript
复制
双亲委派模型意义总结来讲就是:
1.系统类防止内存中出现多份相同的字节码。
2.保证Java程序安全稳定运行。

11.ClassLoader源码分析

ClassLoader是一个抽象类,所有的类加载器都继承自ClassLoader(不包括启动类加载器)。因此它显得格外重要,所以我们才有有更深一步的认识和分析。 简单小结一下ClassLoader抽象类中的一些概念: 二进制概念(Binary name)格式如下: 在这里插入图片描述 把二进制名字转换成文件名字,然后在文件系统中磁盘上读取其二进制文件(class文件),每一个class对象都包含了定义这个类的classloade对象。class类都是类加载器加载的,只要数组类型是JVM根据需要动态生成的。

特别注意的是数组类型:

代码语言:javascript
复制
1.数组类的类对象不是类加载器创建的,而是根据Java运行时的需要自动创建的。
2.数组类的类加载器getClassLoader()和它的元素类型的类加载器相同,如果元素类型是基本类型,那么数组类没有类加载器也就是null。而这个null不同于根类加载器返回的null,它是单纯的null

接下来我们分析一些ClassLoader几个重要的方法。 11.1 loadClass

该方法加载指定名称(包括包名)的二进制类型,该方法在jdk1.2后不建议用户重写但是可以直接调用该方法。loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现。其源码如下,loadClass(String name, boolean resolve)是一个重载方法,其中resolve参数代表是生成class对象的同时进行解析的相关操作

public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 先从缓存查找该class对象,找到就不用重新加载Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {//如果找不到,则委托给父类加载器去加载c = parent.loadClass(name, false);} else {//如果没有父类,则委托给启动加载器去加载c = findBootstrapClassOrNull(name);}} 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// 如果都没有找到,则通过自定义实现的findClass去查找并加载c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {//是否需要在加载时进行解析resolveClass(c);}return c;}}

正如loadClass()方法所展示的,当一个类加载请求到来时,会先使用findLoaderClass()方法从缓存区中寻找该类对象,如果存在直接返回。如果不存在则交给该类加载器的父类加载器去加载,如果没有父类加载器则交给顶层启动类加载器去加载,最后都没有找到的话,会调用findClass()方法去加载(后续会详细讲解)。从loadClass实现也可以知道,如果我们不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载指定自己指定的类,那么可以直接使用this.getClass().getClassLoader().loadClass(className),这样就可以直接调用ClassLoader的loadClass()方法获取class对象。 11.2 findClass

在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader并重写loadClass()方法。从而实现自定义的类加载类,但是在JDK1.2后不建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中。从前面分析可知,findClass()方法是在loadClass()方法中被调用的。当loadClass()方法中父类加载器加载失败后,则会调用自己的findClass()方法来完成来加载。这样就可以保证自定义的类加载器也符合双亲委派的模式,需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出一个ClassNotFoundException异常。同时应该知道的是findClass()方法通常是配合defineClass()方法一起使用,ClassLoader类中的findClass()方法的源码如下:

//直接抛出异常 protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name); }

11.3 defineClass(String name, byte[] b, int off, int len)

defineClass()方法是将byte字节流转换成JVM能给识别的Class对象(ClassLoader中已经实现了该方法逻辑)。通过这个方法不仅能给通过class文件实例化class对象。也可以通过其他方法实例化class对象。如通过网络接收一个类的字节码。然后转换为byte字节流创建对应的Class对象。defineClass()方法通常和findClass()方法一起使用。一般情况下,我们自定义类加载器,会直接覆盖ClassLoader中的findClass()方法并编写加载规则。取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。简单例子如下。

protected Class<?> findClass(String name) throws ClassNotFoundException {// 获取类的字节数组byte[] classData = getClassData(name); if (classData == null) {throw new ClassNotFoundException();} else {//使用defineClass生成class对象return defineClass(name, classData, 0, classData.length);}}

需要注意的是,如果直接调用defineClass()方法生成的Class对象,这个类的Class对象并没有被解析(也可以理解为连接阶段,毕竟解析是连接的最后阶段),其解析操作需要等待初始化阶段进行。 11.4 resolveClass(Class<?> c)

使用该方法可以使类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要使对字节码进行验证,为类变量分配内存并设置初始值,同时将字节码的文件中符号引用转为直接引用。 11.5 ClassLoader小结

以上四种方法是ClassLoader类中比较重要的方法,也是我们经常使用到的方法,SecureClassLoader类扩展了ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及证书的验证)和权限定义认证(主要对class源码的访问权限)的方法。一般我们不会直接跟这个类打交道,而是跟他的子类URLClassLoader有所关联,前面已经说过,ClassLoader是一个抽象类,很多方法都是空的实现,比如findClass(),findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得字节码流等功能。在编写自定义类加载器时候,如果没有特殊的复杂需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClas()及其获取字节码流的方式,使自定义类加载器更加简洁。

代码语言:javascript
复制
loadClass()方法是加载指定名字的class文件,调用findLoadedClass()检查类是否已被加载,如果已经加载就不加载而是直接返回第一次加载结果,所以一个类只会被加载一次。如果没有没有加载,就会抛给父类去加载,父类都加载失败,就会去调用findClass()去加载类。
检查完父类加载器后loadClass回去默认调用findClass()方法,父类ClassLoader中的findClass()方法主要使抛出一个异常。
findClass()方法根据二进制名字找到对应的class文件,返回值是Class对象Class<?>。
defineClass()方法主要是将一个字节数组转换成Class实例,会抛三个异常,但只是throws一个,因为其他两个是运行时异常。

12.自定义类加载器

自定义类加载器的核心目的就是扩展Java虚拟机动态加载类的机制。 JVM默认使用双亲委派机制,虽然双亲委派机制安全性极高,但是有时候我们需要自己的一些方式去加载。比如应用是通过网络传输Java的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其加载,这样则需要自定义类加载器来实现。因此自定义类加载器也是很有必要的。

自定义类加载器一般都是继承ClassLoader类,从上面对loadClass()方法分析来看,我们只需要重写findClass()方法即可,自定义类加载器的重要点:重写findClass()方法。下面直接看自定义类加载器的代码流程。

代码语言:javascript
复制
第一步:编写一个类并继承ClassLoader类。重写其findClass()方法。

package com.example.day08;import java.io.;/** 自定义类加载器*/ public class MyClassLoader extends ClassLoader {//加载的根路径private String root;public String getRoot() {return root;}public void setRoot(String root) {this.root = root;}/**** @param name 包名* @return* @throws ClassNotFoundException*/protected Class<?> findClass(String name) throws ClassNotFoundException {//获取字节码byte[] classData=loadClassData(name);if (classData==null){throw new ClassNotFoundException(name);}else {//生成Class对象return defineClass(name,classData,0,classData.length);}}private byte[] loadClassData(String className) {String fileName=root+ File.separatorChar+className.replace('.',File.separatorChar)+".class";try(ByteArrayOutputStream bos=new ByteArrayOutputStream();InputStream ins=new FileInputStream(fileName);) {System.out.println("fileName: "+fileName);int buffSize=1;/*** 需要一个一个字节的读取,当是1024时,* 报错Exception in thread "main" java.lang.ClassFormatError:* Truncated class file*/byte[] bytes=new byte[buffSize];int len=0;while ((ins.read(bytes)!=-1)){//bos.write(bytes,0,len);bos.write(bytes);}return bos.toByteArray();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return null;}}

代码语言:javascript
复制
在自己想要的位置准备一个class文件,我这里是在桌面准备了一个user.class文件。
在这里插入图片描述
写一个测试类,测试效果

package com.example.day08;/*** 测试自定义类加载器*/ public class TestMyClassLoader {public static void main(String[] args) {MyClassLoader myClassLoader=new MyClassLoader();myClassLoader.setRoot("C:\Users\len\Desktop");Class<?> testClass=null;try {testClass=myClassLoader.loadClass("com.example.day05.User");Object o = testClass.newInstance();System.out.println(o.getClass().getClassLoader());}catch (Exception e) {e.printStackTrace();}} }

代码语言:javascript
复制
测试结果:
在这里插入图片描述
自定义类加载器的核心在于对字节码的获取,如果是加密的字节码则需要在该类中对文件进行解密,上面只是一个简单的Demo。并未涉及到加密,因此省略了解密的过程。这里有几点需要注意。

1.这里传递的文件名需要是类的全限定性名称,即com.example.day05.User格式的,因为defineClass()是按照这种格式进行处理的。
2.最好不要重写loadClass()方法,因为容易破坏双亲委派机制。
3.这User类本身可以被AppClassLoader类加载器加载,因此不能放在类路径下,否则由于双亲委派的机制,会直接导致该类被AppClassLoader加载,而不会通过我们自定义的类加载加载。
4.读取字节码时要一个一个读取字节,因为JVM需要对字节进行验证。如果读取1024个字节,会抛出异常。

13.加载类的三种方式

到这里,相信大家都对类加载器和类的加载都有一定的了解了,那么你知道嘛?常见的加载类的方式有三种。

代码语言:javascript
复制
静态加载:也就是使用new关键字来创建实例对象,
动态加载:使用Class.forName()动态加载(反射加载类型),然后调用类的newInstance()方法实例化对象。
动态加载:通过类加载器的loadClass()方法来加载类,然后调用类的newInstance()方法实例化对象。

13.1 三种方式的区别

代码语言:javascript
复制
第一种和第二种方式使用的类加载器是相同的,都是当前类加载器(this.getClass().getClassLoader()),第三种方式需要用户指定类加载器。
如果需要在当前类路径以外加载类,则只能使用第三种方式,第三种方式加载的类与当前类分属不同的命名空间。
第一种方式是静态加载,第二种和第三种是动态加载。

13.2 两种异常(Exception)

代码语言:javascript
复制
静态加载的时候如果在运行环境中找不到要初始化的类,抛出的是NoClassDefFoundError。它在Java的异常体系中是一个error。
动态加载的时候如果运行环境中找不到要初始化的类,抛出的是ClassNotFoundException。它在Java的异常体系中是一个checked异常。

13.3 Class.forName与ClassLoader.loadClass的区别

代码语言:javascript
复制
首先,我们必须要明白类加载机制的三个主要过程是:加载–>连接–>初始化。

Class.forName():除了将类的.class文件加载到JVM中之外,还对类进行解释,执行类中的static块。
ClassLoder.loadClass():只是将类的.class文件加载到JVM中,并不会执行类的static块,只有在newInstance时才会执行static块。
Class.forName(name, initialize, loader):带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数。创建类的对象 。

下面我们写下代码,加深一下影响。

package com.example.day09; //准备一个测试类 class Demo{static {System.out.println("正在加载Demo...");} }public class Test {public static void main(String[] args) throws ClassNotFoundException {//1.使用 Class.forName()加载类,默认执行static块。Class.forName("com.example.day09.Demo");//获得当前类的类加载器ClassLoader classLoader = Test.class.getClassLoader();//2.使用 Class.forName()加载类,并指定ClassLoader。初始化时不会执行static块。Class.forName("com.example.day09.Demo",false,classLoader);//3.使用ClassLoader类的loadClass()方法加载类,不会执行初始化classLoader.loadClass("com.example.day09.Demo");}}

特别注意: 大家测试的时候需要一个一个测试,因为在使用同一个命名空间下类只能加载一次,为了防止出现误差,大家还是安装我的顺序一个一个在电脑上测试一下。 14.总结

类的加载,连接与初始化:

代码语言:javascript
复制
加载:通过类加载器查找并加载类的.class文件到JVM中
连接:
验证:确保被加载的类的正确性
准备:给类的静态变量分配内存空间,并将其初始化默认值,但是到达初始化之前类变量都没有初始化真正的初始值。(如果是被final修饰的类变量,则直接会被初始成用户想要的值)
解析:把类中的符号引用替换为直接引用,就是在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
初始化:为类的静态变量赋予正确的初始值。

类从磁盘上到内存中需要经历五个阶段:加载-->连接-->初始化-->使用-->卸载

Java程序对类的使用方式可以分为两种:

代码语言:javascript
复制
1.主动使用
2.被动使用

所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时,才能初始化他们。

主动使用:

代码语言:javascript
复制
使用new关键字创建类的实例。
调用类的静态方法
访问某个类或者接口的静态变量(get static ),或者对该静态变量赋值( set static)。
使用反射Class.forName(name)加载。
初始化一个类时,它的父类也会被初始化。
Java虚拟机启动时被标明为启动类的类以及包含Main方法的类
JDK1.7以后提供的动态语言支持(了解即可)

被动使用:

除了上面的七种情况外,其他使用Java类的方式都被看成被动使用,都不会导致类的初始化。 15.总结

初始化入口方法,当进入类加载的初始化阶段,JVM会寻找整个main方法的入口,从而初始化main方法所在的真个类。当需要对一个类进行初始化时,会首先初始化类构造器()。之后初始化对象构造器。

代码语言:javascript
复制
初始化类构造器:JVM会按顺序收集类的静态变量赋值语句和静态代码块,最终组成类构造器由JVM执行。
对象构造器:JVM会按顺序收集类的成员变量赋值语句,普通代码块,最后收集构造方法,将他们组成对象构造器,最后由JVM执行。值得注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化对象的时候执行。
如果在初始化main方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回,如此反复循环。最终返回main方法所在类。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-08-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档