类加载机制
摘要:
Java开发人员经常会在程序中遇到java.lang.ClassNotFoundException这个异常,而这个异常背后涉及的Java知识点就是我们今天要讲的主题,Java的类加载机制。
一、加载的五大过程
JVM类加载机制分为五个部分:加载、验证、准备、解析、初始化。下面我们就从这五个方面来看一下JVM是怎么进行类加载的。
1、加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得从一个Calss文件获取,也可以从压缩包(jar、war)中读取,在运行时动态计算生成(动态代理),或者由其他文件生成(JSP文件装换为对应的Class类。)
2、验证
验证阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3、准备
准备阶段是正式为类变量分配内存并且设置类变量的初始值阶段,即在方法区中分配这些变量锁使用的内存空间。这里需要简要说明一下类变量的初始值赋值。
// 1、变量A在准备阶段过后的初始值是0不是1998,将A赋值为1998的put static 指令是在程序被编译后,存放于类构造器<client>方法之中。
public static int A = 1998;
// 2、这种情况在编译阶段会为B生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将B赋值为1998
public static final int B = 1998;
4、解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用。符号引用就是class文件中的:
- CONSTANT_Class_info
- CONSTANT_Field_info
- CONSTANT_Method_info
等类型的常量。
4.1 符号引用
符号引用与虚拟机实现的布局无关,引用的目标不一定要加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因此符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
4.2 直接引用
直接引用可以是指明目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经在内存中存在。
5、初始化
初始化阶段是类加载的最后一个阶段,前面的类加载阶段之后,处理在加载阶段可以自定义类加载器以外,其他操作都是由JVM主导。到了初始化阶段,才真正执行类中定义的Java程序代码。
5.1 类构造器<Client>
初始化阶段是执行类构造器<Client>方法的过程。<Client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成。虚拟机会保证子<Client>方法执行之前,父类的<Client>方法已经执行完毕,如果一个类中没有静态变量赋值也没有静态代码块,那么编译器可以不为这个类生成<Client>方法。
注意一下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化
- 定义对象数组,不会触发该类的初始化
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,这个参数是告诉虚拟机,是否要对类进行初始化。
- 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
二、类加载器
虚拟机设计团队将类加载动作放到JVM外部实现,以便于应用程序决定如何获取所需要的类,JVM提供了3种类加载
2.1 启动类加载器(BootStrap ClassLoader)
负责加载JAVA_HOME\lib目录中的类,或者通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
2.2 扩展类加载器(Extension ClassLoader)
负责加载JAVA_HOME\lib\ext目录中的类,或者通过java.ext.dirs系统变量指定路径中的类库。
2.3 应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上类库。
三、双亲委派模型
JVM通过双亲委派模型进行类加载,当然我们可以通过继承java.lang.ClassLoader实现自定义的类加载器。双亲委派的具体流程:当一个类收到了类加载请求,它首先不会尝试着自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都会传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(在这个类加载器路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载,采用双亲委派的好处是,一个类不管使用哪个加载器加载,最终都会依次委托到启动类加载器然后再逐级往下进行加载,这样就可以保证使用不同的类加载器加载最终得到的都是同一个Object对象。
四、 类加载机制存在的问题
4.1 类版本冲突
当类路径上存在同一个类的不同版本时,如果类加载器找到一个版本,则不再搜索加载另一个版本。
4.2 无法确定jar之间的依赖关系
现有的JAR标准中缺乏对jar文件之间依赖关系的支持,因此只有在运行时无法找到所需的类时,才会抛出java.lang.ClassNotFundException异常,这种运行时的异常,对开发人员来说是非常不友好的。
4.3 信息隐藏
如果一个jar在类路径上并且被加载,那么所有该jar中的公共类(public class)都会被加载,无法达到某些类不想加载则不被加载的效果,尽管在J2EE中改进了类加载机制,可以支持war包或者ear应用为单元进行加载,但是上述问题并没有很好的解决。
4.4 解决方案
OSGi是一个动态的Java模块(Module)系统,它规定了如何定义一个Module以及这些模块之间如何交互。每个OSGi的Java模块被称为一个bundle。每个bundle都有自己的类路径,可以精确规定哪些Java包和类可以被导出,需要导入哪些其它bundle的哪些类和包,并从而指明bundle之间的依赖关系。另外bundle可以被在运行时间安装,更新,卸载并且不影响整个应用。通过这种方式,分层的类加载机制变成了网状的类加载机制。在应用程序启动之前,OSGi就可以检测出来是否所有的依赖关系被满足,并在不满足时精确报出是哪些依赖关系没被满足。
五、OSGI
5.1 简介《OSGI》百度百科
OSGI(Open Service Gateway Initiative),是面向Java的动态模型系统,是Java动态化模块化系统的一系列规范。
5.2 动态改变构造
OSGI服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理,OSGI技术提供一种面向服务的架构,它能使这些组件动态发现对方。
5.3 模块化编程与热插拔
OSGI旨在实现Java程序的模块化编程提供基础条件,基于OSGI的程序和可能可以实现模块级的热插拔功能,当程序升级更新时,可只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是非常具有诱惑力的特性。
OSGI描述了一个很美好的模块化开发的目标,而且定了实现这个额目标的所需要服务与架构,同时也很成熟的框架实现支持。但并非所有的应用都适合采用OSGI作为基础架构,它在提供强大功能同时,也引入额外的复杂度,因为它不遵循类加载的双亲委派模型。(这个技术现在已经不是主流方向了,RPC才是!)
一、双亲委派机制的介绍与分析
JVM在加载类时,默认采用的是双亲委派机制,通俗讲,就是某个特定的类的类加载器在接收到加载类的请求时,首先将加载任务委托给父类加载器,依次递归(本质上是loadClass函数的递归调用),因此所有的请求最终都会传送到顶层的启动类加载器中。如果父类加载器可以完成这个加载请求,就成功返回;如果父类加载器无法完成加载请求,子类才会尝试自己加载。事实上,大多数情况下,越基础的类由上层加载器加载,因为这些类往往被用户代码经常调用(当然也存在基础类回调用户代码的情况,即破坏双亲委派机制的情形)。接下来我们从系统类加载器和扩展类加载器作为例子简单分析虚拟机默认的双亲委派机制。
ExtClassLoader(扩展类加载器)继承关系图:
AppClassLoader(系统类加载器)继承关系图:
从扩展类加载器和系统类加载器的继承关系图可以看出两者均是继承自java.lang.ClassLoader抽象类。因此介绍下ClassLoader中几个重要的方法:
// 加载指定权限定类名的二进制类型,供用户调用
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// 加载指定权限定类名的二进制类型,指定是否解析(resolve参数不一定能真正达到解析的效果),供继承用
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
// ToDo
}
// findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
// 定义类型,一般在findClass方法中读取到对应字节码后调用,final修饰,不嫩被继承
// JVM已经实现了具体的功能,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用即可。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError{
return defineClass(name, b, off, len, null);
}
在标准扩展类加载器ExtClassLoader和系统类加载器AppClassLoader以及两者的公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码中,均没有对java.lang.ClassLoader中的加载委派规则loadClass方法。因此我们可以从ClassLoader中的loadClass方法的源码中分析虚拟机默认的双亲委派机制的原理
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 根据类路径获取锁
synchronized (getClassLoadingLock(name)) {
// 判断该类是否已经被加载了
Class<?> c = findLoadedClass(name);
// 如果未被加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果存在父加载器,委托给父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 调用本地方法findBootstrapClass() BootStrap类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果加载器未加载
if (c == null) {
long t1 = System.nanoTime();
// 调用findClass方法,实则调用defineClass方法,通过自身加载器加载,如果无法加载则抛出ClassNotFundException
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;
}
}
由上面的代码我们引发一个思考,系统类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器,是否真的是这样呢?我们通过代码来测试一下:
/**
* <p>
* 测试类加载器之间的关系
* </p>
*
* @Author: Liziba
* @Date: 2021/5/31 21:14
*/
public class ClassLoaderRelationshipTest {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
}
输出结果:
通过上述的测试代码和输出结果,可以非常明确的看出ClassLoader.getSystemClassLoader()可以直接获取系统类加载器,而通过ClassLoader.getSystemClassLoader().getParent()可以看出系统类加载器的父类加载器是扩展类加载器,但是ClassLoader.getSystemClassLoader().getParent().getParent()的输出结果为null,是否说明我们的猜想存在问题呢?事实上,由于启动类加载器无法直接通过Java代码获取,他是在虚拟机中实现的,JVM默认采用null来代表启动类加载器。这个点我们可以通过ClassLoader的构造函数中知晓。
// parent设置为私有属性,并且未提供设置接口(Setter方法)
private final ClassLoader parent;
// getSystemClassLoader()方法为获取系统类加载器
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
// 强制设置父类加载器
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
// 在不指定明确的父类加载器时,设置parent加载器为系统类加载器
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains = Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
二、双亲委派机制示例
1、创建测试bean
package com.liziba.classloader.bean;
public class Person {
private String name;
}
2、当前工程中创建测试类
package com.liziba.classloader;
public class ClassLoaderRuleTest {
public static void main(String[] args) {
try {
// 查看Java 类路径
System.out.println(System.getProperty("java.class.path"));
// 调用加载当前类的类加载器加载测试类ClassLoaderRuleTest
Class<?> clazz = Class.forName("com.liziba.classloader.ClassLoaderRuleTest");
// 查看加载当前测试类的类加载器
System.out.println(clazz.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果:
3、将Person.class打包成test.jar复制到<JAVA_RUNTIME_HOME>\lib\ext目录下
再次运行代码测试,查看输出结果:sun.misc.Launcher$ExtClassLoader@7f31245a
由上可以证明前面说的双亲委派机制:系统类加载器在接收到加载请求时,首先将请求委派给父类加载器(标准扩展类加载器)进行加载,而在上面的示例中扩展类加载器抢先加载类Person.class的加载请求。
4、将test.jar复制到<JAVA_RUNTIME_HOME>\lib目录下
输出结果:
第四步和第三步输出的结果是一致的,Person.class的加载请求都有扩展类加载器加载,这和前面所说的双亲委派机制并不矛盾。JVM出于安全考虑,不会加载<JAVA_HOME>/lib目录下存在的陌生类,只能加载JVM指定的类。
5、删除<JAVA_RUNTIME_HOME>\lib\ext的test.jar和当前目录下编译的Person.class
输出结果:系统抛出java.lang.ClassNotFoundException
三、开发自己的类加载器
在类加载过程中,真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
1、文件系统类加载器
package com.liziba.classloader;
import java.io.*;
/**
* <p>
* 文件系统类加载器
* </p>
*
* @Author: Liziba
* @Date: 2021/5/31 23:04
*/
public class FileSystemClassLoader extends ClassLoader {
/** 指定文件路径 */
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = getClassByteData(name);
if (data == null || data.length == 0) {
throw new ClassNotFoundException();
} else {
return defineClass(name, data, 0, data.length);
}
}
/**
* 读取类的字节流、获取字节数组
*
* @param className
* @return
*/
private byte[] getClassByteData(String className) {
String path = classNameCovertToPath(className);
try {
InputStream in = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 4];
int len = 0;
while ((len = in.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 类权限定名转绝对路径
*
* @param className
* @return
*/
private String classNameCovertToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}
测试类
package com.liziba.classloader;
import com.liziba.classloader.bean.Person;
/**
* <p>
* 测试文件类加载器
* </p>
*
* @Author: Liziba
* @Date: 2021/5/31 23:12
*/
public class TestFileSystemClassLoader {
public static void main(String[] args) {
String rootDir = "E:\\workspaceall\\liziba-java\\out\\production\\liziba-java";
String className = "com.liziba.classloader.bean.Person";
FileSystemClassLoader fscl = new FileSystemClassLoader(rootDir);
Class<?> clazz = null;
try {
clazz = fscl.findClass(className);
Object object = clazz.newInstance();
System.out.println(object);
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
输出结果:
2、网络类加载器
package com.liziba.classloader;
import sun.nio.ch.Net;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
/**
* <p>
* 网络类加载器
* </p>
*
* @Author: Liziba
* @Date: 2021/5/31 23:25
*/
public class NetworkClassLoader extends ClassLoader{
/** 指定网络URL */
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 从网络上获取类的字节数组
*
* @param className
* @return
*/
private byte[] getClassData(String className) {
String path = classNameCovertToPath(className);
try {
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 4];
int len = 0;
// 读取类文件的字节
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 类权限定名转绝对路径
*
* @param className
* @return
*/
private String classNameCovertToPath(String className) {
return rootUrl + "/" + className.replace('.', '/') + ".class";
}
}
网络类加载器加载后,一般有两种办法来使用这个类
- 使用Java反射API
- 使用接口
具体的使用过程和上面的文件类加载器的使用大同小异,相信聪明的大货也不需要在演示啦!
- 点赞
- 收藏
- 关注作者
评论(0)