第八篇:JAVA类加载机制源码分析

Java代码首先要编译成class文件字节码,在运行时通过JIT(即时编译器)编译成本地机器码,最后由ClassLoader将其加载解析成Class对象到内存中。通过ClassLoader的loadClass方法的源码加深对Java类加载机制的理解。


1. ClassLoader加载机制简述

Java的类加载遵循父类优先的原则,也就是说ClassLoader是一个有层级的树形组合体系,并且一个ClassLoader要加载一个类,首先逐层向上检查是否有加载器加载过该类,如果有,将结果逐层返回到下级。如果没有,继续检查直到有一层ClassLoader返回没有加载并且它不应该加载,那么该层的下一层就可以加载该类。


PS:父类优先的方式也不是万能的,在JavaEE Web应用程序中,也会使用子女优先加载的方式;


1.1 JVM提供3层基本的类加载平台

BootstrapClassLoader:加载JVM自身需要的类,注意在Hotspot JVM中它严格来说不是JVM类加载体系中的,它并不遵循上述机制,也不是下面ExtClassLoader的父类加载器;

ExtClassLoader:加载特定的类:System.getProperty("java.ext.dirs");也就是JRE/LIB/EXT目录下的类,加载的是sun公司的一些扩展包,它是AppClassLoader的父类加载器;

AppClassLoader:加载System.getProperty("java.class.path");就是classpath,看到这个你可能已经知道eclipse项目下.classpath的作用了,就是告诉AppClassLoader这些类由它加载;

继承自URLClassLoader的自定义类加载器,通过调用getSystemClassLoader获取自己的父加载器(AppClassLoader);


ClassLoader的类层次结构:

第八篇:JAVA类加载机制源码分析第八篇:JAVA类加载机制源码分析


图中的AppClassLoader和ExtClassLoader是Launcher的内部类;

到现在,我们可以也可以看出Java的ClassLoader使用了职责链设计模式,父优先加载,一定程度上保证了程序安全(防止恶意代码替换JSE核心类)。


1.2 JVM加载Class文件到内存的方式:

一是隐式加载:继承或引用某个类时,有JVM负责加载;

二是显式加载:在代码中调用loadClass(),Class.forName,ClassLoader的findClass方法等,显式加载中也可能包含隐式加载;


2. ClassLoader的重要方法:

findClass:主要由URLClassLoader实现,根据URLClassPath去指定地方查找class文件;取得要加载class文件的字节流;

defineCLass:可以将字节流解析成Class对象,该Class对象并未进行resolve;

resolveClass:对Class对象进行Link,载入引用类(超类,接口字段,方法签名,方法中的本地变量);

loadClass:采用默认的加载逻辑根据类名加载一个类,返回Class对象,调用前面3个方法实现;


3. 加载class文件的过程:

3.1 加载字节码到内存:findClass和defineClass方法

首先来看看URLClassLoader中的findClass方法,因为加载的第一步,找到并获取指定class文件的字节流;
[java] view plain copy
  1. protected Class<?> findClass(final String name)  
  2.             throws ClassNotFoundException  
  3.     {  
  4.         final Class<?> result;  
  5.         try {  
  6.             //获取特权,确保有权限可以读取到资源,这里  
  7.             result = AccessController.doPrivileged(  
  8.                     new PrivilegedExceptionAction<Class<?>>() {  
  9.                         public Class<?> run() throws ClassNotFoundException {  
  10.                             //将完整的类名转换成文件路径格式  
  11.                             String path = name.replace('.''/').concat(".class");  
  12.                             //在指定的URLPath中获取对应的class文件资源  
  13.                             Resource res = ucp.getResource(path, false);  
  14.                             if (res != null) {  
  15.                                 try {  
  16.                                     //获取成功将资源传入,最终获取未解析的Class对象  
  17.                                     return defineClass(name, res);  
  18.                                 } catch (IOException e) {  
  19.                                     //不能成功读取文件内容  
  20.                                     throw new ClassNotFoundException(name, e);  
  21.                                 }  
  22.                             } else {  
  23.                                 //不能获取资源  
  24.                                 return null;  
  25.                             }  
  26.                         }  
  27.                     }, acc);  
  28.         } catch (java.security.PrivilegedActionException pae) {  
  29.             //因为要在特权操作中抛出ClassNotFoundException,使用了PrivilegedExceptionAction回调  
  30.             //它会将异常包装,这里要解除包装  
  31.             throw (ClassNotFoundException) pae.getException();  
  32.         }  
  33.         if (result == null) {  
  34.             throw new ClassNotFoundException(name);  
  35.         }  
  36.         return result;  
  37.     }  

findClass方法的作用就是找到并借助defineClass方法返回Class对象(未解析),它获取特权权限去读取资源(Java安全模型对不同的代码是区分Domain的,不同的域(比如不同的项目的代码)可能对其他域的代码限制了访问自身资源的权限,具体可以参看http://www.ibm.com/developerworks/cn/java/j-lo-javasecurity/),并保证了在不同情况下可以正确的抛出ClassNotFoundException;

接下来看看findClass中使用的defineClass方法:
[java] view plain copy
  1. private Class<?> defineClass(String name, Resource res) throws IOException {  
  2.         long t0 = System.nanoTime();  
  3.         int i = name.lastIndexOf('.');  
  4.         URL url = res.getCodeSourceURL();  
  5.         //首先加载包  
  6.         if (i != -1) {  
  7.             String pkgname = name.substring(0, i);  
  8.             // Check if package already loaded.  
  9.             Manifest man = res.getManifest();  
  10.             definePackageInternal(pkgname, man, url);  
  11.         }  
  12.         // Now read the class bytes and define the class  
  13.         //使用nio,获取字节缓冲区  
  14.         java.nio.ByteBuffer bb = res.getByteBuffer();  
  15.         if (bb != null) {  
  16.             // Use (direct) ByteBuffer:  
  17.             CodeSigner[] signers = res.getCodeSigners();  
  18.             CodeSource cs = new CodeSource(url, signers);  
  19.             sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);  
  20.             return defineClass(name, bb, cs);  
  21.         } else {  
  22.             //获取不到缓冲区,直接InputStream获取字节数组  
  23.             byte[] b = res.getBytes();  
  24.             // must read certificates AFTER reading bytes.  
  25.             CodeSigner[] signers = res.getCodeSigners();  
  26.             CodeSource cs = new CodeSource(url, signers);  
  27.             sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);  
  28.             return defineClass(name, b, 0, b.length, cs);  
  29.         }  
  30.     }  

在这个方法中,我们可以看到对于class文件的读取策略,其中CodeSigner和CodeSource分别是代码签名和代码源,它们组合使用与前面提及的保护域机制(ProtectionDomain)当中,可见Java中类加载机制和安全模型是密不可分的。

3.2 验证和解析:defineClass方法和resovleClass方法:

defineClass首先进行:
(1)字节码验证;
(2)类准备:准备类中每个字段、方法和实现接口所需的数据结构
[java] view plain copy
  1. protected final Class<?> defineClass(String name, byte[] b, int off, int len,  
  2.                                          ProtectionDomain protectionDomain)  
  3.             throws ClassFormatError  
  4.     {  
  5.         //首先检查类名;阻止加载“java.”开头包内的类(应有BootStrapLoader加载);  
  6.         //确保同一个包内的Class拥有相同的证书  
  7.         protectionDomain = preDefineClass(name, protectionDomain);  
  8.         //根据CodeSource获取一个URL的字符串表示  
  9.         String source = defineClassSourceLocation(protectionDomain);  
  10.         //字节码验证;类准备(准备字段,方法,实现接口所必需的数据结构);  
  11.         Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);  
  12.         //通过证书为该类设置签名  
  13.         postDefineClass(c, protectionDomain);  
  14.         return c;  
  15.     }  

(3)解析:resolveClass方法,直接通过一个native方法实现
进行LINK,装入引用类,如超类,接口,字段,方法中使用的本地变量。

3.3 显式加载时的过程:

调用this.getClass().getClassLoader().loadClass("className");时的过程:

首先看看URLClassLoader中的loadClass方法:

[java] view plain copy
  1. protected Class<?> loadClass(String name, boolean resolve)  
  2.             throws ClassNotFoundException  
  3.     {  
  4.         //该getClassLoadingLock获取同步锁,该锁用并发Map保存(享元模式)  
  5.         synchronized (getClassLoadingLock(name)) {  
  6.             // First, check if the class has already been loaded  
  7.             Class<?> c = findLoadedClass(name);  
  8.             if (c == null) {  
  9.                 long t0 = System.nanoTime();  
  10.                 try {  
  11.                     // 这里体现了类加载机制中的父优先的查找机制  
  12.                     // 通过上滤直到parent为null  
  13.                     // 这时再去BootstrapClassLoader中查找,在运行用户程序时,这一步一般都是null  
  14.                     if (parent != null) {  
  15.                         c = parent.loadClass(name, false);  
  16.                     } else {  
  17.                         c = findBootstrapClassOrNull(name);  
  18.                     }  
  19.                 } catch (ClassNotFoundException e) {  
  20.                     // ClassNotFoundException thrown if class not found  
  21.                     // from the non-null parent class loader  
  22.                 }  
  23.   
  24.                 if (c == null) {  
  25.                     // If still not found, then invoke findClass in order  
  26.                     // to find the class.  
  27.                     long t1 = System.nanoTime();  
  28.                     //载入字节码到内存  
  29.                     c = findClass(name);  
  30.   
  31.                     // this is the defining class loader; record the stats  
  32.                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);  
  33.                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);  
  34.                     sun.misc.PerfCounter.getFindClasses().increment();  
  35.                 }  
  36.             }  
  37.             //解析连接类  
  38.             if (resolve) {  
  39.                 resolveClass(c);  
  40.             }  
  41.             return c;  
  42.         }  
  43.     }  

反映了JVM类加载机制的过程为:加载一个新类,逐层上滤检查父加载器,直到顶层(parent==null),再去BootStrapClassLoader中查找,如果返回null,就要调用findClass,resolveCass(需要的话);

3.4 初始化Class对象:

类的静态字段,静态初始化器会按顺序赋值/执行;

上面的过程概括一下:(1)加载class文件字节码到内存——>(2)将字节码转换成未Link的Class对象(安全验证/字节码验证/类准备)——>(3)resolveClass方法Link
——>(4)类初始化;

4. 小结

本篇总结了一下Java类加载机制的机制和过程,并从源码角度加以分析。在分析源码的过程也看到了Java中的安全模型的应用,以及并发控制(findClass中的lock并发Map)