目录
一、引言
二、类加载过程
1.Load
2.Link
3.Init
三、案例
1.第1处说明(new关键字与newInstance()方法区别)
2.第 2 处说明(使用类似的方式可获取其他声明如注解、方法等)
3.第 3 处说明(private 成员在类外依然可以修改)
四、类加载器结构
1.最高层Bootstrap
2.第二层Platform ClassLoader(JDK9)
3.第三层Application ClassLoader
五、类加载过程(双亲委派模型)
六、自定义类加载器
1.需要自定义类加载器情形
2.实现自定义类加载器的步骤
在冯·诺依曼定义的计算机模型中,任何程序都需要加载到内存才能与 CPU 进行交流。字节码 .class 文件同样需要加载到内存中,才可以实例化类。"兵马未动,粮草先行。" ClassLoader 正是准备粮草的先行军,它的使命就是提前加载 .class 类文件到内存中。在加载类时,使用的是 Parents Delegation Model ,译为双亲委派模型,这个译名有些不妥。如果意译的话,则译作 “溯源委派加载模型”更加贴切。
Java 的类加载器是一个运行时核心基础设施模块,如下图所示,主要是在启动之初进行类的 Load 、 Link 和 Init , 即加载、链接、初始化。
第一步, Load 阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验 cafe babe 魔法数、常量池、文件长度、是否有父类等,然后创建对应类的 java.lang.Class 实例。
第二步, Link 阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如 final 是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值;解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局。
第三步, Init 阶段执行类构造器<clinit> 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。
类加载是一个将 .class 字节码文件实例化成 Class 对象并进行相关初始化的过程。在这个过程中, JVM 会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。
全小写的 class 是关键字,用来定义类,而首字母大写的 Class ,它是所有 class 的类。这句话理解起来有难度,是因为类已经是现实世界中某种事物的抽象,为什么这个抽象还是另外个类 Class 的对象?示例代码如下:
public class ClassTest {
// 数组类型有一个魔法属性:length 来获取数组长度
private static int[] array = new int[3];
private static int length = array.length;
//任何小写 class 的定义的类,也有一个魔法属性:class,来获取此类的大写 Class 类对象
private static Class one = One.class;
private static Class another = Another.class;
public static void main(String[] args) throws Exception {
// 通过 newInstance 方法创建 One 和 Another 的类对象 (第1处)
One oneObject = one.newInstance();
oneObject.call();
Another anotherObject = another.newInstance();
anotherObject.speak();
// 通过 one 这个大写的 Class 对象,获取私有成员属性对象 Filed (第2处)
Field privateFiledInOne = one.getDeclaredField("inner");
// 设置私有对象的属性可以访问和修改 (第3处)
privateFiledInOne.setAccessible(true);
privateFiledInOne.set(oneObject, "world changed.");
// 成功修改类的私有属性 inner 变量值为 world changed.
System.out.println(oneObject.getInner());
}
}
class One {
private String inner = "time files.";
public void call() {
System.out.println("hello world.");
}
public String getInner() {
return inner;
}
}
class Another {
public void speak() {
System.out.println("easy coding.");
}
}
Class 类下的newInstance()在JDK9中已经置为过时,使用getDelaredConstructor().newlnstance()的方式。这里着重说明一下 new 与newInstance 的区别。
new 关键字是强类型校验,可以调用任何构造方法,在使用 new 操作的时候,这个类可以没有被加载过。
而 Class 类下的 newInstance() 方法是弱类型,只能调用无参数构造方法,如果没有默认构造方法,就抛出InstantiationException 异常,如果此构造方法没有权限访问,则抛出IllegalAccessException 异常。Java 通过类加载器把类的实现与类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。
Java中工厂模式经常使用 newInstance() 方法来创建对象,因此从为什么要使用工厂模式上可以找到具体答案:
(1)初始级别,其中 ExampleInterface 是 Example 的接口
class c = Class.forName("Example");
factory = (ExampleInterface)c.newInstance();
(2)进阶级别,将需要创建对象的类名定义成字符串,作为参数放入forName() 方法中
String className = "Example";
class c = Class.forName(className);
factory = (ExampleInterface)c.newInstance();
(3)变身级别,已经不存在 Example 的类名称,无论 Example 类怎么变化,上述代码不变,甚至可以更换 Example 的兄弟类 Example2、Example3、Example4……,只要他们实现 ExampleInterface 接口就可以
//从xml 配置文件中获得字符串
String className = readfromXMlConfig();
class c = Class.forName(className);
factory = (ExampleInterface)c.newInstance();
从JVM的角度看,我们使用关键字new创建一个类的时候,这个类可以没有被加载。但是使用newInstance()方法的时候,就必须保证:1、这个类已经加载;2、这个类已经连接了。而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载java API的那个加载器。
现在可以看出,newInstance()实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。 这样分步的好处是显而易见的。我们可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。
private 成员在类外是否可以修改?通过 setAccessible( true)操作,即可使用大写 Class 类的 set 方法修改其值。如果没有这一步,则抛出如下异常
Exception in thread "main" java.lang.IllegalAccessException: Class example.ClassTest can not access a member of class example.One with modifiers "private"
通过以上示例,对于 Class 这个"类中之王",不会有恐惧心理了吧?那么回到类加载中,类加载器是如何定位到具体的类文件并读取的呢?
类加载器类似于原始部落结构,存在权力等级制度。
最高的一层是家族中威望最高的 Bootstrap ,它是在 JVM 启动时创建的,通常由与操作系统相关的本地代码实现,是最根基的类加载器,负责装载最核心的 Java 类,比如 Object、System 、String 等;它是通过 C/C++ 实现的,并不存在于 JVM 体系内。
第二层是在 JDK9 版本中,称为 Platform ClassLoader ,即平台类加载器,用以加载一些扩展的系统类,比如 XML,加密、压缩相关的功能类等 ,而 JDK9 之前的加载器是Extension ClassLoader。第二层平台类加载器是通过Java 语言实现。
第三层是 Application ClassLoader 的应用类加载器,主要是加载用户定义的 CLASSPATH 路径下的类。第三层应用类加载器是通过Java 语言实现。
第二、三层类加载器为 Java 语言实现,用户也可以自定义类加载器。查看本地类加载器的方式如下(编译环境JDK7):
ClassLoader classLoader = ClassTest.class.getClassLoader();
// 当前正在使用的类加载器
System.out.println(classLoader);
ClassLoader parent = classLoader.getParent();
// 当前正在使用的类加载器的父加载器
System.out.println(parent);
ClassLoader grandparent = parent.getParent();
// 当前正在使用的类加载器的祖父加载器
System.out.println(grandparent);
打印结果如下:
sun.misc.Launcher$AppClassLoader@146ccf3e
sun.misc.Launcher$ExtClassLoader@7399f9eb
null
因为在JDK7环境中,所以第二层打印结果为ExtClassLoader。AppClassLoader 的 Parent 为 Bootstrap,它是通过 C/C++实现的,并不存在于 JVM 体系内,所以输出为 null,类加载器具有等级制度,但是并非继承关系,以组合的方式来复用父加载器的功能,这也符合组合优先原则,详细的双亲委派模型见下文。
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,要非常礼貌地向上逐级询问:"请问,这个类已经加载了吗?" 被询问的高层次类加载器会自问两个问题,第一,我是否已加载过此类?第二,如果没有,是否可以加载此类?只有当所有高层次类加载器在两个问题上的答案均为"否"时,才可以让当前类加载器加载这个未知类。
如上图所示,左侧绿色箭头向上逐级询问是否已加载此类,直至 Bootstrap ClassLoader ,然后向下逐级尝试是否能够加载此类,如果都加载不了,则通知发起加载请求的当前类加载器, 准予加载。在右侧的三个小标签里,列举了此层类加载器主要加载的代表性类库 , 事实上不止于此。通过如下代码可以查看 Bootstrap 所有已经加载的类库:
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}
执行结果如下:
file:/C:/Software/JDK1.7/jre/lib/resources.jar
file:/C:/Software/JDK1.7/jre/lib/rt.jar
file:/C:/Software/JDK1.7/jre/lib/sunrsasign.jar
file:/C:/Software/JDK1.7/jre/lib/jsse.jar
file:/C:/Software/JDK1.7/jre/lib/jce.jar
file:/C:/Software/JDK1.7/jre/lib/charsets.jar
file:/C:/Software/JDK1.7/jre/lib/jfr.jar
file:/C:/Software/JDK1.7/jre/classes
Bootstrap 加载的路径可以追加,不建议修改或删除原有加载路径 。 在 JVM 中增加如下启动参数,则能通过Class.forName 正常读取到指定类,说明此参数可以增加 Bootstrap 的类加载路径:
-Xbootclasspath/a:/Users/mark/Java/src
如果想在启动时观察加载了哪个 jar 包中的哪个类,可以增加 -XX:+TraceClassLoading参数,此参数在解决类冲突时非常实用,毕竟不同的 JVM 环境对于加载类的顺序并非是一致的。有时想观察特定类的加载上下文,由于加载的类数量众多,调试时很难捕捉到指定类的加载过程,这时可以使用条件断点功能。比如,想查看 HashMap 的加载过程,在 loadClass 处打个断点,并且在 condition 框内输入var1.equals("java.util.HashMap")条件。
在明白了类加载器的实现机制后,知道双亲委派模型并非强制模型(我们可以对其中加载路径进行修改删除,而非一定要加载某些包),用户可以自定义类加载器,在什么情况下需要自定义类加载器呢?
(1)隔离加载类
在某些框架内进行中间件与应用的模块隔离, 把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar 包不会影响到中间件运行时使用的 jar 包。
(2)修改类加载方式
类的加载模型并非强制,除 Bootstrap 外,其他的加载并非定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
(3)扩展加载源
比如从数据库、网络 ,甚至是电视机机顶盒进行加载。
(4)防止源码泄露
Java 代码容易被编译和篡改,可以进行编译加密 。那么类加载器也需要自定义,还原加密的字节码。
实现自定义类加载器的步骤:继承 ClassLoader,重写 findClass() 方法,调用 defineClass() 方法。一个简单的类加载器实现的示例代码如下:
首先我们定义一个待加载的实体类Customer,我们把生成的Customer.class剪切到D盘目录下D:/Customer.class
public class Customer {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Customer{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
接着我们定义一个自定义类加载器
public class CustomClassLoader extends ClassLoader {
// 继承ClassLoader类
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
// 重写findClass方法
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
// 调用defineClass方法
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
throw new ClassNotFoundException();
}
private byte[] getClassFromCustomPath(String name) throws IOException {
// 从自定义路径中加载指定类
// 这里要读入.class的字节,因此要使用字节流
FileInputStream fis = new FileInputStream(new File("D:/Customer.class"));
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true)
{
int i = fc.read(by);
if (i == 0 || i == -1) {
break;
}
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
测试类
public class TestMethod {
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class> clazz = Class.forName("example.Customer", true, customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
System.out.println(obj.getClass().getClassLoader().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试结果
example.CustomClassLoader@4e543c44
sun.misc.Launcher$AppClassLoader@146ccf3e
注意点,很可能你打印出来的是
sun.misc.Launcher$AppClassLoader@146ccf3e
sun.misc.Launcher$ExtClassLoader@7399f9eb
那是因为你没有删除项目路径下编译生成的 Customer.class 文件,你的Eclipse或者IDEA自动编译Customer类(此时是用的AppClassLoader编译)后,生成了Customer.class 文件,当你准备用自定义类加载器的时候,根据双亲委任模型,会向上级请示是否已经加载过此类,显然,当询问到 AppClassLoader 加载器时得到的答案为:是。因此此时自定义类加载器变不会生效
由于中间件一般都有自己的依赖 jar 包,在同一个工程内引用多个框架时,往往被迫进行类的仲裁。按某种规则 jar 包的版本被统一指定,导致某些类存在包路径、类名相同的情况, 就会引起类冲突,导致应用程序出现异常。主流的容器类框架都会
自定义类加载器,实现不同中间件之间的类隔离,有效避免了类冲突。