Class 文件装载流程
类装载条件:
- 创建一个类的实例时;
- 调用类的静态方法时;
- 使用类或接口的静态字段时(final 常量除外);
- 使用 java.lang.reflect 包中的方法反射类的方法是;
- 初始化子类时,要求先初始化父类;
- 作为启动虚拟机,含有 main() 方法的那个类;
Parent:
public class Parent { static { System.out.println("Parent init"); } public static int v = 100;}复制代码
Child:
public class Child extends Parent { static { System.out.println("Child init"); }}复制代码
测试:
public class Main { public static void main(String[] args) { Child child = new Child(); }}复制代码
输出:
Parent initChild init复制代码
修改测试类:
public class Main { public static void main(String[] args) { System.out.println(Child.v); }}复制代码
输出:
Parent init100复制代码
引用一个字段时,只有直接定义该字段的类,才会被初始化。
虽然 Child 类没有被初始化,但是此时 Child 类以及被系统加载,只是没有进入到初始化阶段。
使用 -XX:+TraceClassLoading 打印:
[Loaded Parent from file:/D:/workspace/tutorial-jvm/out/production/tutorial-jvm/][Loaded Child from file:/D:/workspace/tutorial-jvm/out/production/tutorial-jvm/]Parent init100复制代码
如果修改 Parent 静态变量,用 final 修饰,再次执行:
100复制代码
javac 在编译时,将常量直接植入目标类,不再使用被引用类。
加载类
- 通过类的全名,获取类的二进制数据;
- 解析类的二进制数据流为方法区内的数据结构;
- 创建 java.lang.Class 类的实例,表示该类型;
验证类
- 格式检查
- 语义检查
- 字节码验证
- 符号引用验证
准备
虚拟机会为这个类分配相应的内存空间,并设置初始值。
解析类
将类、接口、字段和方法的符号引用专为直接引用。
初始化
类装载的最后阶段,开始执行 Java 字节码。
ClassLoader
ClassLoader,类装载器。CLassLoader 在 Java 中有着非常重要的左右,主要工作在 Class 装在的加载阶段,其主要作用是从系统外部获得 Class 二进制数据流。
认识 ClassLoader
所有的 Class 都是由 ClassLoader 进行加载的,ClassLoader 负责通过各种法师将 Class 信息的二进制数据流读入系统,然后交给 Java 虚拟机进行连接、初始化等操作。
ClassLoader 是一个抽象类,主要方法如下:
- public Class<?> loadClass(String name) throws ClassNotFoundException 给定一个类名,加载一个类;
- protected final Class<?> defineClass(byte[] b, int off, int len) 根据给定的字节流 b 定义一个类,off 和 len 参数表示实际 Class 信息在 byte 数组中的位置和长度。这是受保护的方法,只有在自定义 ClassLoader 子类中可以使用;
- protected Class<?> findClass(String name) throws ClassNotFoundException 查找一个类,这是一个受保护的方法,也是重载 ClassLoader 时,重要的系统扩展点;
- protected final Class<?> findLoadedClass(String name) 寻找已经加载的类。final 修饰,无法被修改;
ClassLoader 中,还有一个重要的字段 parent,也是一个 ClassLoader 的实例,表示这个 ClassLoader 的双亲。类加载过程中, ClassLoader 可能会将某些请求交予自己的双亲处理。
ClassLoader 分类
标准 Java 程序中,Java 虚拟机会创建三种 ClassLoader 为整个应用程序服务:
- BootStrap ClassLoader 启动类加载器
- Extension ClassLoader 拓展类加载器
- App ClassLoader 应用类加载器,也成为系统类加载器
自下往上为自己的双亲。当系统需要使用一个类时,在判断类是否已经被加载时,会先从当前底层类加载器进行判断。当系统需要加载一个类时,会从顶层类开始加载,一次向下尝试,直到成功。
public class Main { public static void main(String[] args) { ClassLoader classLoader = Main.class.getClassLoader(); while (classLoader != null) { System.out.println(classLoader); classLoader = classLoader.getParent(); } }}复制代码
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2sun.misc.Launcher$ExtClassLoader@1b6d3586复制代码
Main 类家在与 AppClassLoader,它的双亲为 ExtClassLoader。但是 ExtClassLoader 无法再取得启动类加载器,是因为这是一个系统级的纯 C 实现。因此任何加载在启动类加载器中的类是无法获取其 ClassLoader 实例的:
String.class.getClassLoader() // null复制代码
双亲委托
在类加载的时候,系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载。在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载。
参考 ClassLoader 中 loadClass() 方法:
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); 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; } }复制代码
双亲为 null 有两种情况,第一,其双亲就是启动类加载器;第二,当前加载器就是启动类加载器。
判断类是否加载时,应用类加载器会顺着双亲路径网上判断,直到启动类加载器。但是启动类加载器不会往下询问,这个委托路线是单向的,理解这点很重要。
双亲委托的弊端
顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类。
当在系统类中,提供一个接口,接口需要在应用中得以实现。该接口绑定一个工厂方法,用于创建该接口的实例。由于启动类加载器无法向下询问,就会出现该工厂发发无法创建由应用类加载器加载的应用实例。拥有这种问题组件有很多,比如 JDBC、Xml Parser 等。
双亲委托补充
以 javax.xml.parses 中实现 XML 文件解析功能模块为例,构造一个 DocumentBuilderFactory 的实例(加载在启动类加载器中):
public static DocumentBuilderFactory newInstance() { return FactoryFinder.find( /* The default property name according to the JAXP spec */ DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory" /* The fallback implementation class name */ "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");}复制代码
跟进这个方法,最后阅读到:
ClassLoader getContextClassLoader() throws SecurityException{ return (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { ClassLoader cl = null; //try { cl = Thread.currentThread().getContextClassLoader(); //} catch (SecurityException ex) { } if (cl == null) cl = ClassLoader.getSystemClassLoader(); return cl; } });}复制代码
Thread 有两个方法:
- public ClassLoader getContextClassLoader()
- public void setContextClassLoader(ClassLoader cl)
通过这两个方法,可以把一个 ClassLoader 置于一个线程实例之中,使其相对共享,默认情况下上下文加载器就是应用类加载器。
突破双亲模式
public class MyClassLoader extends ClassLoader { @Override public Class loadClass(String name) throws ClassNotFoundException { // 重写类加载流程 return super.loadClass(name); }}复制代码
热替换
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。基本上大部分脚本语言天生支持热替换,例如 PHP。
两个不同 ClassLoader 加载同一个类,在虚拟机内部,会认为这 2 个类是完全不同的。
自定义 ClassLoader:
import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.IOException;/** * @author caojiantao */public class MyClassLoader extends ClassLoader { private String fileName; public MyClassLoader(String fileName) { this.fileName = fileName; } @Override protected Class findClass(String name) throws ClassNotFoundException { Class clazz = findLoadedClass(name); if (clazz == null) { try (FileInputStream is = new FileInputStream(fileName + name + ".class"); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { baos.write(buffer, 0, len); } byte[] bytes = baos.toByteArray(); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { e.printStackTrace(); } } return super.findClass(name); }}复制代码
热替换的 Java 类:
/** * @author caojiantao */public class Hello { public void sayHello() { System.out.println("hello"); }}复制代码
测试类:
import java.lang.reflect.Method;/** * @author caojiantao */public class Main { public static void main(String[] args) { while (true) { MyClassLoader classLoader = new MyClassLoader("C:\\Users\\caojiantao\\Desktop\\"); try { Class clazz = classLoader.loadClass("Hello"); Object instance = clazz.newInstance(); Method say = instance.getClass().getMethod("sayHello"); say.invoke(instance); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); break; } } }}复制代码
将 Hello.java 编译后的 class 文件放在桌面,执行程序:
hellohello复制代码
修改 Hello 类,重新编译成 class 文件替换桌面文件:
/** * @author caojiantao */public class Hello { public void sayHello() { System.out.println("hello world"); }}复制代码
输出:
hellohellohello world复制代码
由于双亲无法加载 Hello 类,每次都由自定义的 ClassLoader 加载,从而达到热替换的效果。