13.JAVA类加载器

JAVA类加载器

目的

了解 Java 的类加载机制对我们熟练灵活运用 Java 语言,提高程序的运行效率有着非常重要的作用,知其然也要知其所以然,这样才能从整体提高程序的质量。

类加载器的体系结构

类装载器的体系结构在JAVA虚拟机的安全性和网络移动性发挥重要作用。在图中仅画了一个“类装载器”,实际上JAVA虚拟机种存在多个类装载器,因而图中的方框实际上是一个包含多个类装载器的子系统。

如图:

每个类装载器都有自己的命名空间。

JAVA加载类说明

当执行某个类的时候, java.exe 会帮助我们找到 JRE ,接着找到位于 JRE 内部的 jvm.dll ,这才是真正的 Java 虚拟机器 , 最后加载动态库,激活 Java 虚拟机器。虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰写而成,这个 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载 Launcher.java 之中的 ExtClassLoader ,并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoader 。然后 Bootstrap Loader 再要求加载 Launcher.java 之中的 AppClassLoader ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的。这里要请大家注意的是, Launcher$ExtClassLoader.class 与 Launcher$AppClassLoader.class 都是由 Bootstrap Loader 所加载,所以 Parent 和由哪个类加载器加载没有关系。

BootstrapLoader : sun.boot.class.path 

ExtClassLoader:      java.ext.dirs 

AppClassLoader:      java.class.path

执行java XXX.class的过程

找到JRE——》找到jvm.dll——》启动JVM并进行初始化——》产生Bootstrap Loader——》载入ExtClassLoader——》载入AppClassLoader——》执行java XXX.class

加载类工作原理

