Java 类加载机制分析

在编写 Java 程序时,我们所编写的 .java 文件经编译后,生成能被 JVM 识别的 .class 文件,.class 文件以字节码格式存储类或接口的结构描述数据。JVM 将这些数据加载至内存指定区域后,依此来构造类实例。

1. 类加载过程

JVM 将来自 .class 文件或其他途径的类字节码数据加载至内存,并对数据进行验证、解析、初始化,使其最终转化为能够被 JVM 使用的 Class 对象,这个过程称为 JVM 的类加载机制。

2. ClassLoader

ClassLoader 是 Java 中的类加载器,负责将 Class 加载到 JVM 中,不同的 ClassLoader 具有不同的等级,这将在稍后解释。

2.1 ClassLoader的作用

ClassLoader 的作用有以下 3点:

  • 将 Class 字节码解析转换成 JVM 所要求的 java.lang.Class 对象
  • 判断 Class 应该由何种等级的 ClassLoader 负责加载
  • 加载 Class 到 JVM中

2.2 ClassLoader的主要方法

ClassLoader 中包含以下几个主要方法:

  • defineClass

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    

    作用:将 byte 字节流转换为 java.lang.Class 对象。
    说明:字节流可以来源于.class文件,也可来自网络或其他途径。调用 defineClass 方法时,会对字节流进行校验,校验不通过会抛出 ClassFormatError 异常。该方法返回的 Class 对象还没有 resolve(链接),可以显示调用 resolveClass 方法对 Class 进行 resolve,或者在 Class 真正实例化时,由 JVM 自动执行 resolve.

  • resolveClass

    protected final void resolveClass(Class<?> c)
    

    作用 :对 Class 进行链接,把单一的 Class 加入到有继承关系的类树中。

  • findClass

    Class<?> findClass(String name)
    

    作用:根据类的 binary name,查找对应的 java.lang.Class 对象。
    说明:binary name 是类的全名,如 String 类的 binary name 为 java.lang.String。findClass 通常和 defineClass 一起使用,下面将举例说明二者关系。
    举例:java.net.URLClassLoader 是 ClassLoader 的子类,它重写了 ClassLoader中的 findClass 和 defineClass 方法,我们看下 findClass 的主方法体。

    // 入参为 Class 的 binary name,如 java.lang.String
    protected Class<?> findClass(final String name) throws ClassNotFoundException {
        // 以上代码省略
      
        // 通过 binary name 生成包路径,如 java.lang.String -> java/lang/String.class
        String path = name.replace('.', '/').concat(".class");
        // 根据包路径,找到该 Class 的文件资源
        Resource res = ucp.getResource(path, false);
        if (res != null) {
            try {
               // 调用 defineClass 生成 java.lang.Class 对象
                return defineClass(name, res);
            } catch (IOException e) {
                throw new ClassNotFoundException(name, e);
            }
        } else {
            return null;
        }
      
        // 以下代码省略
    }
    
  • loadClass

     public Class<?> loadClass(String name)
    

    作用:加载 binary name 对应的类,返回 java.lang.Class 对象
    说明:loadClass 和 findClass 都是接受类的 binary name 作为入参,返回对应的 Class 对象,但是二者在内部实现上却是不同的。loadClass 方法实现了 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;
      }
    }
    

    loadClass 方法的实现流程主要为:

    1. 调用 findLoadedClass 方法检查目标类是否被加载过,如果未加载过,则进行下面的加载步骤
    2. 如果存在父加载器,则调用父加载器的loadClass 方法加载类
    3. 父加载类不存在时,调用 JVM 内部的 ClassLoader 加载类
    4. 经过 2,3 步骤,若还未成功加载类,则使用该 ClassLoader 自身的 findClass 方法加载类
    5. 最后根据入参 resolve 判断是否需要 resolveClass,返回 Class 对象

    loadClas 默认是同步方法,在实现自定义 ClassLoader 时,通常的做法是继承 ClassLoader,重写 findClass 方法而非 loadClass 方法。这样既能保留类加载过程的等级加载机制和线程安全性,又可实现从不同数据来源加载类。

3. ClassLoader 的等级加载机制

上文已经提到 Java 中存在不同等级的 ClassLoader,且类加载过程中运用了等级加载机制,下面将进行详细解释。

