Java虚拟机:类加载过程
在冯诺依曼的计算机模型中,任何程序都要加载到内存才能与CPU进行交流。字节码.class文件同样需要加载到内存中,才可以实例化。
其中,类加载器ClassLoader的使命就是加载.class文件到内存中。
在加载类时,使用的是Parents Delegation Model,即双亲委派模型。
Java类加载器是一个运行时核心基础设施模块,如下图,主要是在启动之初进行类的加载(Load)、链接(Link)、初始化(Init)。
第一步,Load阶段。
读取.class文件流,并转换为特定的数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。
第二步,Link阶段。
包括验证、准备和解析三个步骤。
- 验证:是更详细地校验,比如final是否合规、类型是否正确、静态变量是否合理等。
- 准备:为静态变量分配内存,设定默认初始值。
- 解析:解析类和方法之间相互引用正确性,完成内存布局。
第三步,Init阶段。
执行类构造器的<clinit>方法。
类加载器
类加载器类似于原始部落结构,存在权利等级制度。
最高一层是Bootstrap ClassLoader,它是在JVM启动时创建的,是最根基的类加载器,负责装载最核心的Java类,比如Object、System、String等。由C++编写。
第二层是Extension ClassLoader(JDK9之后变为了Platform ClassLoader,平台类加载器)。用以加载扩展的系统类,比如XML、加密、压缩等相关类。Java编写。
第三层是Application ClassLoader,应用类加载器,主要是加载用户自定义classpath下的类。
双亲委派模型
结合上图,我们来看一下ClassLoader的loadClass()方法:
1 /** 2 @Param resolve 是否链接 3 @Param name 类名 4 */ 5 protected Class<?> loadClass(String name, boolean resolve) 6 throws ClassNotFoundException 7 { 8 synchronized (getClassLoadingLock(name)) { 9 // 首先检查该类是否已经被加载过 10 Class<?> c = findLoadedClass(name); 11 if (c == null) { 12 long t0 = System.nanoTime(); 13 try { 14 // 如果父加载器不为空,就用它的父加载器去加载该类 15 if (parent != null) { 16 c = parent.loadClass(name, false); 17 } else { 18 // 到这里的话就是bootstrapClassLoader 19 // 这是一个本地方法,使用bootstrapClassLoader去加载该类 20 // 如果加载不了,就返回空 21 c = findBootstrapClassOrNull(name); 22 } 23 } catch (ClassNotFoundException e) { 24 25 } 26 27 // 如果返回空了,证明其父类加载器不能加载该类 28 if (c == null) { 29 30 long t1 = System.nanoTime(); 31 // 使用自己进行加载 32 c = findClass(name); 33 34 // this is the defining class loader; record the stats 35 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 36 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 37 sun.misc.PerfCounter.getFindClasses().increment(); 38 } 39 } 40 // 如果resove为true,就链接 41 if (resolve) { 42 resolveClass(c); 43 } 44 return c; 45 } 46 }
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载过的类。
如果底层的类加载器想要加载一个未知类,要非常有礼貌的向上级询问:”请问,这个类已经加载了吗?“,被询问的高层类首先要回答自己一个问题:我是否加载过此类?如果答案为否,那么又向上询问,直到Bootstrap ClassLoader为止,如果它也未加载过并且并加载,就会逐层交给下一层类加载器,反应在源码中就是第28行逐层递归退出。
我们看一下Bootstrap所有已经加载的类库:
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
Arrays.asList(urls).forEach(System.out::println);
结果:
file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/resources.jar file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/rt.jar file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/sunrsasign.jar file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/jsse.jar file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/jce.jar file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/charsets.jar file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/lib/jfr.jar file:/D:/Development%20Tools/Java/jdk1.8.0_201/jre/classes
Bootstrap的加载路径是可以追加的,不建议修改或删除原有的加载路径。通过-Xbootclasspath/a:参数可以增加类加载路径。
如果想在启动时观察加载了哪个jar包中的类,可以增加-XX:+TraceClassLoading参数,此参数在解决类冲突时非常实用。
什么时候需要自定义类加载器?
(1)隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。
(2)修改类加载方式。出了Bootstrap外,其他的加载并非都要引入。
(3)扩展加载源。比如从数据库、网络进行加载。
(4)防止源码泄露。Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义。
实现自定义类加载器的步骤:继承ClassLoader,重写findClass()方法,调用defineClass()方法。下面是一个例子:
public class MyClassLoader extends ClassLoader { private String path; private String name; public MyClassLoader(String path, String name) { this.path = path; this.name = name; } @Override public Class<?> findClass(String name) throws ClassNotFoundException { byte[] data = null; try { data = getClassBytes(name); return defineClass(name, data, 0, data.length); } catch (IOException e) { e.printStackTrace(); } return null; }
// 获取字节码流 private byte[] getClassBytes(String className) throws IOException { InputStream in = null; ByteArrayOutputStream baos = null; try { int len = 0; byte[] bytes = new byte[1024]; in = new FileInputStream(new File(path + className + ".class")); baos = new ByteArrayOutputStream(); while ((len = in.read(bytes)) != -1) { baos.write(bytes, 0, len); baos.flush(); } } catch (Exception e) { }finally { baos.close(); in.close(); } return baos.toByteArray(); } } public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { MyClassLoader classLoader = new MyClassLoader("E:\\","my classloader"); Class clz = classLoader.findClass("Dog"); Field[] fields = clz.getDeclaredFields(); System.out.println(Arrays.toString(fields)); }