加载类采用的是全盘负责委托机制(双亲委托)。所谓全盘负责,即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有 Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入;委托机制则是先让parent(父)类加载器 (而不是super,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。此外类加载还采用了cache机制,也就是如果 cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。

ClassLoader加载Class的过程

类加载器对安全的作用

如一个JAVA应用程序,装载了一个类装载器,这个类装载器是通过网络下载装载某个class文件。假设在我们自己的机器上运行这个应用程序,系统启动后,建立了以下双亲-孩子关系链:

装载某类最后会由网络类装载器完成。若某类中引用了java.util.HashMap,因为该类属于启动类加载器中,所以系统环境中该类有效。这就是安全潜在路径。

另外,JAVA允许在同一个包中的类拥有彼此特殊的访问权限,所以JAVA虚拟机又提出了一个运行时包的概念,不同类加载器加载的包,属于不同的类。每个类加载器,都有自己的权限,最高级别具有最高可信度。

类加载器的工作步骤

Java中,在调用类的静态成员,或新建该类的对象等之前,类一定要先装入Java虚拟机中,这是勿庸置疑的。但虚拟机怎样把类装载进来的呢?要经过三步:装载(Load),链接(Link),初始化(Initializ)。其中链接又可分为校验(Verify),准备(Prepare),解析(Resolve)三步。

一、装载(Load)
ClassLoader就是用来装载的。通过指定的className,找到二进制码,生成Class实例,放到JVM中。

ClassLoader从顶向下分为 Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader以及User-Defined ClassLoader(分叉,可以多个)。如下图。

这是Tomcat装载器的例子:

装载过程从源码清析可见:

protected synchronized Class<?> loadClass(String name, boolean resolve)

throws ClassNotFoundException
{

    // 先检查是否已被当前ClassLoader装载。

    Class c = findLoadedClass(name);

    if (c == null) {

        try {

        if (parent != null) {

            // 如果没被当前装载,则递归的到父中装载。
            c = parent.loadClass(name, false);

        } else {

           // 装载器树已到顶,还没找到的话就到Bootstrap装载器中找。注意:虽然Bootstrap是所有加载器的根,但它是C++实现的,不可能放到子的"parent"中,因此,第二层装载器是所有的根了。

            c = findBootstrapClass0(name);

        }

        } catch (ClassNotFoundException e) {

            // 如果祖先都无法装载,则用当前的装载。子类可在findClass方法中调用defineClass,把从自定义位置获得的字节码转换成Class。

            c = findClass(name);

        }

    }

    if (resolve) {

        // Links the specified class.

        resolveClass(c);  //连接

    }

    return c;

 }

注:

1. resolveClass(c)方法的注释是链接类,而不只是解析。

2. 调用resolveClass时语义上是去链接,可以肯定的是没有初始化。当A类中有static B b=new B()时,最晚会在初始化时去装载B。

二、链接

链接就是把load进来的class合并到JVM的运行时状态中。

链接 是三个阶段中最复杂的一个。可以把它分成三个主要阶段:

· 校验。 对二进制字节码的格式进行校验,以确保格式正确、行为正确。 

如格式、长度、是否符合规范,符合超类接口等。

确保超类被加载。

· 准备。 准备类中定义的字段、方法和实现接口所必需的数据结构。比如会为类中的静态变量赋默认值(int等:0, reference:null, char:'/u0000')。 

准备时,还为类变量分配内存。

· 解析。 装入类所引用的其他所有类。可以用许多方式引用类: 

o 超类 

o 接口 

o 字段 

o 方法签名 

o 方法中使用的本地变量 

符号引用换成直接引用。

三、初始化

Initialization of a class consists of executing its static initializers and the initializers for static fields (class variables) declared in the class. Initialization of an interface consists of executing the initializers for fields (constants) declared there.
类的初始化包括:执行静态区块和静态方法的初始化。比如下面这两种代码都会被执行,包括new B()。
static{
  ...
}
static B b=new B();

接口中不允许有static initializer(也就是static{...}),所以对于接口,只会执行静态字段的初始化。

初始化前,装载,链接一定已经执行过!

类初始化前,它的直接父类一定要先初始化(递归),但它实现的接口不需要先被初始化。类似的,接口在初始化前,父接口不需要先初始化。

什么情况下,类的初始化会被触发?

A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

 

· 

· T is a class and an instance of T is created. 

· T is a class and a static method declared by T is invoked. 

· A static field declared by T is assigned. 

· A static field declared by T is used and the field is not a constant variable (§4.12.4)

· T is a top-level class, and an assert statement (§14.10) lexically nested within T is executed. 

当使用类的字段时,即便可以通过子类或子接口访问该字段,但只有真正定义该字段的类会被触发初始化。如下例。

class Super { static int taxi = 1729; }

class Sub extends Super { static { System.out.print("Sub "); } }

class Test {

    public static void main(String[] args) {

               System.out.println(Sub.taxi);

    }

}

只会输出“1729”,不会输出"Sub",也就是说,Sub其实没有被初始化。
四、Class.forName()与ClassLoader.loadClass()

这两方法都可以通过一个给定的类名去定位和加载这个类名对应的 java.long.Class 类对象,区别如下:

1. 初始化

Class.forName()会对类初始化,而loadClass()只会装载或链接。可见的效果就是类中静态初始化段及字节码中对所有静态成员的初始工作的执行(这个过程在类的所有父类中递归地调用). 这点就与ClassLoader.loadClass()不同. ClassLoader.loadClass()加载的类对象是在第一次被调用时才进行初始化的。

你可以利用上述的差异. 比如,要加载一个静态初始化开销很大的类, 你就可以选择提前加载该类(以确保它在classpath下), 但不进行初始化, 直到第一次使用该类的域或方法时才进行初始化

2. 类加载器可能不同

Class.forName(String) 方法(只有一个参数), 使用调用者的类加载器来加载, 也就是用加载了调用forName方法的代码的那个类加载器。当然,它也有个重载的方法,可以指定加载器。 相应的, ClassLoader.loadClass()方法是一个实例方法(非静态方法), 调用时需要自己指定类加载器, 那么这个类加载器就可能是也可能不是加载调用代码的类加载器(调用代用代码类加载器通getClassLoader0()获得)

参考《java虚拟机第二版.pdf》174页

URLClassLoader加载器

好了,现在我们了解了classloader的结构和工作原理,那么我们如何实现在运行时的动态载入和更新呢?只要我们能够动态改变类搜索路径和清除classloader的cache中已经载入的Class就行了,有两个方案,一是我们继承一个classloader,覆盖 loadclass方法,动态的寻找Class文件并使用defineClass方法来;另一个则非常简单实用,只要重新使用一个新的类搜索路径来new 一个classloader就行了,这样即更新了类搜索路径以便来载入新的Class,也重新生成了一个空白的cache(当然,类搜索路径不一定必须更改)。噢,太好了,我们几乎不用做什么工作,java.netURLClassLoader正是一个符合我们要求的classloader!我们可以直接使用或者继承它就可以了! 

这是j2se1.4 API的doc中URLClassLoader的两个构造器的描述: 
URLClassLoader(URL[] urls) 
          Constructs a new URLClassLoader for the specified URLs using the default delegation parent ClassLoader. 
URLClassLoader(URL[] urls, ClassLoader parent) 
          Constructs a new URLClassLoader for the given URLs. 
其中URL[] urls就是我们要设置的类搜索路径,parent就是这个classloader的parent classloader,默认的是system classloader。 


好,现在我们能够动态的载入Class了,这样我们就可以利用newInstance方法来获得一个Object。但我们如何将此Object造型呢?可以将此Object造型成它本身的Class吗?

weblogic的classloader装载策略及部署应用的目录结构

prefer-web-inf-classes

在web应用部署描述文件weblogic.xml 中,包含了元素prefer-web-inf-classes (container-descriptor 元素的子元素)。默认值被设置为false。设置这个元素的值未true,从来改变类装载器的代理模式,提升子类装载器的级别,定义为首先从web应用中加载类。(然后从父类装载器中加载类)。这允许web应用来使用他们自己的第三方类库,这些类库也可能是weblogic应用服务器的。

当使用这一特性的时候,你必须小心,不要web应用的类的实例和服务器定义的实例混淆。否则就会产生ClassCastException 异常。

应用类装载器层次关系

weblogic Server自动建立类装载器层次当一个应用被部署的时候。层次中的根类装载器装载应用中所有EJB jar包中的文件,每个web应用(WAR)都会创建一个子类装载器。

当部署一个应用的时候,weblogic server会自动创建一个具有层次结构的类装载器。在图中,a.Application Classloader负责装载应用中的所有的EJB JAR文件;

b.Web Application Classloader负责装载所有的Web application 中的WAR 文件(所有得jsp文件除外);

c.Jsp Classloader 负责装载Web application 中的所有的jsp 文件;

这样的分层结构有一个好处,就是在Jsp,Servlet中可以直接访问EJB的接口。这种上层装载EJB,下层装载servlet等,最下面装载jsp文件的结构,使得经常变动的jsp,servlet等可以被重新装载而不会涉及到EJB层。

在这种默认的类装载器结构下,有一点需要提出的是:

a. 我们的应用必须打包成一个EAR文件,才会允许我们应用中的jspservlet文件直接访问ejb;如果将WARJAR文件分别打包。Weblogic server会为他们分别生成一个类装载器,作为兄弟节点,这时如果需要在jsp或者servlet中使用ejb,就必须将EJBHome接口与remote接口打包到WAR中才可以。后面这种情况,适合用在将EJB的客户端和EJB部署在不同的JVM中;

b.web application classloader中,不会装载jsp文件,jsp文件由web application classloader的子装载器Jsp classloader负责装载,因为jsp文件经常的变动,通过为jsp设立一个单独的classloader可以避免对jsp的装载影响到其他的java class或者ejb

默认装载器的优点:

a. 调用ejb的时候可以采用call-by-referrence的方式;

b. 允许web module独立的装载,不影响其它的web module;

通过在将整个应用打包成一个EAR文件,可以方便的不用再web module中包含EJBhomeremote接口,就可以方便的通过call-by-referrence来调用ejb;

自定义模块类装载器层次关系

你为一个应用可以创建自定义的类装载器层次管理,以便更好的丛刊类的可见性和重新装载的能力。你通过在weblogic-application.xml 定义元素classloader-structure来构建。

下图详细说明了类装载器是如何被weblogic应用默认组织的。所有ejb类被装载时应用层的类装载器已经存在。每个WEB模块,都有一个子类装载器。

对于大多数应用来说这个层次是比较理想的,因为它允许参照调用ejb(不需要rmi)。它也允许web模块独立的重新装载而不影响其他模块,进一步说,它允许在一个web模块中运行的代码调用任何ejb模块。这个很方便,不需要web模块包含ejb的interface类。注意这些好处并不全部严格遵循J2EE规范。

The following is an example of a classloader declaration (defined in the classloader-structure element in weblogic-application.xml):

Listing 4-3 Example Classloader Declaration

<classloader-structure> 

<module-ref> 

<module-uri>ejb1.jar</module-uri> 

</module-ref> 

<module-ref> 

<module-uri>web3.war</module-uri> 

</module-ref>

<classloader-structure> 

<module-ref> 

<module-uri>web1.war</module-uri> 

</module-ref> 

</classloader-structure>

<classloader-structure> 

<module-ref> 

<module-uri>ejb3.jar</module-uri> 

</module-ref> 

<module-ref> 

<module-uri>web2.war</module-uri> 

</module-ref>

<classloader-structure> 

<module-ref> 

<module-uri>web4.war</module-uri> 

</module-ref> 

</classloader-structure> 

<classloader-structure> 

<module-ref> 

<module-uri>ejb2.jar</module-uri> 

</module-ref> 

</classloader-structure>

</classloader-structure>

</classloader-structure>

The organization of the nesting indicates the classloader hierarchy. The above stanza leads to a hierarchy shown in the following diagram.

Figure 4-3 Example Classloader Hierarchy

架构中的使用说明

高级STARTUP

背景:为了在多个应用内共享配置

如何使用StartupForWeblogic初始化系统?

  需要在weblogic的config.xml文件中增加以下配置:

<StartupClass Arguments="configFilePath=jdlsapp/config/"

        ClassName="cn.com.jdls.foundation.startup.StartupForWeblogic"

        Name="MyStartup Class" Targets="EjbServer"/> 

如何一个DOMAIN下部署多个APPLICATION如何做到独立初始化?

  需要在/WEB-INF/weblogic.xml文件中增加以下配置,要求Web应用优先加载WEB-INF的Jar(即应用Classloader)而非WebLogic的System Classloader得以解决: 

<weblogic-web-app> 
  <container-descriptor> 
    <prefer-web-inf-classes>true</prefer-web-inf-classes> 
  </container-descriptor> 
</weblogic-web-app> 

附录

以上是网上的引用,大家作为参考: 

public class ClassLoaderTest1{ 

private ClassLoaderTest2 test = null; 

public ClassLoaderTest1(){ 

test = new ClassLoaderTest2(); 

public void method(){ 

System.out.println("Loading ClassA"); 

class ClassLoaderTest2{ 

public ClassLoaderTest2(){ 



public void method(){ 

  System.out.println("Loading ClassA"); 

测试程序: 

URL url = null; 

try { 

    url = new URL("file:/E:/JAVA/MyProject/string/"); 

  } catch (MalformedURLException e) {

    e.printStackTrace(); 

  } 

  URLClassLoader cl = new URLClassLoader(new URL[]{url}); 

  URLClassLoader cl1 = new URLClassLoader(new URL[]{url}); 

        try { 

    Class tempClass = cl.loadClass("ClassLoaderTest1"); 

    Class tempClass2 = cl.loadClass("ClassLoaderTest2"); 

    Object test = tempClass.newInstance(); 

    System.out.println(tempClass.getClassLoader()); 

    System.out.println(tempClass2.getClassLoader()); 

  } catch (Exception e) { 

    e.printStackTrace(); 

  } 

当ClassLoaderTest1,ClassLoaderTest2在当前目录和E:/JAVA/MyProject/string/都存在的时候输出为sun.misc.Launcher$AppClassLoader@1050169 

  sun.misc.Launcher$AppClassLoader@1050169 

即都是被AppClassLoader加载的, 即使在E:/JAVA/MyProject/string/下面也存在. 


当ClassLoaderTest1,ClassLoaderTest2只在E:/JAVA/MyProject/string/下存在的时候输出为 

java.net.URLClassLoader@480457 

java.net.URLClassLoader@1a7bf11 

即都是被自定义的加载器加载的,并且也可以Object test = tempClass.newInstance(); 

下面一的是最关键的,因为ClassLoaderTest1需要用到ClassLoaderTest2,如果ClassLoaderTest2被AppClassLoader加载,而ClassLoaderTest1是被自定义的类加载器加载,就会出现如下错误: 

java.lang.IllegalAccessError: tried to access class ClassLoaderTest2 from class ClassLoaderTest1 
at ClassLoaderTest1. <init>(ClassLoaderTest1.java:6) 
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) 
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39) 
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27) 
at java.lang.reflect.Constructor.newInstance(Constructor.java:274) 
at java.lang.Class.newInstance0(Class.java:308) 
at java.lang.Class.newInstance(Class.java:261) 
at ClassLoaderTest.main(ClassLoaderTest.java:43)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值