Java 基础 —— JVM 类加载机制

本文详细阐述了Java的编译期与运行期过程,包括类的加载、验证、准备、解析和初始化,以及类加载器的工作原理、双亲委派模型和自定义类加载的应用。特别强调了类加载时机和生命周期中动态绑定的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、编译期与运行期

(1)编译期源代码文件.java —> JVM字节码文件.class )

源代码文件.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> JVM字节码文件(*.class)

(2)运行期JVM字节码文件.class —> 标机器码

执行引擎执行:JVM字节码(*.class) -> 机器无关优化 -> 中间代码 -> 机器相关优化 -> 中间代码 -> 寄存器分配器 -> 中间代码 -> 目标机器码生成器 -> 目标机器码

二、类的生命周期

加载、验证、准备、解析、初始化、使用、销毁,其中验证、准备、解析统称为连接。
在这里插入图片描述

注意:加载、验证、准备、初始化、卸载的开始顺序固定,而解析可以在初始化之后进行,称为动态绑定或晚期绑定(运行时绑定)

类的生命周期结束与卸载

这里是引用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、类的加载过程(运行期)

在这里插入图片描述

Java程序中的类加载是在运行期间完成的:加载、验证、准备、解析、初始化。其中验证、准备、解析过程完全由虚拟机主导和控制类加载也可以通过自定义类加载器初始化阶段执行程序员编写的 Java 程序代码(转化后的字节码)
在这里插入图片描述

类的加载的时机

在什么情况下需要开始类的加载过程的第一个阶段:加载,虚拟机规范中并没有进行强制约束。但是对于初始化阶段,虚拟机规范规定了有且只有 5
种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

(1)当创建一个类的实例时,比如使用 new 关键字、反射、克隆和反序列化。
(2)调用类的静态方法时,即当使用了字节码invokestatic制冷。
(3)使用类、接口的静态字段(final修饰特殊考虑)时,比如 getstatic 和 putstatic 指令。
(4)对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(5)当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)。
遇到 new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法
(6)对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(7)当对子类进行初始化时,若父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)。
(8)如果一个接口定义了 default 方法,那么直接实现或间接实现了该接口的类的初始化,该接口要在其之前被初始化。
(9)虚拟机启动时,main 方法所在的类作为程序执行的入口会首先被加载
(10)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上这 10 种场景中的行为称为对一个类进行主动使用。除此之外,所有引用类的方式都不会触发初始化,称为被动使用,例如:

(1)通过子类引用父类的静态字段,不会导致子类初始化
(2)通过数组定义来引用类,不会触发此类的初始化。MyClass[] cs = new MyClass[10];
(3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。例如访问用 final static 修饰的String或基本数据类型(不包含包裹对象,如Integer等)

Java 不同数据类型的加载

在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载

引用数据类型中数组比较特殊。数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建,
一个数组类(下面简称为C)创建过程就遵循以下规则:

1)如果数组的元素类型是引用类,那就递归采用类加载去加载和创建这个元素类型,然后 JVM 使用指定的元素类型和数组维度来创建数组类,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
2)如果数组的元素类型是基本数据类型(例如int数组),Java虚拟机只根据指定的数组维度来创建数组类,把数组C标记为与引导类加载器关联。
3)数组类的可见性与它的元素类型的可见性一致,如果元素类型不是引用类型,那数组类的可见性将默认为 public。

1、 加载

类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构(JDK1.8 及以后在元空间)。类的加载的最终产品是位于堆区中的 Class 对象,每个类都有一个 Class 类型的对象,Class 对象封装了类在方法区内的数据结构。Class 类向 Java 程序员提供了访问方法区内的数据结构的接口,通过 Class 类提供的接口,可以获得目标类所关联的 .class 文件中具体的数据结构、方法和字段等信息,也是实现反射的关键数据、入口
注:Class 对象的构造方法是私有的,只有 JVM 能创建
在这里插入图片描述

加载发生在类被使用的时候,如果一个类之前没有被加载,那么就会执行加载逻辑。但是,类加载器并不需要等到某个类被首次主动使用时再加载它,JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

加载过程的主要工作包括:

1)通过类的全限定名从磁盘或者网络中来获取定义此类的二进制字节流 —— 字节流来源:zip 包中、网络、运行时计算生成(动态代理)、其他文件(jsp)、数据库中读取(少见);
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(JDK1.8 及以后在元空间);
3)在堆区中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

注意:加载阶段与连接阶段的部分内容(部分字节码文件格式的验证)一般是交叉进行的

2-1 连接-验证

(1)验证目的

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

(2)验证流程

文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。(格式验证和加载阶段一起执行
元数据验证:该阶段对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求,目的是保证不存在不符合 Java 语言规范的元数据信息。
字节码验证:该阶段主要工作时进行数据流和控制流分析,确定程序语义是合法的、符合逻辑,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证的目的是确保解析动作能正常执行。验证的内容主要有:符号引用中通过字符串描述的全限定名是否能找到对应的类;在指定类中是否存在符号方法的字段描述及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

在这里插入图片描述

2-2 连接-准备——静态变量默认赋值

为类变量分配内存并将类变量初始化为默认值。注意这里分配内存的只包括类变量,也就是静态变量(实例变量会在对象实例化的时候分配在堆上),并且这里的设置初始值是指‘零值’。各个数据类型的默认值:
在这里插入图片描述

注:Java 不支持 boolean 类型,对于 boolean 类型,内部实现是 int。由于 int 的默认值是 0,故 boolean 的默认值是 false。

注意:

1)这里不包含基本数据类型用 static final 修饰的情况,因为 final 修饰的基本数据类型的静态变量在编译时就被分配内存了,准备阶段被显示赋值(注:没有默认初始化,因为是常量不能修改)
2)这里不会为实例变量分配内存和初始化,类变量是在方法区,而实例变量是随着实例对象分配在堆空间中
3)在这个阶段并不会像初始化阶段那样有初始化或者代码执行。

jdk1.8 以前,这些变量所使用的的内存都将在方法区中进行分配;jdk1.8 及以后在元数据区中进行分配

2-3 连接-解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用。通过解析操作,符号引用就可以转换为目标方法在类中方法表中的位置,从而使得方法被成功调用。
拓展:如果使用字面量的方式定义一个字符串常量,则是在解析环节直接进行显式赋值

符号引用与直接引用的区别

符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是符合约定的任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。符号引用主要包括以下三类常量:类和接口的全限定名(java.lang.String的全限定名为java/lang/String)、字段的名称和描述符(java.lang.String[][]二维数组的描述符为[[Ljava.lang.String)和方法的名称和描述符(java.lang.String.toString()方法的描述符为()Ljava.lang.String)

直接引用(Direct References): 直接引用可以是类、方法、字段在内存中的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,引用的目标必定已经在内存中存在

3、初始化——静态变量赋值

类初始化阶段是类加载的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说是字节码),为类的静态变量赋予真正的初始值

初始化阶段是执行类构造器<clinit>()方法的过程。<client>方法是由编译器按语句在源文件中出现的顺序自动收集类中的类变量的显示赋值操作和静态语句块中的语句合并而成的(不包括构造器中的语句。构造器是初始化对象的,类加载完成后,创建对象时候将调用的 <init>() 方法来初始化对象)。
<clinit>() 不需要显式调用父类(接口除外,接口不需要调用父接口的初始化方法,只有使用到父接口中的静态变量时才需要调用)的初始化方法 (),虚拟机会保证<client>方法执行之前,父类的<client>方法已经执行完毕

静态变量与静态语句

静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如下程序:

public class Test {
    static {
        // 给变量赋值可以正常编译通过
        i = 0;
        // 这句编译器会提示"非法向前引用"
        System.out.println(i);
    }

    static int i = 1;
     } 

1) <clinit>() 不需要显式调用父类(接口除外,接口不需要调用父接口的初始化方法,只有使用到父接口中的静态变量时才需要调用)的初始化方法 <clinit>()虚拟机会保证在子类的 <clinit>() 方法执行之前,父类的 <clinit>() 方法已经执行完毕,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

2) <clinit>() 方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法

3) 虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 方法完毕。

Java 编译器不一定为所有类都生成 <clinit> 方法,包括:

(1)一个类中没有声明任何类变量,也没有任何静态代码块;
(2)一个类中声明静态变量,但没有该类变量的显示初始化语句以及静态代码块;
(3)一个类中含有 static final 修饰的基本数据类型字段,这些类字段的初始化语句采用编译时常量表达式。

几种数据类型的赋值时机

1、基本数据类型

(1)static 修饰:在准备阶段赋默认值,在初始化阶段(<clinit>)进行显式赋值和静态代码块赋值;
(2)static final 修饰:使用字面量创建时则编译时分配内存,在解析阶段赋显式赋值;若使用方法获得或者 new 创建,则在初始化阶段(<clinit>)进行赋值。

2、包装类型

无论使用 static final 修饰还是 static 修饰都在初始化阶段(<clinit>)显示赋值;

2、String 字符串

(1)使用字面量创建,若使用 static final 修饰则在准备阶段显示赋值;若使用 static 修饰则在初始化阶段(<clinit>)显示赋值;
(2)使用 new 创建,无论使用 static final 修饰还是 static 修饰都在初始化阶段(<clinit>)显示赋值;

三、类加载器

ClassLoader 是 Java 的核心组件,所有的 Class 都是由 ClassLoader 进行加载的,ClassLoader 负责通过各种方式将 Class 信息的二进制数据流读入 JVM 内部,转换为一个与目标类对应的 java.lang.Class 对象实例,然后交给 Java 虚拟机进行链接、初始化等操作。因此,ClassLoader 在整个装载阶段,只能影响到类的加载,而无法通过 ClassLoader 去改变类的链接和初始化行为。至于它是否可以运行,则由 Execution Engine 决定。
在这里插入图片描述

1、类、类加载器与实例之间的关系

在类加载器的内部实现中,用一个 Java 集合来存放所加载的类的引用。一个 Class 对象总是会引用它的类加载器,调用 Class 对象的 getClassLoader() 方法就能得到它的类加载器。因此,代表某个类的 Class 实例与其类的加载器之间为双向关联关系。
一个类的实例总是引用代表这个类的 Class 对象。在 Object 类中定义了 getClass() 方法,这个方法返回代表对象所属类的 Class 对象的引用。此外,所有 Java 类都有一个静态 Class,它引用代表这个类的 Class 对象。
在这里插入图片描述

2、类的唯一性与类加载器

虚拟机把“通过一个类的全限定名来获取定义此类的二进制字节流”这个加载阶段的动作放到 Java 虚拟机外部去实现,实现这个动作的代码模块称为“类加载器”。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性。如果两个类来源于同一个 Class 文件,只要加载它们的类加载器不同,那么这两个类就必定不相等。这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

3、类加载器分类

显式加载 vs 隐式加载

class 文件的显式加载与隐式加载的方式是指 JVM 加载 class 文件到内存的方式。

1)显式加载指的是在代码中通过调用 ClassLoader 加载 class 对象,如直接使用 Class.forName(name) 或 this.getClass().getClassLoader().loadClass() 加载 class 对象。
2)隐式加载则是不直接在代码中调用 ClassLoader 的方法加载 class 对象,而是通过虚拟机自动加载到内存中,如在加载某个类的 class 文件时,该类的 class 文件中引用了另外一个类的对象,此时额外引用的类将通过 JVM 自动加载到内存中。

// 隐式加载 
User user=new User(); 
// 显式加载,并初始化 
Class clazz=Class.forName("com.test.java.User"); 
// 显式加载,但不初始化
ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");

(1)引导类加载器(BootStrap ClassLoader)

由 C++ 实现,是虚拟机自身的一部分。主要加载的是 JVM 自身需要的类,负责将 <JAVA_HOME>/lib 路径下的核心类库或 -Xbootclasspath 参数指定的路径下的jar包加载到虚拟机内存中(注意:由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把 jar 包丢到 lib 目录下也是没有作用的,出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类)。
BootStrap ClassLoader 并不继承自 Java.lang.ClassLoader,没有父加载器

(2)扩展类加载器(Extension ClassLoader)

这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,由Java语言实现的,是Launcher的静态内部类,它负责加载 <JAVA_HOME>\lib\ext 目录中的或者由系统变量 -Djava.ext.dir 所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
Extension ClassLoader 派生于 ClassLoader 类,父类加载器为启动类加载器

(3)系统类加载器(AppClassLoader)

也称应用程序加载器,是指 Sun 公司实现的 sun.misc.Launcher$AppClassLoader。它负责加载系统类路径 java -classpath 或 -D java.class.path 指定路径下的类库,也就是我们经常用到的 classpath 路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过 ClassLoader.getSystemClassLoader() 方法可以获取到该类加载器
AppClassLoader 派生于ClassLoader类,父类加载器为扩展类加载器
开发者可以直接使用系统类加载器,当应用程序没有自定义类加载器时,默认采用该类加载器

继承关系:
在这里插入图片描述

4、类加载机制的基本特征——双亲委派模型

双亲委派模型(Pattern Delegation Model)要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器这里父子关系通常是子类通过组合关系而不是继承关系来复用父加载器的代码

4.1 工作过程

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
在这里插入图片描述

4.2 双亲委派模式优势

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

4.3 破坏双亲委派模型

双亲委派模型主要出现过3次较大规模“被破坏”的情况。

(1)第一次破坏是因为类加载器和抽象类java.lang.ClassLoader在JDK1.0就存在的,而双亲委派模型在JDK1.2之后才被引入,为了兼容已经存在的用户自定义类加载器,引入双亲委派模型时做了一定的妥协:在java.lang.ClassLoader中引入了一个findClass()方法,在此之前,用户去继承java.lang.Classloader的唯一目的就是重写loadClass()方法。JDK1.2之后不提倡用户去覆盖loadClass()方法,而是把自己的类加载逻辑写到findClass()方法中,如果loadClass()方法中如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型规则的。
(2)第二次破坏是因为模型自身的缺陷,现实中存在这样的场景:基础的类加载器需要求调用用户的代码,而基础的类加载器可能不认识用户的代码。为此,Java设计团队引入的设计时“线程上下文类加载器(Thread Context ClassLoader)”。这样可以通过父类加载器请求子类加载器去完成类加载动作。已经违背了双亲委派模型的一般性原则。
(3)第三次破坏是由于用户对程序动态性的追求导致的。这里所说的动态性是指:“代码热替换”、“模块热部署”等等比较热门的词。说白了就是希望应用程序能够像我们的计算机外设一样,接上鼠标、U盘不用重启机器就能立即使用。OSGi是当前业界“事实上”的Java模块化标准,OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。

5、ClassLoader 使用

ClassLoader 类是一个抽象类,扩展类加载器和应用程序类加载器都继承自 ClassLoader(注:启动类加载器不是继承自 ClassLoader)。

(1)ClassLoader 的方法:
在这里插入图片描述
(2)获取 ClassLoader 的途径

// 获取当前 ClassLoader
this.getClass.getClassLoader(); 
// 获取当前线程上下文的 ClassLoader
Thread.currentThread().getContextClassLoader()
// 获取系统 ClassLoader
ClassLoader.getSystemClassLoader()
// 获取调用者的ClassLoader
DriverManager.getCallerClassLoader()

(3)用 ClassLoader 载入资源的几种方法

所有资源都通过ClassLoader载入到JVM里,那么在载入资源时当然可以使用ClassLoader,只是对于不同的资源还可以使用一些别的方式载入。

1)载入类

假设有类 A 和类 B,A 在其方法里需要实例化 B,载入类可能的方法有3种。对于载入类的情况,用户需要知道 B 类的完整名字(包括包名,例如"com.alexia.B")
(1)使用 Class 静态方法 Class.forName

Class cls = Class.forName("com.alexia.B");   
B b = (B)cls.newInstance(); 

(2)使用 ClassLoader

 /* Step 1. Get ClassLoader */  
 ClassLoader cl = this.getClass.getClassLoader();;  // 如何获得ClassLoader参考1      
 /* Step 2. Load the class */   
 Class cls = cl.loadClass("com.alexia.B"); // 使用第一步得到的ClassLoader来载入B  
 /* Step 3. new instance */   
 B b = (B)cls.newInstance(); 	// 有B的类得到一个B的实例  `

(3)直接new

B b = new B(); 

2)载入文件

假设在com.alexia.A类里想读取文件夹 /com/alexia/config 里的文件sys.properties,读取文件可以通过绝对路径或相对路径,绝对路径很简单,在Windows下以盘号开始,在Unix下以"/"开始。对于相对路径,其相对值是相对于ClassLoader的,因为ClassLoader是一棵树,所以这个相对路径和ClassLoader树上的任何一个ClassLoader相对比较后可以找到文件,那么文件就可以找到。文件有以下三种加载方式:
(1)直接用IO流读取

File f = new File("C:/test/com/aleixa/config/sys.properties"); // 使用绝对路径   
//File f = new File("com/alexia/config/sys.properties"); // 使用相对路径   
InputStream is = new FileInputStream(f);   

(2)使用 ClassLoader

InputStream is = null;  
is = this.getClass().getClassLoader().getResourceAsStream(  "com/alexia/config/sys.properties"); //方法1  
//	is = Thread.currentThread().getContextClassLoader().getResourceAsStream(  "com/alexia/config/sys.properties"); //方法2  
//	is = ClassLoader.getSystemResourceAsStream("com/alexia/config/sys.properties"); //方法3    

(3)使用 ResourceBundle

ResourceBundle bundle = ResourceBundle.getBoundle("com.alexia.config.sys");   

这种用法通常用来载入用户的配置文件

3)web资源的载入方式

在 web 应用里当然也可以使用 ClassLoader 来载入资源,但更常用的情况是使用 ServletContext,如下是 web 目录结构 :
在这里插入图片描述
用户程序通常在 classes 目录下,如果想读取 classes 目录里的文件,可以使用ClassLoader,如果想读取其他的文件,一般使用 ServletContext.getResource()
如果使用ServletContext.getResource(path)方法,路径必须以"/"开始,路径被解释成相对于ContextRoot的路径,此处载入文件的方法和ClassLoader不同,举例"/WEB-INF/web.xml","/download/WebExAgent.rar"

6、自定义类加载器

Java 默认 ClassLoader 只加载指定目录下的 class,如果需要动态加载类到内存,例如要从远程网络下来类的二进制然后调用这个类中的方法实现我的业务逻辑,如此就需要自定义 ClassLoader。

使用自定义加载类的场景:

隔离加载类
修改类加载的方式
扩展加载源
防止源码泄漏

自定义类加载器的步骤:

(1)继承 java.lang.ClassLoader

为什么要继承 ClassLoader 这个抽象类,而不继承 AppClassLoader 呢?因为它和 ExtClassLoader 都是 Launcher 的静态内部类,其访问权限是缺省的包访问权限。

static class AppClassLoader extends URLClassLoader{...} 

(2)重写父类的 findClass() 方法

JDK 的 loadCalss() 方法在所有父类加载器无法加载的时候,会调用本身的 findClass()
方法来进行类加载,因此我们只需重写 findClass() 方法找到类的二进制数据即可
1)首先是需要被加载的简单类:

// 存放于D盘根目录 
public class Test {

    public static void main(String[] args) {
        System.out.println("Test类已成功加载运行!");
        ClassLoader classLoader = Test.class.getClassLoader();
        System.out.println("加载我的classLoader:" + classLoader);
        System.out.println("classLoader.parent:" + classLoader.getParent());
    }
} 

将上述类使用 javac -encoding utf8 Test.java 命令编译成 Test.class 文件。

2)类加载器代码如下:

import java.io.*;

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 加载D盘根目录下指定类名的class
        String clzDir = "D:\\" + File.separatorChar
                + name.replace('.', File.separatorChar) + ".class";
        byte[] classData = getClassData(clzDir);

        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String path) {
        try (InputStream ins = new FileInputStream(path);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()
        ) {

            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
 } 

3)使用类加载器加载调用 Test 类:

public class MyClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 指定类加载器加载调用
        MyClassLoader classLoader = new MyClassLoader();
        classLoader.loadClass("Test").getMethod("test").invoke(null);
    } 
} 

7、沙箱安全机制

有自定义 String 类,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 jdk 自带的文件(rt.jar包中java\lang\String.class),报错信息说没有 main 方法,就是因为加载的是 rt.jar 包中的 string 类。这样可以保证对 java 核心源代码的保护,这就是沙箱安全机制

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值