`

深度分析 Java 的 ClassLoader 机制(源码级别)

    博客分类:
  • java
阅读更多

原文http://blog.jobbole.com/96145/

作者伯乐在线 - HollisChuang

写在前面:Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中,JVM在加载类的时候,都是通过ClassLoaderloadClass()方法来加载class的,loadClass使用双亲委派模式

为了更好的理解类的加载机制,我们来深入研究一下ClassLoader和他的loadClass()方法。

源码分析

1
public abstract class ClassLoader

ClassLoader类是一个抽象类,sun公司是这么解释这个类的:

1
2
3
4
5
6
7
/**
 * A class loader is an object that is responsible for loading classes. The
 * class ClassLoader is an abstract class.  Given the binary name of a class, a class loader should attempt to
 * locate or generate data that constitutes a definition for the class.  A
 * typical strategy is to transform the name into a file name and then read a
 * "class file" of that name from a file system.
**/

大致意思如下:

class loader是一个负责加载classes的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,class loader尝试定位或者产生一个class的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。

接下来我们看loadClass方法的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
        }
    }

还是来看sun公司对该方法的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
     * Loads the class with the specified binary name.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     *
     *
     *    Invoke {<a href="http://www.jobbole.com/members/57845349">@link</a> #findLoadedClass(String)} to check if the class
     *   has already been loaded. 
     *
     *    Invoke the {<a href="http://www.jobbole.com/members/57845349">@link</a> #loadClass(String) loadClass} method
     *   on the parent class loader.  If the parent is null the class
     *   loader built-in to the virtual machine is used, instead. 
     *
     *    Invoke the {<a href="http://www.jobbole.com/members/57845349">@link</a> #findClass(String)} method to find the
     *   class. 
     *
     *
     *
     *  If the class was found using the above steps, and the
     * resolve flag is true, this method will then invoke the {<a href="http://www.jobbole.com/members/57845349">@link</a>
     * #resolveClass(Class)} method on the resulting Class object.
     *
     *  Subclasses of ClassLoader are encouraged to override {<a href="http://www.jobbole.com/members/57845349">@link</a>
     * #findClass(String)}, rather than this method. 
     *
     *  Unless overridden, this method synchronizes on the result of
     * {<a href="http://www.jobbole.com/members/57845349">@link</a> #getClassLoadingLock getClassLoadingLock} method
     * during the entire class loading process.
     *
     */

大致内容如下:

使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String)方法检查这个类是否被加载过 使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机内置的加载器调用findClass(String)方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。 ClassLoader的子类最好覆盖findClass(String)而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)

接下来,我们开始分析该方法。

**protected Class> loadClass(String name, boolean resolve)** 该方法的访问控制符是`protected`,也就是说该方法**同包内和派生类中可用** 返回值类型`Class

>

