图解Java类加载机制

前言

网上有很多的Java类加载机制的介绍, 但是对于初学者而言看起来都太过于深疏, 因此在本文用图解和例子的方式为本文的读者介绍Java的类加载机制。

类加载的概述

双亲委派加载机制

委派模型介绍:

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

通俗的理解就是:

  1. 遇见一个类需要加载的类,它会优先让父加载器去加载。层层传递。
  2. 每个类加载器都有自己的加载区域,它也只能在自己的加载区域里面寻找。
  3. 自定义类加载器也必须实现这样一个双亲委派模型。
  4. 双亲委派机制是隔离的关键, 如String.class
    • 一个JVM里面只能有一个String.class
    • 用户没法自定义个String.class出来。
    • 每个Classloader都有自己的加载区域,需要注意部分配置文件的存放地点。

代码理解:

URLClassLoader loader = (URLClassLoader) Init.class.getClassLoader();
while (loader != null) {
    System.out.println(loader.getClass().getName() + " 加载的路径:");
    URL[] urls = loader.getURLs();
    for (URL url : urls)
        System.out.println(url);
    System.out.println("----------------------------");

    loader = (URLClassLoader)loader.getParent();
}

System.out.println("BootstrapClassLoader加载路径: ");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
    System.out.println(url);
}

输出(有删减):

sun.misc.Launcher$AppClassLoader
file:${JAVA_HOME}/Contents/Home/jre/lib/charsets.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/deploy.jar
file:/Users/baidu/workspace/qyp/job/target/classes/
file:${M2_HOME}/org/apache/zookeeper/zookeeper/3.3.6/zookeeper-3.3.6.jar
file:${M2_HOME}/com/alibaba/fastjson/1.2.7/fastjson-1.2.7.jar
----------------------------
sun.misc.Launcher$ExtClassLoader
file:${JAVA_HOME}/Contents/Home/jre/lib/ext/cldrdata.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/ext/dnsns.jar
----------------------------
BootstrapClassLoader加载路径: 
file:${JAVA_HOME}/Contents/Home/jre/lib/resources.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/rt.jar
file:${JAVA_HOME}/Contents/Home/jre/lib/sunrsasign.jar

可以看出:

  • BootstrapClassLoader加载${JAVA_HOME}/jre/lib 下面的部分jar包。比如java.*、sun.*
  • ExtClassLoader加载${JAVA_HOME}/jre/lib/ext下面的jar包。比如javax.*
  • AppClassLoader加载用户classpath下面的jar包。
  • 如果自定义了classloader, 在符合双亲委派模型的基础上,它加载用户自定义classpath下的jar包, 例如tomcat的WEB-INF/classWEB-INF/lib.

类加载的隔离机制

通过不同的 完整类名 和 classloader, 可以区分两个类。好处为内存隔离(最常见的就是静态变量)。

  • 类名不一致一定不是同一个类
  • 类名一致类加载器不一致也不是同一个类(eaquels false)
  • 类名一致类加载器一致但是类加载器实例不一致也不是同一个类。

针对最后一点:类Foo.class, 如果ClassLoader loader1 = new URLClassLoader();ClassLoader loader2 = new URLClassLoader(); loader1和loader2去加载类Foo.class, 得到的Class也不是一个类。


且看问题:在web应用中假如部署了多个webapp. 为了方便共享就预先在Tomcat lib里面内置了部分类比如Spring、JDBC。而用户自备也有类似的Jar包。 这样会引起什么样的冲突?

答案是不会冲突。

Tomcat提供了一个Child优先的类加载机制:首先由子类去加载, 加载不到再由父类加载。就很好的规避了这个问题。WEB-INF/lib 目录下的类的加载优先级是优于Tomcat lib的。(配置文件在server.xml里面的<Loader delegate ="false"/> default false)上。 可见代码片段:
WebappClassLoaderBase#loadClass

boolean delegateLoad = delegate || filter(name, true);

