Java类加载器干嘛的_Java类加载器原理

Java类加载器负责根据类的全名从文件或网络等不同来源加载类。类加载遵循双亲委派模型,先由父类加载器尝试加载,失败后再由子类加载器加载。双亲委派模型避免了类的冲突,保证了类的唯一性。自定义类加载器可扩展加载路径或实现特定加载逻辑,如加密解密类文件。当类找不到时,会抛出ClassNotFoundException,而NoClassDefFoundError通常在类名校验失败时抛出。
摘要由CSDN通过智能技术生成

ClassLoader介绍

类加载器是负责加载类的一个对象,ClassLoader是一个抽象类。最常见的加载策略是根据的类的全名,然后找到这个类的class文件,然后从文件读取这个类的数据加载到JVM。每个类都能通过getClassLoader方法获取加载这个类的类加载器。

数组类的类对象不是由类加载器创建的,而是根据Java运行时的需要自动创建的。 Class#getClassLoader()返回的数组类的类加载器与其元素类型的类加载器相同;如果元素类型是基本类型,则数组类没有类加载器。

应用程序可以继承 ClassLoader来自定义自己的累加器 ,以便扩展Java虚拟机动态加载类的方式。

类使用双亲委派模型来加载类,ClassLoader的每个实例都有一个父类加载器。当一个类加载加载类时,它会把这个类加载请求委派给它的父类加载器来加载。JVM的内置类加载器是"bootstrap class loader"”,它的parent为null,但可以作为ClassLoader实例的父级。

通常,Java虚拟机以与平台相关的方式从本地文件系统加载类。例如,在UNIX系统上,虚拟机从CLASSPATH环境变量定义的目录中加载类。 但是,某些类可能不是来自文件,它们可能来自其他来源,例如网络,或者它们可以由应用程序构建。方法defineClass(String,byte [],int,int)将字节数组转换为类Class的实例,也可以使用 Class#newInstance创建此新定义的类的实例。

例如,应用程序可以创建网络类加载器以从服务器下载类文件。示例代码可能如下所示:

ClassLoader loader = new NetworkClassLoader(host,port);

Object main = loader.loadClass("Main", true).newInstance();

NetworkClassLoader必须实现 findClass方法来加载类,以及自定义加载动作通过loadClassData()方法从网络加载类。一旦下载了构成类的字节,它应该使用defineClass方法 来创建一个类实例。示例实现是:

class NetworkClassLoader extends ClassLoader {

* String host;

* int port;

*

* public Class findClass(String name) {

* byte[] b = loadClassData(name);

* return defineClass(name, b, 0, b.length);

* }

*

* private byte[] loadClassData(String name) {

* // load the class data from the connection

*

* }

* }

findClass方法参数name要符合JVM定义的规范,以下这些都是合法的类名称:

1 "java.lang.String"

2 "javax.swing.JSpinner$DefaultEditor"

3 "java.security.KeyStore$Builder$FileBuilder$1"

4 "java.net.URLClassLoader$3$1"

何时出发类加载动作?

类加载的触发可以分为隐式加载和显示加载。

隐式加载

隐式加载包括以下几种情况:

遇到new、getstatic、putstatic、invokestatic这4条字节码指令时

对类进行反射调用时

当初始化一个类时,如果其父类还没有初始化,优先加载其父类并初始化

虚拟机启动时,需指定一个包含main函数的主类,优先加载并初始化这个主类

显示加载

显示加载包含以下几种情况:

通过ClassLoader的loadClass方法

通过Class.forName

通过ClassLoader的findClass方法

被加载的类存放在哪里?

JDK8之前会加载到内存中的方法区。

从JDK8到现在为止,会加载到元数据区。

都有哪些ClassLoader?

整个JVM平台提供三类ClassLoader。

Bootstrap ClassLoader

加载JVM自身工作需要的类,它由JVM自己实现。它会加载JAVA_HOME/jre/lib下的文件

ExtClassLoader

它是JVM的一部分,由sun.misc.Launcher.ExtClassLoader实现,他会加载JAVA_HOME/jre/lib/ext目录中的文件(或由System.getProperty("java.ext.dirs")所指定的文件)。

AppClassLoader

应用类加载器,我们工作中接触最多的也是这个类加载器,它由sun.misc.Launcher.AppClassLoader实现。它加载由System.getProperty("java.class.path")指定目录下的文件,也就是我们通常说的classpath路径。

双亲委派模型

双亲委派模型原理

从JDK1.2之后,类加载器引入了双亲委派模型,其模型图如下:

c2a8cb813346

其中,两个用户自定义类加载器的父加载器是AppClassLoader,AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader是没有父类加载器的,在代码中,ExtClassLoader的父类加载器为null。BootstrapClassLoader也并没有子类,因为他完全由JVM实现。

双亲委派模型的原理是:当一个类加载器接收到类加载请求时,首先会请求其父类加载器加载,每一层都是如此,当父类加载器无法找到这个类时(根据类的全限定名称),子类加载器才会尝试自己去加载。

此模型解决的问题

为什么要使用双亲委派模型呢?它可以解决什么问题呢?

亲委派模型是JDK1.2之后引入的。根据双亲委派模型原理,可以试想,没有双亲委派模型时,如果用户自己写了一个全限定名为java.lang.Object的类,并用自己的类加载器去加载,同时BootstrapClassLoader加载了rt.jar包中的JDK本身的java.lang.Object,这样内存中就存在两份Object类了,此时就会出现很多问题,例如根据全限定名无法定位到具体的类。

有了双亲委派模型后,所有的类加载操作都会优先委派给父类加载器,这样一来,即使用户自定义了一个java.lang.Object,但由于BootstrapClassLoader已经检测到自己加载了这个类,用户自定义的类加载器就不会再重复加载了。

所以,双亲委派模型能够保证类在内存中的唯一性。

双亲委派模型实现原理

下面从源码的角度看一下双亲委派模型的实现。

JVM在加载一个class时会先调用classloader的loadClassInternal方法,该方法源码如下

// This method is invoked by the virtual machine to load a class.

private Class> loadClassInternal(String name)

throws ClassNotFoundException

{

// For backward compatibility, explicitly lock on 'this' when

// the current class loader is not parallel capable.

if (parallelLockMap == null) {

synchronized (this) {

return loadClass(name);

}

} else {

return loadClass(name);

}

}

该方法里面做的事儿就是调用了loadClass方法,loadClass方法的实现如下

protected Class> loadClass(String name, boolean resolve)

throws ClassNotFoundException