3.1 Java 中的四层 ClassLoader

  • Bootstrap ClassLoader
    又称启动类加载器。Bootstrap ClassLoader 是 Java 中最顶层的 ClassLoader,它负责加载 JDK 中的核心类库,如 rt.jar,charset.jar,这些是 JVM 自身工作所需要的类。

    Bootstarp ClassLoader 由 JVM 控制,我们无法访问到这个类。虽然它位于类记载器的顶层,但它没有子加载器。需要通过 native 方法,来调用 Bootstap ClassLoader 来加载类,如下:

    private native Class<?> findBootstrapClass(String name);
    

    以下代码能够输出 Bootstrap ClassLoader 加载的类库路径:

    System.out.print(System.getProperty("sun.boot.class.path"));
    
    运行结果:
    C:\Software\Java8\jre\lib\resources.jar;
    C:\Software\Java8\jre\lib\rt.jar;
    C:\Software\Java8\jre\lib\jsse.jar;
    C:\Software\Java8\jre\lib\jce.jar;
    C:\Software\Java8\jre\lib\charsets.jar;
    C:\Software\Java8\jre\lib\jfr.jar;
    C:\Software\Java8\src.zip
    
  • Ext ClassLoader

    又称扩展类加载器。Ext ClassLoader 负责加载 JDK 中的扩展类库,这些类库位于 /JAVA_HOME/jre/lib/ext/ 目录下。如果我们将自己编写的类打包丢到该目录下,则该类将由 Ext ClassLoader 负责加载。

    以下代码能够输出 Ext ClassLoader 加载的类库路径:

    System.out.println(System.getProperty("java.ext.dirs"));
    
    运行结果:
    C:\Software\Java8\jre\lib\ext;
    C:\Windows\Sun\Java\lib\ext
    

    这里自定义了一个类加载器,全名为 com.eric.learning.java._classloader.FileClassLoader,我们想让它能够由 Ext ClassLoader加载,需要进行如下步骤:

    • 在 /JAVA_HOME/jre/lib/ext/ 目录下按照类的包结构新建目录
    • 将编译好的 FileClassLoader.class 丢到目录 /JAVA_HOME/jre/lib/ext/com/eric/learning/java/_classloader 下
    • 运行命令 jar cf test.jar com,生成 test.jar
    • 现在就可以用 ExtClassLoader 来加载类 FileClassLoader 了
      ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();
      Class<?> clazz = classLoader.loadClass("com.eric.learning.java._classloader.FileClassLoader");
      System.out.println(clazz.getName());
      

      ClassLoader.getSystemClassLoader() 获得的是 Ext ClassLoader 的子加载器, App ClassLoader

  • App ClassLoader

    继承关系图

    又称系统类加载器,App ClassLoader 负责加载项目 classpath 下的 jar 和 .class 文件,我们自己编写的类一般有它负责加载。App ClassLoader 的父加载器为 Ext ClassLoader。

    以下代码能够输出 App ClassLoader 加载的 .class 和 jar 文件路径:

     System.out.println(System.getProperty("java.class.path"));
    
    运行结果:
    C:\Coding\learning\target\classes;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.8.8\jackson-core-2.8.8.jar;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.8.8\jackson-databind-2.8.8.jar;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.8.8\jackson-annotations-2.8.8.jar
    

    笔者的项目通过 Maven 来管理,\target\class 是 Maven 工程里 .class 文件的默认存储路径,其余如 jackson-core-2.8.8.jar 是通过 Maven 引入的第三方依赖包。

  • Custom ClassLoader
    自定义类加载器,自定义类加载器需要继承抽象类 ClassLoader 或它的子类,并且所有 Custom ClassLoader 的父加载器都是 AppClassLoader,下面简单解释下这点。

    抽象类 ClassLoader 中有2种形式的构造方法:

    // 1
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    // 2
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    

    构造器1 以 getSystemClassLoader() 作为父加载器,而这个方法返回的即是 AppClassLoader。
    构造器2 表面上看允许我们指定当前类加载器的parent,但是如果我们试图将 Custom ClassLoader 的构造方法写成如下形式:

    public class FileClassLoader extends ClassLoader {
        public FileClassLoader(ClassLoader parent) {
            super(parent);
        }
    }
    

    在构造 FileClassLoader 实例时,new FileClassLoader( ClassLoader ) 将抛出异常:

    Java 的 security manager 不允许自定义类构造器访问上述的 ClassLoader 的构造方法。

3.2 等级加载机制

​ 如同我们在抽象类 ClassLoader 的 loadClass 方法所看到那样,当通过一个 ClassLoader 加载类时,会先自底向上检查父加载器是否已加载过该类,如果加载过则直接返回 java.lang.Class 对象。如果一直到顶层的 BootstrapClassLoader 都未加载过该类,则又会自顶向下尝试加载。如果所有层级的 ClassLoader 都未成功加载类,最终将抛出 ClassNotFoundException。如下图所示:


3.3 为何采用等级加载机制

​ 首先,采用等级加载机制,能够防止同一个类被重复加载,如果父加载器已经加载过某个类,再次加载时会直接返回 java.lang.Class 对象。

​ 其次,不同等级的类加载器的存在能保证类加载过程的安全性。如果只存在一个等级的 ClassLoader,那么我们可以用自定义的 String 类替换掉核心类库中的 String 类,这会造成安全隐患。而现在由于在 JVM 启动时就会加载 String 类,所以即便存在相同 binary name 的 String 类,它也不会再被加载。

4. 从 JVM 角度看类加载过程

​ 在 JVM 加载类时,会将读取 .class 文件中的类字节码数据,并解析拆分成 JVM 能识别的几个部分,这些不同的部分都将被存储在 JVM 的 方法区。然后 JVM 会在 堆区 创建一个 java.lang.Class 对象,用来封装该类在方法区的数据。 如下图所示:

​ 上文提到 .class 文件中的类字节码数据,会被 JVM 拆分成不同部分存储在方法区,而方法区实际就是用于存储类结构信息的地方。我们看看方法区都有哪些东西:

-   类及其父类的 binary name
-   类的类型 (class or interface)
-   访问修饰符 (public,abstract,final 等)
-   实现的接口的全名列表
-   常量池
-   字段信息
-   方法信息
-   静态变量
-   ClassLoader 引用
-   Class 引用

​ 方法区存储的这些类的各部分结构信息,能通过 java.lang.Class 类中的不同方法获得,可以说 Class 对象是对类结构数据的封装。

5. 一个简单的自定义类加载器例子

// 传入 .class 文件的绝对路径,加载 Class
public class FileClassLoader extends ClassLoader {

    // 重写了 findClass 方法
    @Override
    public Class<?> findClass(String path) throws ClassNotFoundException {
        File file = new File(path);
        if (!file.exists()) {
            throw new ClassNotFoundException();
        }
        
        byte[] classBytes = getClassData(file);
        if (classBytes == null || classBytes.length == 0) {
            throw new ClassNotFoundException();
        }
        return defineClass(classBytes, 0, classBytes.length);
    }

    private byte[] getClassData(File file) {
        try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new  
             ByteArrayOutputStream()) {
            byte[] buffer = new byte[4096];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[] {};
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容