java虚拟机类加载器

欢迎关注本人公众号



类从被加载到虚拟机内存中开始,到卸装出内存为止,它的整个生命周期包括了:加载,验证,准备,解析,初始化,使用和卸载七个阶段。其中验证、准备和解析三个部分称为连接,也就是说,一个Java类从字节代码到能够在JVM中被使用,需要经过加载、链接和初始化这三个步骤 。我们看一看Java虚拟机的体系结构。

Java虚拟机的体系结构如下图所示:

 

 

  •  类装载器子系统,它根据给定的完整类名来装载类或接口
  • 执行引擎,它负责执行那些包含 在被装载类的方法中的指令。
  • 方法区以及一个,它们是由该虚拟机实例中所有线程共享的。当虚拟机装载一个class 件时,它会从这个class文件包含的二进制数据中解析类型信息。然后,它把这些类型信息入到方法区中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。
  • Java是由许多 栈帧 或者 帧组成的,一个栈帧包含 一个Java方法调用的状态。当线程调用一个Java方法时。虚拟机压入一个新的栈帧到该线程的Java栈中;当该方法返回时,这个栈帧就会从Java栈中被弹出或者抛弃。Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑。同时也使于Java虚拟机在那些只有很少通用寄存器的平台上实现。另外,Java虚拟机的这种基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。

我们将按照类的生命周期来谈谈Java虚拟机的工作,本篇我们先谈谈加载:

Java类加载的全过程,是加载、验证、准备、解析和初始化这五个阶段的过程。而加载阶段是类加载过程的一个阶段。在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。 

这三件事情中,通过一个类的全限定名来获取定义此类的二进制字节流这个动作是在Java虚拟机外部来实现的,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块被称为“类加载器”。

站在Java虚拟机的角度讲,只存在两种不同的类加载喊叫:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(只限于Hot Spot,像MPR、Maxine等虚拟机,整个虚拟机本身都是由Java编写的,Bootstrap ClassLoader自然也是由Java语言实现的),是虚拟机自身的一部分;另外一种 用户自定义类装载器。前者是 Java 虚拟机实现的一部分,后者是 Java 程序的一部分。由不同的类装载器装载的类将被放在虚拟机内部的不同命名空间中  

从Java开发人员的角度来看,类加载器还可以划分的更细致一些,分为系统提供的类加载器与用户自定义的类加载器。 

系统提供的类加载器: 

系统提供的类装载器主要由下面三个:

  • 启动类加载器(bootstrap classloader):它用来加载 Java 的核心库,是用原生代码(本地代码,与平台有关)来实现的,并不继承自java.lang.ClassLoader。这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识加的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
  • 扩展类加载器(extensions classloader):扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量java.ext.dir 指定位置中的类库加载到内存中
  • 应用程序类加载器(application classloader):系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,由于这个类加载器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序默认的类加载器。
用户自定义的类装载器  

用户自定义的类装载器是普通的Java对象,它的类必须派生自java.lang.ClassLoader类。ClassLoader中定义的方法为程序为程序提供了访问类装载器机制的接口。此外,对于每一个被装载的类型,Java虚拟机都会为它创建一个java.lang.Class类的实例来代表该类型。和所有其它对象一样,用户自定义的类装载器以有Class类的实例都放在内存中的堆区,而装载的类型信息则都放在方法区。

下面介绍一下java.lang.ClassLoader类:

为了完成加载类的这个职责,  java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即  java.lang.Class 类的一个实例。除此之外, ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等。 ClassLoader 提供了一系列的方法,比较重要的方法如下表所示:
方法 说明
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class<?> c) 链接指定的 Java 类。

类装载器的特征 

Java类加载器有两个比较重要的特征:层次组织结构和代理模式。这两个特征也就是我们平时说的类加载器的双亲委派模型。层次组织结构指的是除了顶层为启动类加载器之外,其余的类加载器都有一个父类加载器,通过getParent()方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结构。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。代理模式则指的是一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成。

  • 类加载器的树状组织结构

