JVM的类加载器
JVM的类加载器类加载器(ClassLoader)是JVM的核心组件之一,负责将.class文件加载到JVM,但从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
类加载器的层次结构
Bootstrap ClassLoader(启动类加载器)
Extension ClassLoader(扩展类加载器)
Application ClassLoader(应用程序类加载器)
双亲委派模式
双亲委派模式,可以用一句话来说表达:**任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载,**双亲委派模型的工作流程如下:
当一个类加载器收到类加载请求时,首先不会自己尝试加载,而是委托给父类加载器
父类加载器会继续向上委托,直到启动类加载器
如果父类加载器无法完成加载,子加载器才会尝试自己加载
其实JVM 对类的唯一标识,可以简单的理解为由ClassLoader id + PackageName + ClassName组成,因此在一个运行程序中有可能存在两个包名和类名完全一致的类,但是如果这两个类不是由一个 ClassLoader 加载,会被视为两个不同的类,此时就无法将一个类的实例强转为另外一个类,这就是类加载器的隔离性。
自定义类加载器
我们可以通过继承ClassLoader类来实现自定义的类加载器。通常只需要重写findClass方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import java.io.*; public class MyClassLoader extends ClassLoader { private String classPath;
public MyClassLoader(String classPath) { this.classPath = classPath; }
@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); } }
private byte[] getClassData(String className) { String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try (InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; }
public static void main(String[] args) throws Exception { MyClassLoader classLoader = new MyClassLoader("/path/to/classes"); Class<?> clazz = classLoader.loadClass("com.example.Test"); Object obj = clazz.newInstance(); System.out.println(obj.getClass().getClassLoader()); } }
|
类加载过程
类加载过程分为以下三个阶段:
加载(Loading)
链接(Linking)
验证(Verification): 确保Class文件的字节流符合JVM规范
准备(Preparation): 为类变量分配内存并设置初始值
解析(Resolution): 将符号引用转换为直接引用
初始化(Initialization)
- 执行类构造器
<clinit>()
方法,为类变量赋正确的初始值
打破双亲委派模型
在某些场景下需要打破双亲委派模型,例如热部署、OSGi框架、Tomcat等Web容器,打破双亲委派的示例(参考互联网CSDN):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class BreakDelegationClassLoader extends ClassLoader { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if (name.startsWith("com.example.break")) { return findClass(name); } return super.loadClass(name); }
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } return defineClass(name, classData, 0, classData.length); } }
|
类加载器的应用场景
模块化与热部署
每个模块使用独立的类加载器
修改模块后可以重新加载而不影响其他模块
代码加密
实现不同版本类库共存
Android中的DexClassLoader
常见面试问题QA
Q:如何判断两个类是否相同?
A:在JVM中,两个类是否相同不仅取决于类名是否相同,还取决于加载它们的类加载器是否相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class ClassIdentityTest { public static void main(String[] args) throws Exception { String classPath = "/path/to/classes"; MyClassLoader loader1 = new MyClassLoader(classPath); MyClassLoader loader2 = new MyClassLoader(classPath);
Class<?> clazz1 = loader1.loadClass("com.example.Test"); Class<?> clazz2 = loader2.loadClass("com.example.Test");
System.out.println(clazz1 == clazz2); System.out.println(clazz1.getClassLoader()); System.out.println(clazz2.getClassLoader()); } }
|