`
sunlujing
  • 浏览: 177938 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

ClassLoader 深入解析

    博客分类:
  • j2se
阅读更多

 

 

本文是根据是在《北京圣思园深入JAVA虚拟机系列视频》的基础上自己整理而来,内容范围没有超过其系列所述,在此给予说明。

在进入ClassLoader的分析之前我们先看一个JAVA程序例子。

 

class Singleton {

         /* case 1 */

         private static Singleton singleton = new Singleton();

         public static int counter1;

         public static int counter2 = 0;

 

         /**

          * case 2

* public static int counter1 = 0;

* public static int counter2 = 0;

          * private static Singleton singleton = new Singleton();

          */

         private Singleton() {

                   counter1++;

                   counter2++;

         }

 

         public static Singleton getInstance() {

                   return singleton;

         }

}

 

public class MyTest {

         public static void main(String[] args) {

                   Singleton singleton = Singleton.getInstance();

                   System.out.println("counter1 = " + singleton.counter1);

                   System.out.println("counter2 = " + singleton.counter2);

         }

}

/**

 * result in case 1:

 * counter1 = 1

 * counter2 = 0

 * result in case 2:

 * counter1 = 1

 * counter2 = 1

 */


 

上面的代码在case1 case2 条件下运行结果却不一样,仅仅由于private static Singleton singleton = new Singleton();位置不同,要想了解其中的原因需要从类的使用时JVM完成的动作说起,在一个类被JVM使用时大致经历如下三步(加载 链接(验证--准备--解析) 初始化)


 

那么一个类被JVM使用时,必须预先经历如上图所述的加载过程。那么什么样的条件才会触发上述过程的执行呢?JAVA程序使用类分为主动使用和被动使用,在JVM的实现规范中要求,所有类的“主动使用“虚拟机才执行上述过程初始化相应的类,那么问题就归结为“主动使用”的意义。

  1. 创建类的实例。Object A = new ClassA();

2.  访问某个类或接口的静态变量或对静态变量赋值。如Class A{static a} 访问A.a时。需要 指出的是访问类的static final int x = 0(编译时常量)并不被认为是类的主动使用,同样 的假如有条件 Class A extends B;B{static a}如果使用A.a时只会初始化类B,这种情况被认 为是对父类的主动使用。

3. 调用类的静态方法

4.使用反射机制(Class.ForName(xxx),ClassLoader.load(并不会初始化类)

5. 初始化一个类的子类时,父类也被主动使用

6.   启动类(java TestMain

 

下面文章将针对上述过程给出比较详细的说明。

加载过程

总的来说类的加载是JVM使用类加载器(如系统类加载器、扩展加载器、根加载器)在特定的加载路径里寻找class文件,并将class文件中的二进制数据读入到内存中,其中class的数据结构被放置在运行时数据区的方法区类,并且在堆区里创建该类的Class对象,用来封装类的数据结构信息。其中类加载类的方式有:文件系统加载、网络加载、zip jar 归档文件加载、数据库中提取、动态编译的源文件加载。

 

类加载的最终产品是位于堆区中的Class对象,其封装了类在方法区内数据结构,并且向Java程序员提供了访问方法区内数据结构的接口,需要指出的是,类的加载并不都是主动使用时才加载,加载器可以实现为有预加载功能,如使用一定的算法预测类的使用。在上面的叙述中我们提到过JVM使用类加载器对class文件进行加载(本文后面部分将着重描述类的加载机制)。


 

连接过程

类加载后,就是连接阶段了,连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行环境中去。连接的第一个阶段是类的验证,验证的内容如下:

1.类文件结构的检查,确保类的文件遵循java类文件的固定格式。

2. 语义检查:确保类本身符合java语言的语法规定,比如验证final类型没有子 类,final方法没有被从写,private没有被重写。

3.  字节码验证:确定字节码流可以被java虚拟机安全的执行。

4.二进制兼容验证:确保相互应用的类之间协调一致。

做完验证之后就是类的准备阶段,完成的工作为类的静态变量分配内存并设置为初始值。如有类

Sample{

Static int a = 1;

Static long b;

}

系统会为 a 分配4个字节,并设置初始值为 0,为b分配8个字节并设置初始值为 0.

 

做完类的准备工作之后就是类的解析,主要工作就是把类中的二进制中的符号引用替换为直接引用。我们举个例子 void show{  objectA.print()};objectA.print() 就是对ClassA的一个符号引用,经过解析之后该处的代码会被一个指向ClassA中方法区print方法的指针。

初始化过程

下面是类的初始化过程,初始化的主要步骤为:检查该类是否已经被加载和连接;如果该类有父类,且没有初始化,对父类进行加载 连接 初始化;假如类中存在初始化语句,依次执行初始化语句。而静态变量的声明以及静态代码块都被看作是类的初始化语句,java虚拟机会按照初始化语句在类文件中的顺序依次来执行他们。如 static int a =1,static{ a = 3}这样的语句都会被JVM顺序执行。

         前面提到,当JVM初始化一个类时要求他的父类已经被初始化,这样的机制并不适用于接口,初始化一个类时,它实现的接口并不需要被初始化,初始化一个接口时,其父接口也不需要被初始化,只有当程序首次使用接口中的静态变量时,才会导致接口的初始化。

         通过上面的论述,我们大致对类的使用有了一个初步的了解,接下来我们将分析本文开始时提出的那个程序的运行结果

case 1 中,在MyTest那种使用 Singleton singleton = Singleton.getInstance();这样的语句为类的主动使用这会触发Singleton类的加载 连接 初始化。

  private static Singleton singleton = new Singleton();A

       public static int counter1;B

       public static int counter2 = 0;C

在加载完后的连接阶段的准备期,会为singleton分配内存,设定默认值为nullcounter1默认值为 0counter2 默认值为 0. 进入初始化阶段,第一步为singleton赋值 会调用Singleton的构造方法,此时执行counter1++counter2++ counter1 = 1,counter2 = 1;第二步为counter1赋值,由于没有赋值语句counter1 仍为1;第三步为 counter2赋值,counter2 被赋值为 0,所以结果是counter1 =1

Counter2 =0

         case2 使用如下语句

       public static int counter1;A

       public static int counter2 = 0;B

       private static Singleton singleton = new Singleton();C

连接准备阶段结束后counter1 = 0,counter2 = 0,singleton = null;初始化时 第一步A语句不用初始化 counter1 =0;第二部B语句初始化为0counter2 =0;第三步 调用构造函数 counter1++counter2++ counter1 = 1,counter2 = 1;所以case2的结果为 counter1 =1 counter2 = 1

在下面的部分,我将给大家深入的介绍一下JVM的类加载器。


 

类加载器分析

         在上面的例子中我们看到一个类的主动使用会经历加载、链接、初始化的过程,类的加载需要使用JVM的类加载器,JVM的类加载器使用父亲委托机制。我们首先看看JVM的类加载器树形结构:

          BootStrap(根类加载器)

              ||

          Extend(扩展类加载器)

              ||

    System(系统类或应用类加载器)

              ||

  用户自定义类加载器


从上面的结构可以看出,JVM的类加载器一共有四大类,其中根类加载器、扩展类加载器、系统类加载器(应用类加载器)为JVM自带的类加载器。

1.根类加载器。负责加载虚拟机的核心类库,如java.lang.*等。根类加载器从系统 属性sun.boot.class.path 所指定的目录中加载类库。根类加载器的实现依赖于底层 操作系统,属于jvm实现的一部分,使用c++语言编写。它并没有ClassLoader类, 也没有父加载器。使用如stringObject.getClass().getClassLoader()将返null

  2. 扩展类加载器(Ext)。从图中可以看出它的父加载器是根加载器。它 java.ext.dirs系统属性所指定的目录中加载类库,或者从jdk的安装目录 jre\lib\ext子目录下加载类库,如果你把用户创建的jar放在这个目录下会被扩展类 加载器加载。扩展类加载器使用纯java实现,继承了ClassLoader

3.    系统类加载器,也叫应用类加载器(APP),它的父加载器是Ext加载器。它从 环境 变量里(安装JDK时设立的classpath)或从系统属性java.class.path加载 类,它是 用户自定义的类加载器的默认父加载器,采用纯java实现,继承 ClassLoader

4.   用户自定义加载器。系统类加载器的子类,必须要继承自ClassLoader之类,并 且重 findClass方法。

假设我们自定义ClassLoader loadA,并且使用loadA.load(Class),所谓的父亲委托机制就是loadA 委托父加载器(App)加载ClassApp委托ExtExt委托BootStrap,如果BootStrap不能加载,则让Ext加载,逐级下发,如果直到loadA还不能加载Class这抛出ClassNotFindException。委托机制是SUN公司基于安全性考虑的,这样可以保证Object这样的重要类只能有JVM加载。我们定义若一个类加载器能够成功加载类Class,我们则称这个加载器为该类的定义加载器,其下的子加载器为初始化加载器。如在上述的类之中,假设App类加载器加载了类Class App为定义加载器,APPLoadA为初始化加载器。

需要指出的是加载器之间的父子关系并不是指类之间的继承关系,而是指加载器之间的包装关系。一对父子可能是同一个类加载器的实例,也可能不是。例如我们自定义类加载器MyClassLoaderLoadA = new MyClassLoader(); LoadB = new MyClassLoader(loadA), 我们称loadB包装了loadALoadAloadB的父加载器。

运行时包决定了protecetd类和protected成员是否能够访问。我们知道所有的protected的成员需要同一个包下的类才能访问。如果我们定义java.lang.Spy类,我们是否就能访问java.lang.*下的核心protected资源呢?运行时包包括,包名相同,类加载器相同,所有java.lang.Spy java.lang.* 不在相同的运行时包,答案是否定的。

 

下面我们以视频中一个详细的例子来讲述用户自定义加载器的实现。首先给出例子中类加载器的树形关:   

 

           BootStrap(根类加载器)

              ||               \\

        ||          LoaderC -----> d:app/otherlib           

          Extend(扩展类加载器)

              ||

    System(系统类或应用类加载器)

              ||

  LoaderB    ------> d:app/serverlib

              ||              

    LoaderA    ------> d:app/clientlib 


 

在例子中我们定义三个类加载器,在重写的findClass方法中设定好加载路径。JDK API给出一个自定义ClassLoad的方法:

 class MyClassLoader extends ClassLoader {

         public Class findClass(String name) {

             byte[] b = loadClassData(name);

             return defineClass(name, b, 0, b.length);

         }

         private byte[] loadClassData(String name) {

             // load the class data from the connection

              . . .

         }

     }


   从上面的代码可以看出通过重写findClass方法并在loadClassData中设定好加载.class 文件的路径可以实现自定义的加载机制。

假设我们已经定义好自己的加载器MyClassLoader,我们使用如下的代码便能够构造出例子中的类加载器树形结构。

MyClassLoader loadB = new MyClassLoader(“loadB”);

loadB.setPath(“D://app//serverlib”);

MyClassLoader loadA = new MyClassLoader(loadB,“loadA”);

loadB.setPath(“D://app//cientlib”);

MyClassLoader loadC = new MyClassLoader(null,”loadC”)//null代表父加载器为Bootstrap

loadC.setPath(“D://app//otherlib”);

我们一个测试类Sample 和一个测试类Dog进行加载测试。

Class Sample{

Static{

   New Dog();

}

}   


 

Class文件的存放路径如下:

     d:app/syslib/MyClassLoader.class

     d:app/serverlib/Sample.class

     d:app/clientlib/Dog.class

      d:app/otherlib/Sample.class,Dog.class 


 

TestCase1 :

Class clazz = loadA.loadClass(“Sample”);

Clazz.newInstance();

TestCase2 :

Class clazz = loadB.loadClass(“Sample”);

Clazz.newInstance();

TestCase3 :

Class clazz = loadC.loadClass(“Sample”);

Clazz.newInstance();

case1 由加载器的树形结构可以看出:loadA加载Sample时委托父亲LoadB加载Sample,由于再向上委托并不能加载Sample,所以 Sample LoadB app/serverlib 下加载,

对于Dog类,LoadA的所有父加载器都不能加载,所以有loadA app/clientlib下加载

 

case2 loadB app/serverlib中加载 Sample,由于loadB 与其父加载器都不能加载Dog,所以会抛出ClassNotfoundException。此时如果把Dog拷贝到 syslib下,Dog类就会被appCloader加载,而不会出现ClassNotFound错误。

 

Case3 LoadC直接委托Bootstrap加载Sample,由于无法加载只能有自己加载,所以Sample Dog都会从 app/otherlib/下加载.

假设 我们在MyClassLoader中写意给main方法测试

TestCase4 :

Class clazz = loadA.loadClass(“Sample”);

Sample sample  =  Clazz.newInstance();

此时会导致一个NoClassDefError,这主要是JVM的类加载器命名空间规则导致的,在jvm中子加载器的命令空间包含了父加载器加载的所有类,反过来则不成立,因为MyClassLoader类是有appLoader加载的,所以其看不见有LoadBloadA加载的类。

在这里顺便提一下,在一个类主动使用时,该类就开始起生命周期 加载,链接,初始化,使用,卸载。Jvm自带的加载器加载的Class是不能够被卸载的,只有用户自定义加载器加载的类才能被卸载,卸载机制是根据对类的引用计数情况而定,这与GC根据引用情况回收垃圾差不多。


appendix : 视频下载链接文件(电驴)

                  

 

0
2
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics