摘要

本博文的主要介绍JVM 的类加载机制,其实,JVM 的类加载机制和Java 的类加载机制类似,但 JVM 的类加载过程稍有些复杂。JVM 通过加载 .class 文件,能够将其中的字节码解析成操作系统机器码。那这些文件是怎么加载进来的呢?又有哪些约定?接下来我们就详细介绍 JVM 的类加载机制,同时介绍三个实际的应用场景。另外还将介绍双亲委派机制,该机制在平常的工作中也有大量的相关应用,博文将理论联系实践综合分析这些问题,帮助大家更好的理解的JVM的类在的加载原理。

一、类加载过程

现实中并不是说,我把一个文件修改成 .class 后缀,就能够被 JVM 识别。类的加载过程非常复杂,主要有这几个过程:加载、验证、准备、解析、初始化。这些术语很多地方都出现过,我们不需要死记硬背,而应该要了解它背后的原理和要做的事情。

JVM——类加载(ClassLoader)系统与字节码原理_加载

1.1 加载

加载的主要作用是将外部的 .class 文件,加载到 Java 的方法区内,你可以回顾一下我们在上一课时讲的内存区域图。加载阶段主要是找到并加载类的二进制数据,比如从 jar 包里或者 war 包里找到它们。

1.2 验证

肯定不能任何 .class 文件都能加载,那样太不安全了,容易受到恶意代码的攻击。验证阶段在虚拟机整个类加载过程中占了很大一部分,不符合规范的将抛出 java.lang.VerifyError 错误。像一些低版本的 JVM,是无法加载一些高版本的类库的,就是在这个阶段完成的。

1.3 准备

从这部分开始,将为一些类变量分配内存,并将其初始化为默认值。此时,实例对象还没有分配内存,所以这些动作是在方法区上进行的。

code-snippet 1:
public class A {
static int a ;
public static void main(String[] args) {
System.out.println(a);
}
}
code-snippet 2:
public class A {
public static void main(String[] args) {
int a ;
System.out.println(a);
}
}

为什么会有这种区别呢?

这是因为局部变量不像类变量那样存在准备阶段。类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值(也可以是指定值);另外一次在初始化阶段,赋予程序员定义的值。

因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不一样了,如果没有给它赋初始值,是不能使用的。

1.4 解析

解析在类加载中是非常非常重要的一环,是将符号引用替换为直接引用的过程。这句话非常的拗口,其实理解起来也非常的简单。符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。

直接引用的对象都存在于内存中,你可以把通讯录里的女友手机号码,类比为符号引用,把面对面和你吃饭的人,类比为直接引用。

解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段都做了哪些工作呢?大体可以分为:

  • 类或接口的解析
  • 类方法解析
  • 接口方法解析
  • 字段解析

我们来看几个经常发生的异常,就与这个阶段有关。

  • java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。
  • java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。
  • java.lang.NoSuchMethodError 找不到相关方法时的错误。

解析过程保证了相互引用的完整性,把继承与组合推进到运行时。

1.5 初始化

如果前面的流程一切顺利的话,接下来该初始化成员变量了,到了这一步,才真正开始执行一些字节码。

public class A {
static int a = 0 ;
static {
a = 1;
b = 1;
}
static int b = 0;
public static void main(String[] args) {
System.out.println(a);
System.out.println(b);
}
}

结果是 1 0。a 和 b 唯一的区别就是它们的 static 代码块的位置。这就引出一个规则:static 语句块,只能访问到定义在 static 语句块之前的变量。所以下面的代码是无法通过编译的。

static {
b = b + 1;
}
static int b = 0;

JVM 会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕。所以,JVM 第一个被执行的类初始化方法一定是 java.lang.Object。另外,也意味着父类中定义的 static 语句块要优先于子类的。

public class A {
static {
System.out.println("1");
}
public A(){
System.out.println("2");
}
}
public class B extends A {
static{
System.out.println("a");
}
public B(){
System.out.println("b");
}
public static void main(String[] args){
A ab = new B();
ab = new B();
}
}
1
a
2
b
2
b

你可以看下这张图。其中 static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,所以上面的 static 代码块只会执行一次,它对应的是 方法。

而对象初始化就不一样了。通常,我们在 new 一个新对象的时候,都会调用它的构造方法,就是 ,用来初始化对象的属性。每次新建对象的时候,都会执行。所以,上面代码的 static 代码块只会执行一次,对象的构造方法执行两次。再加上继承关系的先后原则,不难分析出正确结果。

JVM——类加载(ClassLoader)系统与字节码原理_jvm_02

二、类加载器

整个类加载过程任务非常繁重,虽然这活儿很累,但总得有人干。类加载器做的就是上面 5 个步骤的事。如果你在项目代码里,写一个 java.lang 的包,然后改写 String 类的一些行为,编译后,发现并不能生效。JRE 的类当然不能轻易被覆盖,否则会被别有用心的人利用,这就太危险了。那类加载器是如何保证这个过程的安全性呢?其实,它是有着严格的等级制度的。

JVM——类加载(ClassLoader)系统与字节码原理_加载_03

1.1 四种类加载器

1.1.1 Bootstrap ClassLoader

这是加载器中的大 Boss,任何类的加载行为,都要经它过问。它的作用是加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。当然这些 jar 包的路径是可以指定的,-Xbootclasspath 参数可以完成指定操作。这个加载器是 C++ 编写的,随着 JVM 启动。

1.1.2 Extention ClassLoader

扩展类加载器,主要用于加载 lib/ext 目录下的 jar 包和 .class 文件。同样的,通过系统变量 java.ext.dirs 可以指定这个目录。这个加载器是个 Java 类,继承自 URLClassLoader。

1.1.3 App ClassLoader

这是我们写的 Java 类的默认加载器,有时候也叫作 System ClassLoader。一般用来加载 classpath 下的其他所有 jar 包和 .class 文件,我们写的代码,会首先尝试使用这个类加载器进行加载。

1.1.4 Custom ClassLoader

自定义加载器,支持一些个性化的扩展功能。

1.2 双亲委派机制

双亲委派机制的意思是除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。打个比方。有一个家族,都是一些听话的孩子。孙子想要买一块棒棒糖,最终都要经过爷爷过问,如果力所能及,爷爷就直接帮孙子买了。但你有没有想过,“类加载的双亲委派机制,双亲在哪里?明明都是单亲?”我们还是用一张图来讲解。可以看到,除了启动类加载器,每一个加载器都有一个parent,并没有所谓的双亲。但是由于翻译的问题,这个叫法已经非常普遍了,一定要注意背后的差别。

我们可以翻阅 JDK 代码的 ClassLoader#loadClass 方法,来看一下具体的加载过程。和我们描述的一样,它首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。同时,我们也注意到,这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效。

这个模型的好处在于 Java 类有了一种优先级的层次划分关系。比如 Object 类,这个毫无疑问应该交给最上层的加载器进行加载,即使是你覆盖了它,最终也是由系统默认的加载器进行加载的。如果没有双亲委派模型,就会出现很多个不同的 Object 类,应用程序会一片混乱。

JVM——类加载(ClassLoader)系统与字节码原理_jvm_04

JVM——类加载(ClassLoader)系统与字节码原理_类加载器_05

 

1.2.1 一些自定义加载器

下面我们就来聊一聊可以打破双亲委派机制的一些案例。为了支持一些自定义加载类多功能的需求,Java 设计者其实已经作出了一些妥协。

类加载子系统作用

  • 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
  • 加载的类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

类加载器ClassLoader角色

类的装载过程

  • 通过一个类的全限定明获取定义此类的二进制字节流;
  • 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据;
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

补充:加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络获取,典型场景web Applet)
  • 从zip压缩包中读取,成为日后jar、war格式的基础
  • 运行时计算生成,使用最多的是:动态代理技术
  • 由其他文件生成,典型场景: JSP应用
  • 从专有数据库中提取.class文件,比较少见
  • 从加密文件中获取,典型的防class文件被反编译的保护捞施

类的链接过程

验证:

  • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
  • 主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。

准备

  • 为类变量分配内存并且设置该类变量的默认初始值,即零值;
  • 这里不包含用final修饰的sttic,因为final在编译的时候就会分配了,准备阶段会显式初始化;
  • 之类不会为实例变量分配初始化,类变量会分配在方法去中,而实例变量是会随着对象一起分配到java堆中。

解析

  • 将常量池内的符号引用转换为直接引用的过程。
  • 事实上,解析操作网晚会伴随着jvm在执行完初始化之后再执行
  • 符号引用就是一组符号来描述所引用的目标。符号应用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

初始化

  • 初始化阶段就是执行类构造器方法clinit()的过程。
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。 ​​我们注意到如果没有静态变量c,那么字节码文件中就不会有clinit方法​

JVM——类加载(ClassLoader)系统与字节码原理_加载_06

  •  构造器方法中指令按语句在源文件中出现的顺序执行

JVM——类加载(ClassLoader)系统与字节码原理_jvm_07

  • clinit()不同于类的构造器。(关联:构造器是虚拟机视角下的init())
  • 若该类具有父类,jvm会保证子类的clinit()执行前,父类的clinit()已经执行完毕

JVM——类加载(ClassLoader)系统与字节码原理_jvm_08

  •  虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。

JVM——类加载(ClassLoader)系统与字节码原理_jvm_09

类加载器分类

  • JVM支持两种类型的加载器,分别为引导类加载器(BootStrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
  • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
  • 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有三个,如下所示:

JVM——类加载(ClassLoader)系统与字节码原理_jvm_10

自定义类与核心类库的加载器

  • 对于用户自定义类来说:使用系统类加载器AppClassLoader进行加载
  • java核心类库都是使用引导类加载器BootStrapClassLoader加载的
/**
* ClassLoader加载
*/
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

//获取其上层 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6

//获取其上层 获取不到引导类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(bootStrapClassLoader);//null

//对于用户自定义类来说:使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

//String 类使用引导类加载器进行加载的 -->java核心类库都是使用引导类加载器加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null

}
}