,这里用到**泛型**。这里使用通配符?作为泛型实参表示对象可以 接受任何类型(类类型)。因为该方法不知道要加载的类到底是什么类,所以就用了通用的泛型。String name要查找的类的名字,boolean resolve,一个标志,true表示将调用resolveClass(c)`处理该类

throws ClassNotFoundException 该方法会抛出找不到该类的异常,这是一个非运行时异常

synchronized (getClassLoadingLock(name)) 看到这行代码,我们能知道的是,这是一个同步代码块,那么synchronized的括号中放的应该是一个对象。我们来看getClassLoadingLock(name)方法的作用是什么:

1
2
3
4
5
6
7
8
9
10
11
protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }

以上是getClassLoadingLock(name)方法的实现细节,我们看到这里用到变量parallelLockMap ,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个putIfAbsent(className, newLock);方法来给刚刚创建好的对象赋值,这个方法的作用我们一会讲。那么这个parallelLockMap变量又是哪来的那,我们发现这个变量是ClassLoader类的成员变量:

1
private final ConcurrentHashMap parallelLockMap;

这个变量的初始化工作在ClassLoader的构造函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap();
            package2certs = new ConcurrentHashMap();
            domains =
                Collections.synchronizedSet(new HashSet());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable();
            domains = new HashSet();
            assertionLock = this;
        }
    }

这里我们可以看到构造函数根据一个属性ParallelLoadersRegistered状态的不同来给parallelLockMap 赋值。 我去,隐藏的好深,好,我们继续挖,看看这个ParallelLoaders又是在哪赋值的呢?我们发现,在ClassLoader类中包含一个静态内部类private static class ParallelLoaders,在ClassLoader被加载的时候这个静态内部类就被初始化。这个静态内部类的代码我就不贴了,直接告诉大家什么意思,sun公司是这么说的:Encapsulates the set of parallel capable loader types,意识就是说:封装了并行的可装载的类型的集合。

上面这个说的是不是有点乱,那让我们来整理一下: 首先,在ClassLoader类中有一个静态内部类ParallelLoaders,他会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么他就给parallelLockMap定义,就是new一个 ConcurrentHashMap(),那么这个时候,我们知道如果当前的加载器是具有并行能力的,那么parallelLockMap就不是Null,这个时候,我们判断parallelLockMap是不是Null,如果他是null,说明该加载器没有注册并行能力,那么我们没有必要给他一个加锁的对象,getClassLoadingLock方法直接返回this,就是当前的加载器的一个实例。如果这个parallelLockMap不是null,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。然后就是创建一个新的Object对象,调用parallelLockMapputIfAbsent(className, newLock)方 法,这个方法的作用是:首先根据传进来的className,检查该名字是否已经关联了一个value值,如果已经关联过value值,那么直接把他关联 的值返回,如果没有关联过值的话,那就把我们传进来的Object对象作为value值,className作为Key值组成一个map返回。然后无论 putIfAbsent方法的返回值是什么,都把它赋值给我们刚刚生成的那个Object对象。 这个时候,我们来简单说明一下getClassLoadingLock(String className)的作用,就是: 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。

Class c = findLoadedClass(name); 在这里,在加载类之前先调用findLoadedClass方法检查该类是否已经被加载过,findLoadedClass会返回一个Class类型的对象,如果该类已经被加载过,那么就可以直接返回该对象(在返回之前会根据resolve的值来决定是否处理该对象,具体的怎么处理后面会讲)。 如果,该类没有被加载过,那么执行以下的加载过程

1
2
3
4
5
6
7
8
9
10
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
}

如果父加载器不为空,那么调用父加载器的loadClass方法加载类,如果父加载器为空,那么调用虚拟机的加载器来加载类。

如果以上两个步骤都没有成功的加载到类,那么

1
c = findClass(name);

调用自己的findClass(name)方法来加载类。

这个时候,我们已经得到了加载之后的类,那么就根据resolve的值决定是否调用resolveClass方法。resolveClass方法的作用是:

链接指定的类。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java™规范中的Execution描述进行链接……

至此,ClassLoader类以及loadClass方法的源码我们已经分析完了,那么。结合源码的分析,我们来总结一下:

总结

java中的类大致分为三种:

1.系统类 2.扩展类 3.由程序员自定义的类

类装载方式,有两种:

1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。 2.显式装载, 通过class.forname()等方法,显式加载需要的类

类加载的动态性体现:

一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的 基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵, 这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现

java类装载器

1
2
3
4
5
6
7
8
9
10
11
Java中的类装载器实质上也是类,功能是把类载入jvm中,值得注意的是jvm的类装载器并不是一个,而是三个,层次结构如下:
 
  Bootstrap Loader  - 负责加载系统类
 
        |
 
      - - ExtClassLoader  - 负责加载扩展类
 
                |
 
               - - AppClassLoader  - 负责加载应用类

为什么要有三个类加载器,一方面是分工,各自负责各自的区块,另一方面为了实现委托模型,下面会谈到该模型

类加载器之间是如何协调工作的

前面说了,java中有三个类加载器,问题就来了,碰到一个类需要加载时,它们之间是如何协调工作的,即java是如何区分一个类该由哪个类加载器来完成呢。 在这里java采用了委托模型机制,这个机制简单来讲,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类

下面举一个例子来说明,为了更好的理解,先弄清楚几行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Public class Test{
 
    Public static void main(String[] arg){
 
      ClassLoader c  = Test.class.getClassLoader();  //获取Test类的类加载器
 
        System.out.println(c);
 
      ClassLoader c1 = c.getParent();  //获取c这个类加载器的父类加载器
 
        System.out.println(c1);
 
      ClassLoader c2 = c1.getParent();//获取c1这个类加载器的父类加载器
 
        System.out.println(c2);
 
  }
 
}

运行结果:

1
2
3
4
5
……AppClassLoader……
 
……ExtClassLoader……
 
Null

可以看出Test是由AppClassLoader加载器加载的,AppClassLoaderParent 加载器是 ExtClassLoader,但是ExtClassLoaderParentnull 是怎么回事呵,朋友们留意的话,前面有提到Bootstrap Loader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,所以在java程序代码里试图打印出其内容时,我们就会看到输出为null

类装载器ClassLoader(一个抽象类)描述一下JVM加载class文件的原理机制

类装载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件,在java中类装载器把一个类装入JVM,经过以下步骤:

1、装载:查找和导入Class文件 2、链接:其中解析步骤是可以选择的 (a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用 3、初始化:对静态变量,静态代码块执行初始化工作

类装载工作由ClassLoder和其子类负责。JVM在运行时会产生三个ClassLoader:根装载器ExtClassLoader(扩展类装载器)和AppClassLoader,其中根装载器不是ClassLoader的子类,由C++编写,因此在java中看不到他,负责装载JRE的核心类库,如JRE目录下的rt.jar,charsets.jar等。ExtClassLoaderClassLoder的子类,负责装载JRE扩展目录ext下的jar类包;AppClassLoader负 责装载classpath路径下的类包,这三个类装载器存在父子层级关系****,即根装载器是ExtClassLoader的父装载 器,ExtClassLoader是AppClassLoader的父装载器。默认情况下使用AppClassLoader装载应用程序的类

Java装载类使用“全盘负责委托机制”。“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入;“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全方面考虑的,试想如果一个人写了一个恶意的基础类(如java.lang.String)并加载到JVM将会引起严重的后果,但有了全盘负责制,java.lang.String永远是由根装载器来装载,避免以上情况发生 除了JVM默认的三个ClassLoder以外,第三方可以编写自己的类装载器,以实现一些特殊的需求。类文件被装载解析后,在JVM中都有一个对应的java.lang.Class对象,提供了类结构信息的描述。数组,枚举及基本数据类型,甚至void都拥有对应的Class对象。Class类没有public的构造方法,Class对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构造的。

分享到:
评论

相关推荐

    java classloader

    `JAVA2深度历险.jpg`可能是关于Java 2 SDK的深度探索的图像资料,可能涵盖了ClassLoader的部分内容。而`CH_02.深入类别载入器.pdf`直接指向ClassLoader的深入讨论,可能包括自定义ClassLoader的实现、双亲委派模型等...

    ClassLoader原理

    博客中提到的源码分析,意味着我们可以深入到Java运行时系统的内部,查看ClassLoader的具体实现,例如`sun.misc.Launcher$AppClassLoader`是Java应用的默认类加载器,而`BootStrapClassLoader`是根加载器,负责加载...

    dubbo源码解析2

    5. **Java其他内容**:除了以上提及的知识点外,还需要对Java中的序列化、SPI、代理、ClassLoader、ScriptEngine等内容有一定的了解。 6. **设计模式**:设计模式在Dubbo的实现中扮演着极其重要的角色,通过学习...

    ClassLoader总结

    ClassLoader是Java语言中的一个重要组成部分,它是负责加载类到JVM(Java虚拟机)中的机制。这篇博客主要讨论了ClassLoader的工作原理、分类以及如何自定义ClassLoader。ClassLoader与源码和工具的关联在于,理解其...

    JDK源码包(包含sun包下的源码)

    然而,理解这些源码可以帮助开发者更好地理解Java平台的工作机制,尤其是在进行高性能或低级别编程时。例如,`sun.misc.Unsafe`类提供了对内存操作的直接访问,这在某些高效率的数据结构实现中非常有用。 本压缩包...

    JAVA源码阅读的真实体会.pdf

    总的来说,Java源码阅读是一个渐进的过程,需要结合实际项目经验,逐步积累,才能真正从源码中获得深度理解和技能提升。在整个过程中,保持好奇心,坚持不懈,是成为一名出色程序员的必备品质。

    dubbo源码解析

    Dubbo源码分析首先从源码阅读路径开始,然后介绍Dubbo的架构和HelloWorld例子。在核心机制分析部分,从设计模式、Bean加载开始,深入到Spring的可扩展Schema和bean加载流程。接着,分析了Dubbo的Extension机制,其中...

    jdk8源码.zip

    在OpenJDK 8源码中,类加载机制主要由`src/share/classes/java/lang/ClassLoader`目录下的`ClassLoader`类及其子类实现。理解类加载的过程,包括双亲委派模型、自定义类加载器的创建等,可以帮助我们解决类冲突、...

    深入java虚拟机第二版

    《深入Java虚拟机第二版》是一本专注于Java技术体系核心的权威著作,它详细解析了Java虚拟机(JVM)的工作原理,为开发者提供了深入理解Java程序运行机制的关键知识。这本书不仅包含了源码分析,还特别优化了PDF...

    hotspot-jdk8u262-ga.tar.gz

    《深入解析Java HotSpot虚拟机源码:JDK8u262版》 Java HotSpot虚拟机作为Oracle JDK的核心组成部分,...在实际开发中,结合源码分析,我们可以更好地理解和利用HotSpot的特性,为我们的应用程序提供更好的性能保障。

    安卓逆向学习笔记之FART主动调用组件设计和源码分析.docx

    ### 安卓逆向学习笔记之FART主动调用组件设计和源码分析 #### 一、概述 本文档旨在探讨安卓逆向工程中的一个重要工具——FART(Find And Replace Tool),并着重介绍如何利用该工具进行主动调用组件的设计与源码...

    高薪面试题 java

    - **查看过的JDK源码**:了解应聘者对Java核心API的熟悉程度。如`HashMap`、`ArrayList`等数据结构实现。 - **常用库**:例如Spring框架的核心组件或Hibernate等ORM框架。了解这些库可以帮助评估应聘者的技术广度和...

    深入理解jvm源码

    《深入理解Java虚拟机》是Java开发者深入了解JVM(Java Virtual Machine)的必备书籍,尤其对于想要提升技术深度、优化程序性能的工程师来说,更是不可或缺的参考资料。这本书的第二版全面覆盖了JVM的最新发展,包括...

    jvm虚拟机源码学习资料

    "jvm虚拟机源码学习资料"是一份专门针对JVM内部机制深度探索的学习资源,其中包含了配置jar包cglib-2.2.2.jar,这是一款常用的Java代码生成库,常用于动态代理和AOP(面向切面编程)场景。 1. JVM结构与工作原理 ...

    java+经典面试题+答案

    这些面试题涵盖了Java多线程、类加载机制以及前端React框架中的组件扩展技术,这些都是面试中常见的技术点,对理解和提升开发者的技术水平至关重要。熟悉这些知识点将有助于在面试中展现出扎实的理论基础和技术实力...

    java8rt.jar源码-fwh-JVM:对于JVM情况

    类加载机制深度解析 Jvm Java 类加载机制深度解析 wenhao, 文浩 , Jvm, classLoader , Java 准备: Markdown编写工具 有道云笔记 Idea开发工具 GItHub项目地址 分析: 类加载运行全过程: 当我们用java命令运行某个...

    迷宫java实现

    在Java中,我们可以使用`Class.getResource()`或`ClassLoader.getResource()`方法来加载这些资源。 “src”目录则是源代码存放的地方,按照Java的标准目录结构,可能包含一个或多个包(package),每个包下有对应的...

    JDK 性能

    本文将深入探讨JDK性能相关的知识,主要涉及源码分析和工具使用。 首先,了解JDK的源码对于性能优化至关重要。通过阅读和理解JDK源码,我们可以得知Java内部的运行机制,从而找到性能瓶颈并进行针对性优化。例如,...

    javaSourceLearn:jdk源码构建

    List、Set、Map三大接口以及其实现类(ArrayList、LinkedList、HashSet、TreeSet、HashMap、LinkedHashMap等)的源码分析能帮助我们更高效地利用数据结构,优化程序性能。 标签“系统开源”表明这个项目鼓励开放和...

    java面试宝典

    我们可以得知这本《java面试宝典》旨在帮助求职者全面准备Java开发相关的技术面试,其中不仅包含了大量关于Java核心知识点的解析,还涉及了并发编程、JVM原理以及源码层面的深度理解,非常适合即将参加Java技术面试...

Global site tag (gtag.js) - Google Analytics