在系统提供的类加载器中,除了启动类加载器之外,所有的类加载器都有一个父类加载器。通过getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是启动类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。

 

下面代码演示了类加载器的树状组织结构 

复制代码
 public class ClassloaderTree {
     /**
     *  @param  args
     
*/
     public  static  void  main(String[] args) {
        ClassLoader loader = ClassloaderTree. class .getClassLoader();
         while (loader!= null ){
            System.out.println(loader);
            loader = loader.getParent();
        }

    }

}
复制代码

打印出来的结果为:

sun.misc.Launcher$AppClassLoader@1372a1a 

sun.misc.Launcher$ExtClassLoader@ad3ba4

第一个输出的是 ClassLoaderTree类的类加载器,即系统类加载器。它是 sun.misc.Launcher$AppClassLoader类的实例;第二个输出的是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader类的实例。需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent()方法返回 null 

代理模式

代理模式说的是双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。 

那么为什么要使用代理模式呢,每个类加载器都由自己加载不是很好吗,搞得这么复杂干吗?要解释这个,就得首先说明Java虚拟机是如何判定两个类是相同的。在Java虚拟机中,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,通俗来说,Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。这个说的相同,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等情况。

下面我们通过编写自定义的类加载器来分别加载同一个class文件,进而确定得到的类是否相同。 

在 上表中列出的 java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现(loadClass的具体实现如下所示)。该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写findClass()方法。

复制代码
 protected synchronized Class<?> loadClass(String name, boolean resolve)  

  throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClass0(name);
        }
        } catch (ClassNotFoundException e) {
            // If still not found, then invoke findClass in order
            
// to find the class.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
    }

复制代码

在本例中,我们为了说明两个类加载器加载同一个class会得到不同的类,所以重写loadClass方法,如果只重写findClass方法,会由双亲来加载class文件,得不到想到的效果。

自定义的ClassLoader代码如下:

复制代码
import  java.io.InputStream;

public  class  MyClassLoader  extends  ClassLoader {

    @Override
     public  Class<?> loadClass(String name)  throws  ClassNotFoundException {
         //  TODO Auto-generated method stub
        String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
        InputStream is = getClass().getResourceAsStream(fileName);
         if ( is ==  null ){
             return  super .loadClass(name);
        }
         try  {
             byte [] b =  new  byte [is.available()];
            is.read(b);
             return  defineClass(name,b,0,b.length);
        }  catch  (IOException e) {
            e.printStackTrace();
        }
         return  super .loadClass(name);
    }

} 
复制代码

代码很简单,就是加载与MyClassLoader同目录下的class类文件。

写一个辅助类

复制代码
package  com.xiaoruoen.test;

public  class  Sample {
    
     private  Sample instance;
    
     public  void  setSample(Object obj){
         this .instance = (Sample)obj;
    }
} 
复制代码

写一个测试的类:

复制代码
 package com.xiaoruoen.test;
import  java.lang.reflect.InvocationTargetException;
import  java.lang.reflect.Method;

public  class  ClassLoaderTest {

     /**
     *  @param  args
     *  @throws  ClassNotFoundException 
     *  @throws  IllegalAccessException 
     *  @throws  InstantiationException 
     *  @throws  NoSuchMethodException 
     *  @throws  SecurityException 
     *  @throws  InvocationTargetException 
     *  @throws  IllegalArgumentException 
     
*/
     public  static  void  main(String[] args)  throws  InstantiationException, IllegalAccessException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        MyClassLoader loader1 =  new  MyClassLoader();
        MyClassLoader loader2 =  new  MyClassLoader();
        Class class1 = loader1.loadClass("com.xiaoruoen.test.Sample");
        Class class2 = loader2.loadClass("com.xiaoruoen.test.Sample");
        Object obj1 = class1.newInstance();
        Object obj2 = class2.newInstance();
        Object obj3 = loader1.loadClass("com.xiaoruoen.test.ClassLoaderTest");
        System.out.println(obj3);
        System.out.println(obj3  instanceof  com.xiaoruoen.test.ClassLoaderTest);
        Method method = class1.getMethod("setSample", java.lang.Object. class );
        method.invoke(obj1, obj2);
    }

}
复制代码

 上面代码运行的结果为:

复制代码
class  com.xiaoruoen.test.ClassLoaderTest
false
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.xiaoruoen.test.ClassLoaderTest.main(ClassLoaderTest.java:29)
Caused by: java.lang.ClassCastException: com.xiaoruoen.test.Sample cannot be cast to com.xiaoruoen.test.Sample
    at com.xiaoruoen.test.Sample.setSample(Sample.java:8)

... 5 more  

复制代码

从给出的结果来看:

给出的运行结果可以看到,运行时抛出了 java.lang.ClassCastException 异常。虽然两个对象 obj1 和 obj2 的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。同样,我们使用了
MyClassLoader加载了一个名为
com.xiaoruoen.test.ClassLoaderTest的类,并实例化了这个类的对象。两行输出结果中,从第一句可以看到这个对象确实是类com.xiaoruoen.test.ClassLoaderTest实例化出来的对象,但从第二句可以发现这个对象与类
com.xiaoruoen.test.ClassLoaderTest做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,一个是由我们自定义的类加载器加载的,虽然都是来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结果自然是false。

了解了这一点之后,就可以理解代理模式的设计动机了。 

代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object 类,也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类( 由上面的例子就可以看出来 ),而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的

Apache  Xerces 所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的  javax.xml.parsers.DocumentBuilderFactory 类中的 newInstance() 方法用来生成一个新的  DocumentBuilderFactory 的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory ,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl 。而问题在于, SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。 引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器 也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。




-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

JAVA虚拟机类加载机制


 Java语言是一种编译后再经过解释器执行的过程, 解释器主要就是如何处理解释Class文件的二进制字节流。JVM主要包含三大核心部分:运行时数据区,类加载器和执行引擎。

  虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验、准备、解析和初始化,最终就会形成可以被虚拟机使用的Java类型,这就是一个虚拟机的类加载机制。Java中的类是动态加载的,只有在运行期间使用到该类的时候,才会将该类加载到内存中,Java依赖于运行期动态加载和动态链接来实现类的动态使用。

  一个类的整个生命周期如下:

  加载,验证,准备,初始化和卸载在开始的顺序上是固定的,但是可以交叉进行。

  在Java中,对于类有且仅有四种情况会对类进行“初始化”。

  1)使用new关键字实例化对象的时候,读取或设置一个类的静态字段时候(除final修饰的static外),调用类的静态方法时候,都只会初始化该静态字段或者静态方法所定义的类。

  2)使用reflect包对类进行放射调用的时候,如果类没有进行初始化,则先要初始化该类

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

  4)虚拟机启动的时候,会初始化一个有main方法的主类。

  注意:通过子类引用父类静态字段,只会初始化父类不会初始化子类;通过数组定义来引用类,也不会触发该类的初始化;常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此也不会触发定义常量的类的初始化。

  一、类加载过程

  1、加载

  加载阶段主要完成三件事,即通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在Java堆中生成一个代表此类的Class对象,作为访问方法区这些数据的入口。这个加载过程主要就是靠类加载器实现的,这个过程可以由用户自定义类的加载过程。

  2、验证

  这个阶段目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证:

  文件格式验证:基于字节流验证,验证字节流是否符合Class文件格式的规范,并且能被当前虚拟机处理。

  元数据验证:基于方法区的存储结构验证,对字节码描述信息进行语义验证。

  字节码验证:基于方法区的存储结构验证,进行数据流和控制流的验证。

  符号引用验证:基于方法区的存储结构验证,发生在解析中,是否可以将符号引用成功解析为直接引用。

  3、准备

  仅仅为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即零值,这里不包含用final修饰的static,因为final在编译的时候就会分配了,同时这里也不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

  4、解析

  解析主要就是将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析。

  这里要注意如果有一个同名字段同时出现在一个类的接口和父类中,那么编译器一般都会拒绝编译。

  5、初始化

  初始化阶段依旧是初始化类变量和其他资源,这里将执行用户的static字段和静态语句块的赋值操作。这个过程就是执行类构造器方法的过程。

  方法是由编译器收集类中所有类变量的赋值动作和静态语句块的语句生成的,类构造器方法与实例构造器方法不同,这里面不用显示的调用父类的方法,父类的方法会自动先执行于子类的方法。即父类定义的静态语句块和静态字段都要优先子类的变量赋值操作。

  二、类加载器

  1、类加载器的分类

  启动类加载器(Bootstrap ClassLoader): 主要负责加载lib目录中的,或是-Xbootclasspath参数指定的路径中的,并且可以被虚拟机识别(仅仅按照文件名识别的)的类库到虚拟机内存中。它加载的是System.getProperty("sun.boot.class.path")所指定的路径或jar

  扩展类类加载器(Extension ClassLoader):主要负责加载libext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。它加载的是

  System.getProperty("java.ext.dirs")所指定的路径或jar。

  应用程序类加载器(Application ClassLoader):也叫系统类加载器,主要负责加载ClassPath路径上的类库,如果应用程序没有自定义自己类加载器,则这个就是默认的类加载器。

  它加载的是System.getProperty("java.class.path")所指定的路径或jar。

  2、类加载器的特点

  1)运行一个程序时,总是由Application Loader(系统类加载器)开始加载指定的类。

  2)在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。

  3)Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null.

  3、类加载器的双亲委派模型

  类加载器双亲委派模型的工作过程是:如果一个类加载器收到一个类加载的请求,它首先将这个请求委派给父类加载器去完成,每一个层次类加载器都是如此,则所有的类加载请求都会传送到顶层的启动类加载器,只有父加载器无法完成这个加载请求(即它的搜索范围中没有找到所要的类),子类才尝试加载。

  下面是一个类加载器双亲委派模型,这里各个类加载器并不是继承关系,它们利用组合实现的父类与子类关系。

  4、类加载的几种方式

  1)命令行启动应用时候由JVM初始化加载,加载含有main的主类。

  2)通过Class.forName()方法动态加载类,默认会执行初始化块。如果指定了ClassLoader,则不会执行初始化块。

  3)通过ClassLoader.loadClass()方法动态加载类,不会执行初始化块。

  5、类加载实例

  当在命令行下执行:java HelloWorld(HelloWorld是含有main方法的类的Class文件),JVM会将HelloWorld.class加载到内存中,并在堆中形成一个Class的对象HelloWorld.class。

  基本的加载流程如下:

  1)寻找jre目录,寻找jvm.dll,并初始化JVM;

  2)产生一个Bootstrap Loader(启动类加载器);

  3)Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为BootstrapLoader。

  4)Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为ExtendedLoader。

  5)最后由AppClass Loader加载HelloWorld类



=============================================================
JAVA虚拟机示例

类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行。
研究类加载机制的第二个目的是让程序能动态的控制类加载,比如热部署等,提高程序的灵活性和适应性。
 
一、简单过程
 
Java程序运行的场所是内存,当在命令行下执行:
java HelloWorld
命令的时候,JVM会将HelloWorld.class加载到内存中,并形成一个Class的对象HelloWorld.class。
其中的过程就是类加载过程:
1、寻找jre目录,寻找jvm.dll,并初始化JVM;
2、产生一个Bootstrap Loader(启动类加载器);
3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。
4、Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。
5、最后由AppClass Loader加载HelloWorld类。
 
以上就是类加载的最一般的过程。
 
二、类加载器各自搜索的目录
 
为了弄清楚这个问题,首先还要看看System类的API doc文档。
 
 
相关值的描述
java.versionJava 运行时环境版本
java.vendorJava 运行时环境供应商
java.vendor.urlJava 供应商的 URL
java.homeJava 安装目录
java.vm.specification.versionJava 虚拟机规范版本
java.vm.specification.vendorJava 虚拟机规范供应商
java.vm.specification.nameJava 虚拟机规范名称
java.vm.versionJava 虚拟机实现版本
java.vm.vendorJava 虚拟机实现供应商
java.vm.nameJava 虚拟机实现名称
java.specification.versionJava 运行时环境规范版本
java.specification.vendorJava 运行时环境规范供应商
java.specification.nameJava 运行时环境规范名称
java.class.versionJava 类格式版本号
java.class.pathJava 类路径
java.library.path加载库时搜索的路径列表
java.io.tmpdir默认的临时文件路径
java.compiler要使用的 JIT 编译器的名称
java.ext.dirs一个或多个扩展目录的路径
os.name操作系统的名称
os.arch操作系统的架构
os.version操作系统的版本
file.separator文件分隔符(在 UNIX 系统中是“/”)
path.separator路径分隔符(在 UNIX 系统中是“:”)
line.separator行分隔符(在 UNIX 系统中是“/n”)
user.name用户的账户名称
user.home用户的主目录
user.dir用户的当前工作目录
 
可惜这个帮助文档并不全,直接用程序打印出来如下:
                for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) { 
                        System.out.println(entry.getKey()+"\t"+entry.getValue()); 
                }
 
java.runtime.nameJava(TM) SE Runtime Environment
sun.boot.library.pathQ:\jdk6\jre\bin
java.vm.version14.0-b16
java.vm.vendorSun Microsystems Inc.
java.vendor.urlhttp://java.sun.com/
path.separator;
idea.launcher.port7532
java.vm.nameJava HotSpot(TM) Client VM
file.encoding.pkgsun.io
sun.java.launcherSUN_STANDARD
user.countryCN
sun.os.patch.levelService Pack 3
java.vm.specification.nameJava Virtual Machine Specification
user.dirE:\projects\testScanner
java.runtime.version1.6.0_14-b08
java.awt.graphicsenvsun.awt.Win32GraphicsEnvironment
java.endorsed.dirsQ:\jdk6\jre\lib\endorsed
os.archx86
java.io.tmpdirC:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\
line.separator
java.vm.specification.vendorSun Microsystems Inc.
user.variant
os.nameWindows XP
sun.jnu.encodingGBK
java.library.pathQ:\jdk6\bin;.;C:\WINDOWS\Sun\Java\bin;C:\WINDOWS\system32;C:\WINDOWS;Q:\jdk6\bin;Q:\JavaFX\javafx-sdk1.2\bin;Q:\JavaFX\javafx-sdk1.2\emulator\bin;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\MySQL Server 5.1\bin;C:\Program Files\StormII\Codec;C:\Program Files\StormII
java.specification.nameJava Platform API Specification
java.class.version50
sun.management.compilerHotSpot Client Compiler
os.version5.1
user.homed:\我的文档
user.timezone
java.awt.printerjobsun.awt.windows.WPrinterJob
idea.launcher.bin.pathC:\IDEA8\bin
file.encodingUTF-8
java.specification.version1.6
java.class.pathQ:\jdk6\jre\lib\alt-rt.jar;Q:\jdk6\jre\lib\charsets.jar;Q:\jdk6\jre\lib\deploy.jar;Q:\jdk6\jre\lib\javaws.jar;Q:\jdk6\jre\lib\jce.jar;Q:\jdk6\jre\lib\jsse.jar;Q:\jdk6\jre\lib\management-agent.jar;Q:\jdk6\jre\lib\plugin.jar;Q:\jdk6\jre\lib\resources.jar;Q:\jdk6\jre\lib\rt.jar;Q:\jdk6\jre\lib\ext\dnsns.jar;Q:\jdk6\jre\lib\ext\localedata.jar;Q:\jdk6\jre\lib\ext\sunjce_provider.jar;Q:\jdk6\jre\lib\ext\sunmscapi.jar;Q:\jdk6\jre\lib\ext\sunpkcs11.jar;E:\projects\testScanner\out\production\testScanner;C:\IDEA8\lib\idea_rt.jar
user.nameAdministrator
java.vm.specification.version1
java.homeQ:\jdk6\jre
sun.arch.data.model32
user.languagezh
java.specification.vendorSun Microsystems Inc.
awt.toolkitsun.awt.windows.WToolkit
java.vm.infomixed mode, sharing
java.version1.6.0_14
java.ext.dirsQ:\jdk6\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
sun.boot.class.pathQ:\jdk6\jre\lib\resources.jar;Q:\jdk6\jre\lib\rt.jar;Q:\jdk6\jre\lib\sunrsasign.jar;Q:\jdk6\jre\lib\jsse.jar;Q:\jdk6\jre\lib\jce.jar;Q:\jdk6\jre\lib\charsets.jar;Q:\jdk6\jre\classes
java.vendorSun Microsystems Inc.
file.separator\
java.vendor.url.bughttp://java.sun.com/cgi-bin/bugreport.cgi
sun.io.unicode.encodingUnicodeLittle
sun.cpu.endianlittle
sun.desktopwindows
sun.cpu.isalist
 
1、Bootstrap Loader(启动类加载器):加载System.getProperty("sun.boot.class.path")所指定的路径或jar。
2、Extended Loader(标准扩展类加载器ExtClassLoader):加载System.getProperty("java.ext.dirs")所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld
 
3、AppClass Loader(系统类加载器AppClassLoader):加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld
 
ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其他搜索路径加载类,就要产生新的类加载器。
 
三、类加载器的特点
 
1、运行一个程序时,总是由AppClass Loader(系统类加载器)开始加载指定的类。
2、在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
3、Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null.
 
四、类加载器的获取
 
很容易,看下面例子
public class HelloWorld { 
        public static void main(String[] args) { 
                HelloWorld hello = new HelloWorld(); 
                Class c = hello.getClass(); 
                ClassLoader loader = c.getClassLoader(); 
                System.out.println(loader); 
                System.out.println(loader.getParent()); 
                System.out.println(loader.getParent().getParent()); 
        } 
}
 
打印结果:
sun.misc.Launcher$AppClassLoader@19821f 
sun.misc.Launcher$ExtClassLoader@addbf1 
null 

Process finished with exit code 0
 
从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(启动类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
 
五、类的加载
 
类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
 
三种方式区别比较大,看个例子就明白了:
public class HelloWorld { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()来加载类,不会执行初始化块 
                loader.loadClass("Test2"); 
                //使用Class.forName()来加载类,默认会执行初始化块 
//                Class.forName("Test2"); 
                //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
//                Class.forName("Test2", false, loader); 
        } 
}
 
public class Test2 { 
        static { 
                System.out.println("静态初始化块执行了!"); 
        } 
}
 
分别切换加载方式,会有不同的输出结果。
 
六、自定义ClassLoader
 
为了说明问题,先看例子:
package test; 

import java.net.MalformedURLException; 
import java.net.URL; 
import java.net.URLClassLoader; 

/** 
* 自定义ClassLoader 

* @author leizhimin 2009-7-29 22:05:48 
*/ 
public class MyClassLoader { 
        public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { 
                URL url = new URL("file:/E:\\projects\\testScanner\\out\\production\\testScanner"); 
                ClassLoader myloader = new URLClassLoader(new URL[]{url}); 
                Class c = myloader.loadClass("test.Test3"); 
                System.out.println("----------"); 
                Test3 t3 = (Test3) c.newInstance(); 
        } 
}
 
public class Test3 { 
        static { 
                System.out.println("Test3的静态初始化块执行了!"); 
        } 
}
 
运行后:
---------- 
Test3的静态初始化块执行了! 

Process finished with exit code 0
 
可以看出自定义了ClassLoader myloader = new URLClassLoader(new URL[]{url});已经成功将类Test3加载到内存了,并通过默认构造方法构造了对象Test3 t3 = (Test3) c.newInstance();
 
有关ClassLoader还有很重要一点:
同一个ClassLoader加载的类文件,只有一个Class实例。但是,如果同一个类文件被不同的ClassLoader载入,则会有两份不同的ClassLoader实例(前提是着两个类加载器不能用相同的父类加载器)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值