{

synchronized (getClassLoadingLock(name)) {

// First, check if the class has already been loaded

// 先查看这个类是否已经被自己加载了

Class> c = findLoadedClass(name);

if (c == null) {

long t0 = System.nanoTime();

try {

// 如果有父类加载器,先委派给父类加载器来加载

if (parent != null) {

c = parent.loadClass(name, false);

} else {

// 如果父类加载器为null,说明ExtClassLoader也没有找到目标类,则调用BootstrapClassLoader来查找

c = findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {

// ClassNotFoundException thrown if class not found

// from the non-null parent class loader

}

// 如果都没有找到,调用findClass方法,尝试自己加载这个类

if (c == null) {

// If still not found, then invoke findClass in order

// to find the class.

long t1 = System.nanoTime();

c = findClass(name);

// this is the defining class loader; record the stats

sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);

sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

sun.misc.PerfCounter.getFindClasses().increment();

}

}

if (resolve) {

resolveClass(c);

}

return c;

}

}

源码中已经给出了几个关键步骤的说明。

代码中调用BootstrapClassLoader的地方实际是调用的native方法。

由此可见,双亲委派模型实现的核心就是这个loadClass方法。

实现自己的类加载器

类加载器的作用

类加载器有啥作用呢?我们再回到上面的源码。

从上文我们知道JVM通过loadClass方法来查找类,所以,他的第一个作用也是最重要的:在指定的路径下查找class文件(各个类加载器的扫描路径在上文已经给出)。

然后,当父类加载器都说没有加载过目标类时,他会尝试自己加载目标类,这就调用了findClass方法,可以看一下findClass方法的定义:

protected Class> findClass(String name) throws ClassNotFoundException {

throw new ClassNotFoundException(name);

}

可以发现他要求返回一个Class对象实例,这里我通过一个实现类sun.rmi.rmic.iiop.ClassPathLoader来说明一下findClass都干了什么。

protected Class findClass(String var1) throws ClassNotFoundException {

// 从指定路径加载指定名称的class的字节流

byte[] var2 = this.loadClassData(var1);

// 通过ClassLoader的defineClass来创建class对象实例

return this.defineClass(var1, var2, 0, var2.length);

}

他做的事情在注释中已经给出,可以看到,最终是通过defineClass方法来实例化class对象的。

另外可以发现,class文件字节的获取和处理我们是可以控制的。所以,第二个作用:我们可以在字节流解析这一步做一些自定义的处理。 例如,加解密。

protected final Class> defineClass(String name, byte[] b, int off, int len)

throws ClassFormatError

{

return defineClass(name, b, off, len, null);

}

被final掉了,没办法覆写,所以这里看似不能做什么事儿了。

小结一下:

通过loadClass在指定的路径下查找文件。

通过findClass方法解析class字节流,并实例化class对象。

什么时候需要自己实现类加载器

当JDK提供的类加载器实现无法满足我们的需求时,才需要自己实现类加载器。

根据上述类加载器的作用,可能有以下几个场景需要自己实现类加载器

当需要在自定义的目录中查找class文件时(或网络获取)

class被类加载器加载前的加解密(代码加密领域)

如何实现自己的类加载器

接下来,实现一个在自定义class类路径中查找并加载class的自定义类加载器。

package com.lordx.sprintbootdemo.classloader;

import java.io.ByteArrayOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.InputStream;

/**

* 自定义ClassLoader

* 功能:可自定义class文件的扫描路径

* @author zhiminxu

*/

// 继承ClassLoader,获取基础功能

public class TestClassLoader extends ClassLoader {

// 自定义的class扫描路径

private String classPath;

public TestClassLoader(String classPath) {

this.classPath = classPath;

}

// 覆写ClassLoader的findClass方法

protected Class> findClass(String name) throws ClassNotFoundException {

// getDate方法会根据自定义的路径扫描class,并返回class的字节

byte[] classData = getDate(name);

if (classData == null) {

throw new ClassNotFoundException();

} else {

// 生成class实例

return defineClass(name, classData, 0, classData.length);

}

}

private byte[] getDate(String name) {

// 拼接目标class文件路径

String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";

try {

InputStream is = new FileInputStream(path);

ByteArrayOutputStream stream = new ByteArrayOutputStream();

byte[] buffer = new byte[2048];

int num = 0;

while ((num = is.read(buffer)) != -1) {

stream.write(buffer, 0 ,num);

}

return stream.toByteArray();

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

}

使用自定义的类加载器

package com.lordx.sprintbootdemo.classloader;

public class MyClassLoader {

public static void main(String[] args) throws ClassNotFoundException {

// 自定义class类路径

String classPath = "/Users/zhiminxu/developer/classloader";

// 自定义的类加载器实现:TestClassLoader

TestClassLoader testClassLoader = new TestClassLoader(classPath);

// 通过自定义类加载器加载

Class> object = testClassLoader.loadClass("ClassLoaderTest");

// 这里的打印应该是我们自定义的类加载器:TestClassLoader

System.out.println(object.getClassLoader());

}

}

跟ClassLoader相关的几个异常

ClassNotFoundException

这个异常,相信大家经常遇到。

那么,到底啥原因导致抛出这个异常呢?

看一下ClassLoader的源码,在JVM调用loadClassInternal的方法中,就会抛出这个异常。

其声明如下:

// This method is invoked by the virtual machine to load a class.

private Class> loadClassInternal(String name)

throws ClassNotFoundException

{

// For backward compatibility, explicitly lock on 'this' when

// the current class loader is not parallel capable.

if (parallelLockMap == null) {

synchronized (this) {

return loadClass(name);

}

} else {

return loadClass(name);

}

}

这里面loadClass方法会抛出这个异常,再来看loadClass方法

public Class> loadClass(String name) throws ClassNotFoundException {

return loadClass(name, false);

}

调用了重写的loadClass方法

protected Class> loadClass(String name, boolean resolve)

throws ClassNotFoundException

{

synchronized (getClassLoadingLock(name)) {

// First, check if the class has already been loaded

// 查看findLoadedClass的声明,没有抛出这个异常

Class> c = findLoadedClass(name);

if (c == null) {

long t0 = System.nanoTime();

try {

if (parent != null) {

// 这里会抛,但同样是调loadClass方法,无需关注

c = parent.loadClass(name, false);

} else {

c = findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {

// catch住没有抛,因为要在下面尝试自己获取class

// 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

// to find the class.

long t1 = System.nanoTime();

// 关键:这里会抛

c = findClass(name);

// this is the defining class loader; record the stats

sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);

sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

sun.misc.PerfCounter.getFindClasses().increment();

}

}

if (resolve) {

resolveClass(c);

}

return c;

}

}

再来看findClass方法

/**

* Finds the class with the specified binary name.

* This method should be overridden by class loader implementations that

* follow the delegation model for loading classes, and will be invoked by

* the {@link #loadClass loadClass} method after checking the

* parent class loader for the requested class. The default implementation

* throws a ClassNotFoundException.

*

* @param name

* The binary name of the class

*

* @return The resulting Class object

*

* @throws ClassNotFoundException

* If the class could not be found

*

* @since 1.2

*/

protected Class> findClass(String name) throws ClassNotFoundException {

throw new ClassNotFoundException(name);

}

果然这里抛出的,注释中抛出这个异常的原因是说:当这个class无法被找到时抛出。这也就是为什么在上面自定义的类加载器中,覆写findClass方法时,如果没有找到class要抛出这个异常的原因。

至此,这个异常抛出的原因就明确了:在双亲委派模型的所有相关类加载器中,目标类在每个类加载器的扫描路径中都不存在时,会抛出这个异常。

NoClassDefFoundError

这也是个经常会碰到的异常,而且不熟悉的同学可能经常搞不清楚什么时候抛出ClassNotFoundException,什么时候抛出NoClassDefFoundError。

我们还是到ClassLoader中搜一下这个异常,可以发现在defineClass方法中可能抛出这个异常,defineClass方法源码如下:

/**

* ... 忽略注释和参数以及返回值的说明,直接看异常声明

*

* @throws ClassFormatError

* If the data did not contain a valid class

*

* 在这里。由于NoClassDefFoundError是Error下的,所以不用显示throws

* @throws NoClassDefFoundError

* If name is not equal to the binary

* name of the class specified by b

*

* @throws IndexOutOfBoundsException

* If either off or len is negative, or if

* off+len is greater than b.length.

*

* @throws SecurityException

* If an attempt is made to add this class to a package that

* contains classes that were signed by a different set of

* certificates than this class, or if name begins with

* "java.".

*/

protected final Class> defineClass(String name, byte[] b, int off, int len,

ProtectionDomain protectionDomain)

throws ClassFormatError

{

protectionDomain = preDefineClass(name, protectionDomain);

String source = defineClassSourceLocation(protectionDomain);

Class> c = defineClass1(name, b, off, len, protectionDomain, source);

postDefineClass(c, protectionDomain);

return c;

}

继续追踪里面的方法,可以发现是preDefineClass方法抛出的,这个方法源码如下:

/* Determine protection domain, and check that:

- not define java.* class,

- signer of this class matches signers for the rest of the classes in

package.

*/

private ProtectionDomain preDefineClass(String name,

ProtectionDomain pd)

{

// 这里显示抛出,可发现是在目标类名校验不通过时抛出的

if (!checkName(name))

throw new NoClassDefFoundError("IllegalName: " + name);

// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias

// relies on the fact that spoofing is impossible if a class has a name

// of the form "java.*"

if ((name != null) && name.startsWith("java.")) {

throw new SecurityException

("Prohibited package name: " +

name.substring(0, name.lastIndexOf('.')));

}

if (pd == null) {

pd = defaultDomain;

}

if (name != null) checkCerts(name, pd.getCodeSource());

return pd;

}

这个校验的源码如下

// true if the name is null or has the potential to be a valid binary name

private boolean checkName(String name) {

if ((name == null) || (name.length() == 0))

return true;

if ((name.indexOf('/') != -1)

|| (!VM.allowArraySyntax() && (name.charAt(0) == '[')))

return false;

return true;

}

所以,这个异常抛出的原因是:类名校验未通过。

但这个异常其实不止在ClassLoader中抛出,其他地方,例如框架中,web容器中都有可能抛出,还要具体问题具体分析。

参考

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值