针对Tomcat, 做一个加载路径的介绍:

  • Tomcat起始于catalina.sh里面的命令 java org.apache.catalina.startup.Bootstrap start
  • 因为显式的指定了java命令,因此
    • BootstrapClassLoader负责加载${JAVA_HOME}/jre/lib部分jar包
    • ExtClassLoader加载${JAVA_HOME}/jre/lib/ext下面的jar包
    • AppClassLoader加载bootstrap.jartomcat-juli.jar (只显示的指定了这两个jar包)
    • 之后Tomcat通过初始化了三个URLClassLoader, 并指定加载路径 (见catalina.properties#common.loader配置)
    • 除了common外, server和shardLoader的加载路径一般都没有显示的指定, 因此这三个Loader实际上都是URLClassLoader。
    • 同时,它顺便指定了当前线程的contextClassLoader(讲解见下面小节)。
    • Tomcat对于WEB应用的启动都是依赖于web.xml的, 里面配置的Filter、Listener、Servlet根据Tomcat的定义都是由WebappClassLoaderBase来加载的。
    • 毕竟Filter、Listener、Servlet等入口都是被WebappClassLoaderBase加载的,而一般开发者不会主动指定ClassLoader。那么除非指定了ClassLoader,所有的webapp都是它加载的(刚好它的加载空间包含了这些类)
    • 在需要Spring的时候已经由App自身加载得到, 就不会再去寻找Tomcat lib里面的Spring。
  • 自此,Tomcat的类加载区分完毕。 通过 “子优先” 这个机制,可以保证多个 Tomcat App 之间做到良好的隔离。

contextClassLoader

Thread.currentThread().getContextClassLoader()一般有两个用处:给SPI用, 找配置文件用。

SPI用处

之前讲解过java的委托加载机制1
看图

UserClassLoader -> AppClassLoader->ExtClassLoader -> Bootstrap

委派链左边的ClassLoader就可以很自然的使用右边的ClassLoader所加载的类。

情况反过来,右边的ClassLoader所加载的代码需要反过来去找委派链靠左边的ClassLoader去加载东西怎么办呢?没辙,双亲委托机制是单向的,没办法反过来从右边找左边。

ServiceLoader.load(Class.class); 在加载类的时候, ServiceLoader由BootStrap加载,而一般的SPI都是在用户的classpath下。鉴于方法调用默认是使用的调用类的ClassLoader去加载, 显然BootStrap是加载不了没在它的路径下的Class的, 这个时候就可以传入一个Thread.currentThread().getContextClassLoader(), 就可以很轻松的找到资源文件.

找文件用处

这个跟上诉的SPI机制其实也差不多, 都是每个ClassLoader负责一定的区域, 如果当前区域找不到再使用线程的Loader去找。
比如在Tomcat中执行一个 new File(), 会不会发现文件到${catalina.home}/bin里面去了?

类加载的顺序

当需要用一个类的时候, 必须先加载它。

顺序概述

老生常谈:

  1. 装载:查找和导入Class文件;
  2. 链接:把类的二进制数据合并到JRE中;
    • 校验:检查载入Class文件数据的正确性;
    • 准备:给类的静态变量分配存储空间;
    • 解析:将符号引用转成直接引用;
  3. 初始化:对类的静态变量,静态代码块执行初始化操作

解读(Useless.class为例):

public class Useless {

    public Serializable s1 = new Serializable() {
        {
            System.out.println("域变量");
        }
    };

    public static Serializable s2 = new Serializable() {
        {
            System.out.println("静态域变量");
        }
    };
    public static int num = 3;

    static {
        System.out.println("静态代码块");
    }

    {
        System.out.println("代码块");
    }

}
  • 装载即通过查找 Useless.class, 得到二进制码。并生产出该类的数据结构,得到一个Class对象。
  • 校验即校验二进制码的数据,比如编译级别、是否符合Java规范等等
  • 准备即为 s2 和 num 赋值 null 和 0。
  • 解析可以参考这篇文字java – JVM的符号引用和直接引用
  • 初始化即另s2得到值,令num得到3。

可以看到, 类加载的整个过程跟域变量和代码块都是没什么关系的

类加载的一般方式

方式一:
Class.forName
方式二
ClassLoader.loadClass

见代码片段:

Class z;
z =  Class.forName("Useless");   // 1
z = Class.forName("Useless", true, MainFather.class.getClassLoader());  // 2
z = Class.forName("Useless", false, MainFather.class.getClassLoader());  // 3

z = MainFather.class.getClassLoader().loadClass("Useless");   // 4

一般理解为 1, 3, 4 等价。2会初始化类里面的静态元素和静态代码块(即类加载的初始化步骤)。

类加载的触发点

摘自http://www.cnblogs.com/ITtangtang/p/3978102.html

  (1) 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

  (2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  (3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

类的实例化

类只有在加载进入JVM之后才能被使用, 但是一般情况下还需要把类做实例化操作后来用。 一般区分为显示的实例化和隐式的示例化。

类的实例化的目的是为了得到一个类的对象。

  • new 方法为隐式的实例化。方便可用性高。
  • newInstance()为显式的实例化。必须要一个无参构造器。
  • 两种实例化方式都需要ClassLoader的参与。显示的实例化往往指定,隐式的实例化则默认是调用的这个类的类加载器。
  • 实例化的目的都是为了得到类的对象。而类在实例化之前已经初始化完毕。
  • 实例化的时候会为域变量赋值,并执行代码块的方法。

多线程环境下,为何也只有一个Class的对象

2019年04月11日
闲来无事翻了翻老文章。 发现忘了介绍Java类加载如何保证在内存里面只有这一个类对象的。
声明 这个题目是个伪命题,如果自研ClassLoader, 然后还不符合规范去实现, JVM里面肯定是会有多个Class的对象的。
本小节只讲多线程环境下的Case。

BootStrap ClassLoader

  既然是类加载,在双亲委派模型下, 类似于 ”rt.jar“ 一类的类文件, BootStrapClassLoader由 加载。
  具体源码没有翻过, 不过main() 函数里面一定会触发加载Object.class, String.class。
  此时不存在多线程的情况(`执行多个java命令那叫多进程,不是一个JVM`)。

URLClassLoader

   ExtClassLoader 是它的实现类。 
   最终的加载是委托给 ClassLoader#loadClass(String, boolean)。 
   它使用了一个同步块,同步块的对象锁锁的是 #getClassLoadingLock(String)。 
   使用了ConcurrentHashMap的一个特性: putIfAbsent。 
   因此,多线程环境中:
   putIfAbsent 保证了只有一个线程能往ConcurrentHashMap里面塞对象,且他们GET的对象一定是同一个。
   synchronized 保证了多线程环境下,只有一个类能够被加载。 之后的类加载都是获取加载好的类。

AppClassLoader

   AppClassLoader继承于URLClassLoader, 但是重写了ClassLoader#loadClass(String, boolean)。
   这个逻辑很简单,先找是否加载了这个类knownToNotExist(String), 方法是同步的。
   否则沿用ClassLoader#loadClass(String, boolean)逻辑。

如上, 至少在JDK原生的ClassLoader环境下, JDK通过synchronized/ConcurrentHashMap 等机制保证了各种环境下Class对象的唯一性。
至于BootStrapClassLoader, 没有翻源码。StringObject 肯定是独一份的。

图解和举例

普通Java应用

见类:Useless.java

import java.io.Serializable;

public class Useless extends UselessParent {

    public Serializable s1 = new Serializable() {
        {
            System.out.println("域变量");
        }
    };

    public static Serializable s2 = new Serializable() {
        {
            System.out.println("静态域变量");
        }
    };

    static {
        System.out.println("静态代码块");
    }

    {
        System.out.println("代码块");
    }

}

见类:UselessParent.java

import java.io.Serializable;

public class UselessParent {

    public Serializable s1 = new Serializable() {
        {
            System.out.println(getClass() + "域变量");
        }
    };

    public static Serializable s2 = new Serializable() {
        {
            System.out.println(getClass() + "静态域变量");
        }
    };

    static {
        System.out.println("静态代码块");
    }

    {
        System.out.println(getClass() + "代码块");
    }

}

和执行类:MainFather.java

public class MainFather {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
            InstantiationException {
        Useless u;
        // u = new Useless();
        System.out.println(Useless.s2);
        System.out.println("-----------------------------------------");
        Class z;
        z =  Class.forName("com.baidu.qyp.job.clazz.Useless");
        System.out.println("-----------------------------------------");
        z = Class.forName("com.baidu.qyp.job.clazz.Useless", true, MainFather.class.getClassLoader());
        System.out.println("-----------------------------------------");
        z = Class.forName("com.baidu.qyp.job.clazz.Useless", false, MainFather.class.getClassLoader());
        System.out.println("-----------------------------------------");

        z = MainFather.class.getClassLoader().loadClass("com.baidu.qyp.job.clazz.Useless");
        System.out.println("-----------------------------------------");

        u = (Useless) z.newInstance();
        System.out.println("-----------------------------------------");
        u = new Useless();
    }
}

执行结果:

class UselessParent$2静态域变量
静态代码块
class Useless$2静态域变量
静态代码块
Useless$2@378bf509
-----------------------------------------
-----------------------------------------
-----------------------------------------
-----------------------------------------
-----------------------------------------
class UselessParent$1域变量
class Useless代码块
class Useless$1域变量
class Useless代码块
-----------------------------------------
class UselessParent$1域变量
class Useless代码块
class Useless$1域变量
class Useless代码块

执行附图:

Created with Raphaël 2.2.0 java MainFather 初始化MainFather Useless.s2触发Useless初始化 先初始化UselessParent 赋值UselessParent静态域 执行UselessParent静态代码 Useless同UselessParent 打印输出Useless.s2 余下四步皆只是加载类, 因为类已经被加载,因此无任何操作 newInstance()和new效果一致 实例化子类时优先实例化父类 因为父类和子类已经初始化,不再初始化 优先域变量,其次代码块 程序结束

这也很好的解释了一个问题: 为什么静态元素和静态代码块在一个虚拟机里面只会执行一次:

  1. 默认习惯都是不会指定ClassLoader的,所属类也就只有一次初始化过程。
  2. 赋值静态域,或者执行静态代码块,是在类加载的流程中执行的。而这样的操作只会有一次。
  3. 赋值域,或者执行代码块,是在类实例化的流程中执行的,这样的操作根据程序需求可能有多次。

日常Web应用

这里的Web应用指的就是Tomcat Web应用。 其中的Tomcat启动模块跟普通Java应用并无区别。 附上一张Spring Web流程图。

Created with Raphaël 2.2.0 java BootStrap start 初始化BootStrap 遇见类调用则初始化,遇见new或者newInstance()则实例化 这些类的加载器都是AppClassLoader Tomcat使用反射的方式初始化了Tomcat_lib 上述步骤指定了Loader的是URLClassLoader Tomcat指定了WEB-INF/class等位置由Tomcat加载 使用的Loader为WebappClassLoaderBase 这里全都是用户代码,或者引用的第三方jar包 所有类实例化方式完全一致 初始化子类时先初始化父类 实例化子类时优先实例化父类 因为父类和子类已经初始化,不再初始化 优先域变量,其次代码块 程序结束

参考文章:
http://www.cnblogs.com/ityouknow/p/5603287.html
http://www.cnblogs.com/ITtangtang/p/3978102.html

附录

UserClassLoader AppClassLoader ExtClassLoader BootStrap 委托父加载器 UserClassLoa der 用户的Loader 委托父加载器 只关心java. * sun.*包 委托父加载器 UserClassLoader AppClassLoader ExtClassLoader BootStrap

  1. 双亲委托加载的序列图 ↩︎

  • 6
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值