虚拟机自带的加载器

  • ①启动类加载器(引导类加载器,BootStrap ClassLoader)
  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部
  • 它用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承自java.lang.ClassLoader,没有父加载器
  • 加载拓展类和应用程序类加载器,并指定为他们的父加载器
  • 处于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类
  • ②拓展类加载器(Extension ClassLoader)
  • java语言编写 ,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载
  • ③应用程序类加载器(系统类加载器,AppClassLoader)
  • java语言编写, 由sun.misc.Launcher$AppClassLoader实现。
  • 派生于ClassLoader类
  • 父类加载器为拓展类加载器
  • 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
  • 该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
/**
* 虚拟机自带加载器
*/
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("********启动类加载器*********");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
//获取BootStrapClassLoader能够加载的api路径
for (URL e:urls){
System.out.println(e.toExternalForm());
}

//从上面的路径中随意选择一个类 看看他的类加载器是什么
//Provider位于 /jdk1.8.0_171.jdk/Contents/Home/jre/lib/jsse.jar 下,引导类加载器加载它
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//null

System.out.println("********拓展类加载器********");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")){
System.out.println(path);
}
//从上面的路径中随意选择一个类 看看他的类加载器是什么:拓展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@4dc63996
}
}

用户自定义类加载器

用户自定义类加载器的好处

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

用户自定义类加载器实现步骤:

  • 1.开发人员可以通过继承抽象类java.lang.classLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
  • 2.在JDK1.2之前,在自定义类加载器时,总会去继承classLoader类并重写loadclass ()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass ()方法,而是建议把自定义的类加载逻辑写在findclass ()方法中。
  • 3.在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findclass ()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。。

ClassLoader的常用方法及获取方法

JVM——类加载(ClassLoader)系统与字节码原理_jvm_11

ClassLoader继承关系

JVM——类加载(ClassLoader)系统与字节码原理_加载器_12

类加载器的必要性

一般情况下,Java开发人员并不需要在程序中显式地使用类加载器,但是了解类加载器的加载机制却显得至关重要。从以下几个方面说:

  • 避免在开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时,手足无措。只有了解类加载器的 加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题
  • 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了。
  • 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。

命名空间

何为类的唯一性?

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
  • 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

类加载机制的基本特征

双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可以在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现。例如,Java中JNDI、JDBC、文件系统、Cipher等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。

可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。

单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。

类加载器之间的关系

Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}

try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}

Thread.currentThread().setContextClassLoader(this.loader);
  • ExtClassLoader的Parent类是null
  • AppClassLoader的Parent类是ExtClassLoader
  • 当前线程的ClassLoader是AppClassLoader

注意,这里的Parent类并不是Java语言意义上的继承关系,而是一种包含关系

ClassLoader源码解析

ClassLoader与现有类的关系:

JVM——类加载(ClassLoader)系统与字节码原理_加载_13

除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。

ClassLoader的主要方法

抽象类ClassLoader的主要方法:(内部没有抽象方法)

public final ClassLoader getParent()

返回该类加载器的超类加载器

public Class<?> loadClass(String name) throws ClassNotFoundException

加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回 ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。

protected Class<?> findClass(String name) throws ClassNotFoundException

查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。

  • 在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类。但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
  • 需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的。$\color{red}{一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
protected final Class<?> defineClass(String name, byte[] b,int off,int len)

根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。

  • 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,θ,classData.length);
}
}
protected final void resolveClass(Class<?> c)

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

protected final Class<?> findLoadedClass(String name)

查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例。这个方法是final方法,无法被修改。

private final ClassLoader parent;

它也是一个ClassLoader的实例,这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可能会将某些请求交予自己的双亲处理。

SecureClassLoader与URLClassLoader

接着SecureClassLoader扩展了ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联。

前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

JVM——类加载(ClassLoader)系统与字节码原理_jvm_14

ExtClassLoader与AppClassLoader

了解完URLClassLoader后接着看看剩余的两个类加载器,即拓展类加载器ExtClassLoader和系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。

sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的,其类主要类结构如下:

JVM——类加载(ClassLoader)系统与字节码原理_类加载器_15

我们发现ExtClassLoader并没有重写loadClass()方法,这足矣说明其遵循双亲委派模式,而AppClassLoader重载了loadClass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。

Class.forName()与ClassLoader.loadClass()

Class.forName()

  • Class.forName():是一个静态方法,最常用的是Class.forName(String className);
  • 根据传入的类的全限定名返回一个Class对象。该方法在将Class文件加载到内存的同时,会执行类的初始化。
Class.forName("com.atguigu.java.Helloworld");

ClassLoader.loadClass()

  • ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法。
  • 该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器。
Classloader cl = ......; cl.loadClass("com.atguigu.java.Helloworld");

博文参考