摘要:以上文中的类的加载过程为例,它的加载器为系统类加载器。自定义加载器编写自定义加载器并不困难,只要继承抽象类并覆盖方法就行了。源码来自参考资料类加载机制与类加载器架构深入探讨类加载器
序
我是在关于Java的面试题里了解到类加载器的,在这之前从未想过Java里类是如何被加载、解析的,一直以为只要Import就好了。事实上Java类加载器是一块非常重要的内容,可以用在类层次划分、OSGi、热部署、代码加密等领域。即使业务上可能没有涉及到,了解相关知识对排除BUG也是有帮助的。
类加载器基本概念平时在编写代码时,想使用什么类就Import就好了,好像这些类一开始就在JVM里了一样,现在我们知道这是因为JVM自动为我们加载了这些类。顾名思义,类加载器的工作主要是加载Java字节码文件(也就是.class文件)到虚拟机里,并解析为java.lang.Class类的一个实例。到这里,被加载的类还是不能像平时一样直接new一个对象出来的。因为一个类总共要经历加载、验证、解析、初始化等4个步骤后才是Java里的一个类型。后面几个步骤不是本文重点,大家可以自行学习。
类加载器的组成类加载器一共有4种,分别是引导类加载器(bootstrap class loader)、扩展类加载器(extensions class loader)、系统类加载器(system class loader)、自定义加载器,它们之间的加载关系如下图所示:
其中,除了引导类加载器是用原生代码实现,其余的加载器都是继承自抽象类java.lang.ClassLoader。而且系统自带的3个加载器都有自己的特殊之处。
引导类加载器引导类加载器是用来加载Java的核心库,像是java.lang包等这些Java应用必备的类都是引导类加载器加载的。加载路径是<JAVA_HOME>lib目录中的或者是-Xbootclasspath参数所指定的目录中,被JVM所识别的文件(通过名字识别,名字必须是rt.jar)。因为引导类加载器是用原生代码实现的,所以不能在Java代码中直接引用到引导类加载器。
扩展类加载器顾名思义,扩展类加载器是用来加载Java的扩展类库。加载路径是<JAVA_HOME>libext目录中的或者是java.ext.dirs系统变量所指定的路径中的所有类库。
系统类加载器系统类加载器的加载路径是Java应用的类路径(CLASSPATH),也就是说在没有自定义加载器的情况下,Java应用的类都是由系统类加载器加载的。而且该加载器可以用ClassLoader类的getSystemClassLoader()方法直接获取到。
除了引导类加载器,每个加载器都有一个父加载器。比如加载器A加载了加载器B,那么加载器A就是加载器B的父加载器,可以通过java.lang.ClassLoader的getParent()方法获取父加载器,而且Java中每个Class对象都维护着一个加载器引用,可以通过getClassLoader()方法获取加载该类的加载器。
例如下面这段代码:
public class Main { public static void main(String[] args) { ClassLoader loader = Main.class.getClassLoader(); while (loader != null) { System.out.println(loader.toString()); loader = loader.getParent(); } } }
这里输出了Main类的加载器与其所有的父加载器,运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@1540e19d Process finished with exit code 0
我们看到Main类的加载器是系统类加载器,它的父加载器是扩展类加载器。扩展类加载器的父加载器应该是引导类加载器才对,这里没有输出是因为有些JDK的实现里在父加载器为引导类加载器的情况下是返回null的。
双亲委托模式第一次看到双亲委托模式这个词的时候就感觉意义不明,完全不知道是什么意思。在了解了加载器的加载过程之后,才发现是一种代理模式。
以上文中的Main类的加载过程为例,它的加载器为系统类加载器。但是系统类加载器不会直接去加载这个类,而是先委托给它的父加载器,也就是扩展类加载器。同样,扩展类加载器也会先委托给它的父加载器,一直委托到引导类加载器才开始真正的尝试加载,如果加载失败就返回由发出委托的加载器尝试加载。
这样做的目的是为了保护Java核心库和保持类型安全。因为在JVM中判断两个类是否相同,不仅仅是看它们的全名是否相同,还要判断它们的加载器是否相同。通过双亲委托模式就能保证每次加载核心库的加载器都是引导类加载器,从而防止出现类似于多个java.lang.Object类型这种情况。
自定义加载器编写自定义加载器并不困难,只要继承抽象类java.lang.ClassLoader并覆盖findClass(String name)方法就行了。不建议覆盖 loadClass(String name)方法,因为这个方法里面封装了前面提到的双亲委托模式,覆盖可能会导致该模式失效。
// 源码来自 https://www.ibm.com/developerworks/cn/java/j-lo-classloader public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } 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); } } private byte[] getClassData(String className) { String path = classNameToPath(className); 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; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace(".", File.separatorChar) + ".class"; } }参考资料
Java类加载机制与Tomcat类加载器架构
深入探讨 Java 类加载器
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/76722.html
摘要:如果需要支持类的动态加载或需要对编译后的字节码文件进行解密操作等,就需要与类加载器打交道了。双亲委派模型,双亲委派模型,约定类加载器的加载机制。任何之类的字节码都无法调用方法,因为该方法只能在类加载的过程中由调用。 jvm系列 垃圾回收基础 JVM的编译策略 GC的三大基础算法 GC的三大高级算法 GC策略的评价指标 JVM信息查看 GC通用日志解读 jvm的card table数据...
摘要:最终形成可以被虚拟机最直接使用的类型的过程就是虚拟机的类加载机制。即重写一个类加载器的方法验证验证是连接阶段的第一步,这一阶段的目的是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版》读书笔记与常见相关面试题总结 本节常见面试题(推荐带着问题阅读,问题答案在文中都有提到): 简单说说类加载过...
摘要:看到的只是,而由泛型附加的类型信息对来说是不可见的。然后再加载执行类的静态变量以及静态语句块。接口中基本数据类型为而抽类象不是的。本地方法接口主要是调用或实现的本地方法及返回结果。用户自定义类加载器,在程序运行期间,通过的子类动态加载。 编译机制 编译主要是把 .Java文件转换为 .class 文件。其中转换后的 .class 文件就包含了元数据,方法信息等一些信息。比如说元数据就...
摘要:如问到是否使用某框架,实际是是问该框架的使用场景,有什么特点,和同类可框架对比一系列的问题。这两个方向的区分点在于工作方向的侧重点不同。 [TOC] 这是一份来自哔哩哔哩的Java面试Java面试 32个核心必考点完全解析(完) 课程预习 1.1 课程内容分为三个模块 基础模块: 技术岗位与面试 计算机基础 JVM原理 多线程 设计模式 数据结构与算法 应用模块: 常用工具集 ...
阅读 1808·2021-11-25 09:43
阅读 1295·2021-11-22 15:08
阅读 3666·2021-11-22 09:34
阅读 3193·2021-09-04 16:40
阅读 2844·2021-09-04 16:40
阅读 514·2019-08-30 15:54
阅读 1314·2019-08-29 17:19
阅读 1707·2019-08-28 18:13