一、类加载器
1.1、是什么
在Java中,类加载器(ClassLoader)是一个负责将Java类文件(.class文件)加载到Java虚拟机(JVM)中的组件。类加载器将字节码文件转换为内存中的Class对象,从而使得JVM可以执行这些类。简单讲就是把实现 类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。
1.2、类加载器的职责
加载类:找到并读取类文件(.class 文件),并将其内容转换为内存中的 Class 对象。
通过全限定类名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
连接类:将类的二进制数据组合成可以在 JVM 中使用的格式。
验证:确保类的字节码符合 JVM 的要求和安全规则。
准备:为类的静态字段分配内存,并初始化为默认值。
解析:将符号引用转换为直接引用。
初始化类:执行类的静态初始化块和静态字段的初始化。
二:类加载器特性
2.1、委托机制(Delegation Model)
遵循父类委托模型(Parent Delegation Model):当一个类加载器需要加载一个类时,它会先把请求委托给它的父类加载器,如果父类加载器不能完成这个请求,子类加载器才会自己尝试加载。这种机制确保了Java类的一致性和安全性。
2.2、 可见性(Visibility)
根据委托机制,子类加载器可以看到父类加载器加载的类,但父类加载器不能看到子类加载器加载的类。这意味着,子类加载器可以引用父类加载器加载的类,而反之则不行。
2.3、单一性(Uniqueness)
在委托机制下,同一个类不会被加载多次。这意味着,JVM中任意一个类都是由一个类加载器加载的一个类实例,这确保了类的单一性。
2.4、隔离性(Isolation)
不同的类加载器可以加载同名的类并且互相隔离。不同的类加载器加载的同名类在JVM中是不同的类,可以共存而互不干扰。
2.5、动态性(Dynamic Loading)
支持在运行时动态加载类:这意味着,可以在程序运行过程中根据需要加载新的类,这在插件系统和动态模块化应用中非常有用。
2.6、可扩展性(Extensibility)
开发者可以创建自定义类加载器,通过继承java.lang.ClassLoader并重写findClass方法来实现自定义的类加载逻辑。
三、类加载器分类
3.1、引导类加载器(Bootstrap ClassLoader)
作用:加载 Java 核心类库,即 JAVA_HOME/lib 目录下的 JAR 文件(如 rt.jar)。
特点:由本地代码实现(通常是 JVM 本身的实现),负责加载最基本的 Java 类,如 java.lang.*、java.util.* 等。
3.2、扩展类加载器(Extension ClassLoader)
作用:加载扩展类库,即 JAVA_HOME/lib/ext 目录下的 JAR 文件。
特点:用于加载扩展的 Java 类库,通常包括一些标准扩展(如加密、压缩等功能的库)。
3.3、 应用类加载器(Application ClassLoader 或 System ClassLoader)
作用:加载应用程序类路径(classpath)下的类和资源。
特点:默认从 classpath 指定的目录和 JAR 文件中加载类。
3.4、LaunchedURLClassLoader
作用:Spring Boot 特有的类加载器,用于加载 Spring Boot 可执行 JAR 包中的嵌入 JAR 文件。
特点:继承自 java.net.URLClassLoader,能够处理 Spring Boot fat JAR 结构中的嵌入 JAR 文件。Spring Boot 的 Launcher 类在启动时会创建并使用这个类加载器。
3.5、Custom ClassLoaders(自定义类加载器)
作用:在某些情况下,开发者可能会定义自定义类加载器,以实现特定的类加载行为。
特点:这些自定义类加载器可以在应用程序或框架内被使用,以实现更灵活的类加载机制。
四、如何查看类加载器
4.1、代码输出
4.2、使用arthas工具
arthas命令:
sc -d + 全路径类名
Classloader -l 查看当前项目所有类加载器
五、类加载模型原理
5.1、双亲委派模型工作原理
jvm对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。
如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。
如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader
如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式
5.2、反向委派机制
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、dubbo等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载。而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。
自定义的类加载器:先使用自定义的类加载器加载不能加载再委托父加载器(AppClassloader)加载;这个双亲委派是想违背的
六、自定义类加载器应用
4.1 、skywalking如何使用自定义类加载器
SkyWalking 使用名为 AgentClassLoader 的自定义类加载器。该类加载器是 SkyWalking Java 代理的一部分,该代理负责动态装入类和仪器应用程序代码,以便进行监视和跟踪。
skywalking-agent.jar 中的所有类和我们的应用程序中的类(待增强的目标类)一起是由 AppClassLoader 类加载器加载的,而我们定义的一些插件是由自定义的类加载器 AgentClassLoader 加载的,应用程序中的类(目标类)和插件拦截器类他们之间是互相不可见的,需要将目标类的类加载器作为插件拦截器类的加载器 AgentClassLoader 的父加载器,这样插件拦截器类可以读取到目标类的相关类了。
public class AgentClassLoader extends ClassLoader {
static {
tryRegisterAsParallelCapable();
}
private static final ILog logger = LogManager.getLogger(AgentClassLoader.class);
/** * The default class loader for the agent. */
private static AgentClassLoader DEFAULT_LOADER;
private List classpath;
private List allJars;
private ReentrantLock jarScanLock = new ReentrantLock();
/** * Functional Description:
solve the classloader dead lock when jvm start * only support JDK7+,
since ParallelCapable appears in JDK7+ */
private static void tryRegisterAsParallelCapable() {
Method[] methods = ClassLoader.class.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
String methodName = method.getName();
if ("registerAsParallelCapable".equalsIgnoreCase(methodName)) {
try {method.setAccessible(true);method.invoke(null);
}
catch (Exception e) {
logger.warn(e, "can not invoke ClassLoader.registerAsParallelCapable()");
}return;}}}
public static AgentClassLoader getDefault() {
return DEFAULT_LOADER;
}
/** * Init the default * * @throws AgentPackageNotFoundException */ public static void initDefaultLoader() throws AgentPackageNotFoundException {if (DEFAULT_LOADER == null) {synchronized (AgentClassLoader.class) {if (DEFAULT_LOADER == null) {DEFAULT_LOADER = new AgentClassLoader(PluginBootstrap.class.getClassLoader());}}}}//说明自定义插件到哪里去加载public AgentClassLoader(ClassLoader parent) throws AgentPackageNotFoundException {super(parent);File agentDictionary = AgentPackagePath.getPath();classpath = new LinkedList();classpath.add(new File(agentDictionary, "plugins"));classpath.add(new File(agentDictionary, "activations"));}//重写findClass 方法,@Overrideprotected Class> findClass(String name) throws ClassNotFoundException {List allJars = getAllJars();String path = name.replace('.', '/').concat(".class");for (Jar jar : allJars) {JarEntry entry = jar.jarFile.getJarEntry(path);if (entry != null) {try {URL classFileUrl = new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + path);byte[] data = null;BufferedInputStream is = null;ByteArrayOutputStream baos = null;try {is = new BufferedInputStream(classFileUrl.openStream());baos = new ByteArrayOutputStream();int ch = 0;while ((ch = is.read()) != -1) {baos.write(ch);}data = baos.toByteArray();} finally {if (is != null)try {is.close();} catch (IOException ignored) {}if (baos != null)try {baos.close();} catch (IOException ignored) {}}return defineClass(name, data, 0, data.length);} catch (MalformedURLException e) {logger.error(e, "find class fail.");} catch (IOException e) {logger.error(e, "find class fail.");}}}throw new ClassNotFoundException("Can't find " + name);}@Overrideprotected URL findResource(String name) {List allJars = getAllJars();for (Jar jar : allJars) {JarEntry entry = jar.jarFile.getJarEntry(name);if (entry != null) {try {return new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + name);} catch (MalformedURLException e) {continue;}}}return null;}@Overrideprotected Enumeration findResources(String name) throws IOException {List allResources = new LinkedList();List allJars = getAllJars();for (Jar jar : allJars) {JarEntry entry = jar.jarFile.getJarEntry(name);if (entry != null) {allResources.add(new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + name));}}final Iterator iterator = allResources.iterator();return new Enumeration() {@Overridepublic boolean hasMoreElements() {return iterator.hasNext();}@Overridepublic URL nextElement() {return iterator.next();}};}private List getAllJars() {if (allJars == null) {jarScanLock.lock();try {if (allJars == null) {allJars = new LinkedList();for (File path : classpath) {if (path.exists() && path.isDirectory()) {String[] jarFileNames = path.list(new FilenameFilter() {@Overridepublic boolean accept(File dir, String name) {return name.endsWith(".jar");}});for (String fileName : jarFileNames) {try {File file = new File(path, fileName);Jar jar = new Jar(new JarFile(file), file);allJars.add(jar);logger.info("{} loaded.", file.toString());} catch (IOException e) {logger.error(e, "{} jar file can't be resolved", fileName);}}}}}} finally {jarScanLock.unlock();}}return allJars;}private class Jar {private JarFile jarFile;private File sourceFile;private Jar(JarFile jarFile, File sourceFile) {this.jarFile = jarFile;this.sourceFile = sourceFile;}}}
TypeScript |
4.2、有些公司是如何使用自定义类加载器加载插件的
4.2.1、会下沉了哪些能力
4.2.2、举个
将一些通用的能力下沉到“插件”,自定义类加载器WhitzardClassloader继承URLClassLoader,重写了findClass,findResource,findResources方法;
Java |
4.2.3、验证类加载器加载了类
4.3、"手撕"自定义类加载器
4.3.1、无自定义类加载器——利用bytebuddy定义一个agent拦截器
Java |
项目引入,查看执行结果
Java |
4.3.2、有自定义类加载器——利用bytebuddy定义一个agent
Java |
Java |
项目引入,查看类加载结果
七、总结指导意义
1. 调试和排查问题
类冲突和版本问题:在复杂的项目中,可能会存在同一个类的多个版本。理解类加载机制有助于排查这些类冲突和版本不一致的问题。
ClassNotFoundException 和 NoClassDefFoundError:知道类加载的过程可以帮助开发者快速定位这些错误的原因,例如类路径配置错误或依赖未正确加载。
2. 性能优化
类加载时间:了解类加载的时机和机制,可以帮助优化应用启动时间。减少不必要的类加载和避免大量静态初始化可以提升应用性能。
避免重复加载:通过理解类加载的缓存机制,可以避免重复加载类,从而减少内存使用和提高加载速度。
3. 模块化和插件化开发
相对对于需要经常改动的功能可以使用插件,避免业务方频繁更新版本。
4. 安全性
防止类替换/修改:理解双亲委派模型,有助于防止应用核心类库被恶意替换,提升应用安全性。
权限管理:通过自定义类加载器,可以对加载的类进行权限管理,确保只有可信的代码被加载和执行。
5. 热部署
实现热部署:理解类加载机制可以帮助实现应用的热部署,通过自定义类加载器,可以在不重启 JVM 的情况下动态加载新的类和资源,提高开发效率和系统可用性。
6. 设计和架构
优化架构设计:在设计系统架构时,理解类加载机制可以帮助优化类的加载和依赖管理,设计出更高效和可维护的系统。
隔离性和可维护性:通过合理使用类加载器,可以实现类和资源的隔离,提升系统的可维护性和扩展性。