天天看点

JVM基础(一) ClassLoader的工作机制

对ClassLoader的理解

  • 顾名思义,Class loader 最基本的功能就是将Class加载到JVM中
  • 在加载过程中,Class loader 能够审查每个类应该由哪个class loader加载,采用双亲委派模型来实现
  • 除了加载外,class loader也负责将加载后的字节码重新解析成JVM同一要求的对象格式

类加载器对于所加载类的影响

  • 每一个类加载器都有一个独立的命名空间
  • 独立的命名空间一位这比较两个类对象是否相等的前提是由同一个类加载器加载的

JVM加载类的两种方式:

  • 隐式加载:不通过显示调用class loader来加载需要的类,而是通过JVM因需自动加载到内存当中的方式。比如加载一个类的时候会隐式加载它的父类。
  • 显示加载:通过调用classLoader来加载类的方式,比如
this.getclass().getClassLoader().loadClass();
Class.forName("className");
           

双亲委派模型

Class Loader的类结构层次

  • 大多数Java程序,会用到系统提供的三种类加载器:
    • 启动类加载器(Bootstrap Class Loader):用C++实现的类加载器
      • 是虚拟机的一部分, 主要加载JVM自身工作需要的类,完全由JVM控制,开发者无法访问.(无法被Java代码引用)
      • 将指定目录下的符合虚拟机规范的类加载到虚拟机内存中,默认是\lib
    • 拓展类加载器(Extension Class Loader):负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用拓展类加载器
    • 应用程序加载器(App Class Loader):由于这个类加载器是Class Loader只能怪的getSystemClassLoder()方法的返回值,所以又称系统加载器 ,它负责加载classPath路径上的指定的类库 ,如果程序中没有定义过类加载器,一般作为默认的类加载器
    • 类加载器路径的设置参数列表
    Class Loader类型 参数选项 说明
    -Xbootclasspath: 设置引导类加载器的搜索路径
    BootstrapClassLoader -Xbootclasspath/a: 把路径添加到已存在的搜索路径的后面
    -Xbootclasspath/p: 把路径添加到已存在的搜索路径的前面
    ExtClassLoader -Djava.ext.dirs 设置ExtClassLoader的搜索路径
    AppClassLoader -Djava.class.path= 或-classpath 设置AppClassLoader的搜索路径
  • 除了bootstrap class loader,其他的类加载器都直接或间接继承自java.lang.ClassLoader 这个抽象类
  • 如果我们要实现自己的类加载器,无论是直接实现抽象类Class Loader,还是继承URLClassLoader类,或者其他子类,它的父加载器都是AppClassLoader
  • 双亲委派模型都要求除了bootstrap class loader,每个类加载器,都有自己的父类加载器,父子关系一般采取组合(composition)实现(通过子类包含父类的实例,通过调用实例的方法达到类似继承的效果,但是能依旧保持较好的封装性)

双亲委托模型的工作过程

  如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有所需的类)时,子加载器才会自己尝试加载.Java类随着它的类加载器一起具备了一种带有优先级的层次关系

类在JVM中的生命周期

加载→验证→准备→解析链接→初始化→使用→卸载

  • 类的加载过程,前面已经提到,就是双亲委派模型
  • 我们在这一部分主要关注的是链接部分(链接之前类必须被成功加载)
    • 验证(Verification):验证是用来确保Java类的二进制表示在构造上是完全正确的。 假如验证进程出现错误的话, 会抛出java. lang. VerifyError错误
    • 准备(Preparation):准备阶段是正式为类变量分配内存并设置类变量内存的阶段,这些变量所使用的内存都将在方法区进行分配(类变量就是指的静态变量) ,在初始化阶段才执行静态代码块和将静态字段初始化为默认值
    • 解析(Resolution):
      • 解析的过程就是确保这些被引用的类能被正确的找到,解析的过程可能会导致其它的Java类被加载z。
      • 不同的JVM实现可能选择不同的解析策略。
        • 一种做法是在链接的时候,就递归的把所有依赖的形式引用都进行解析。
        • 而另外的做法则可能是只在一个形式引用真正需要的时候才进行解析。也就是说如果一个Java类只是被引用了,但是并没有被真正用到,那么这个类有可能就不会被解析。
      • 对于解析的理解,其实可以是在虚拟机中对于符号引用转换成直接引用的验证和替换过程
        • 符号引用:唯一标识一个类、方法,包含描述信息的字符串
        • 直接引用:指向方法区的本地指针,指向实例变量,实例方法的直接引用都是偏移量

Class Loader的类结构分析

//将byte字节流解析成JVM能够识别的Class对象,有了这个方法一位着我们
//不仅可以通过Class文件获得Class对象,其他的字节流都可以
Class<?> defineClass ( byte[] , int , int )
//实现类的加载规则,从而取得要加载类的字节码
Class<?> findClass(String)
//如果不想重新定义加载类额规则,也没有复杂的处理逻辑
//只是想能够一个加载一个自己指定的类,可以直接使用load
Class<?> loadClass(String)
//链接参数类,链接参照上面,不再赘述
void resolveClass(Class<?>)
           

双亲委托模型的实现

protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{
        //检查指定类是否被当前类加载器加载过
        Class c = findLoadedClass(name);
        if( c == null ){//如果没被加载过,委派给父加载器加载
            try{
                if( parent != null )
                    c = parent.loadClass(name,resolve);
                else 
                    c = findBootstrapClassOrNull(name);
            }catch ( ClassNotFoundException e ){
                //如果父加载器无法加载
            }
            if( c == null ){//父类不能加载,由当前的类加载器加载
                c = findClass(name);
            }
        }
        if( resolve ){//如果要求立即链接,那么加载完类直接链接
            resolveClass();
        }
        //将加载过这个类对象直接返回
        return c;
    }
           

具体细节都已经在前文提到,不再赘述。

常见加载类错误分析

  • 类加载中最常见的错误是ClassNotFoundException和NoClassDefFoundError两个异常,既然常见我们就尽早分析,而且尽可能详尽地说明:
    • ClassNotFoundException:这个异常发生在显示加载类的时候,没有找到对应类的字节码,显示加载的方式如下:
      • 通过类class中的forName()方法
      • 通过ClassLoader中的loadClass()方法
      • 通过ClassLoader中的findSystemClass()方法
    • NoClassDefFoundError:隐式加载类时出现,涉及隐式加载的情景:
      • 使用new关键字
      • 属性引用加载某个类
      • 继承了某个接口或类
      • 以及方法的某个参数中引用了某个类
  • UnsatisfiedLinkError是一个在解析native标识的方法时出现的错误,是库文件缺失造成的,无法链接到本地的代码实现库(native实现)
package com.company;

/**
 * Created by liulin on 16-4-19.
 */
public class NoLibException {
    public native void nativeMethod();

    static{
        //静态代码块,在初始化类的时候输出
        System.out.println("NoLib");
    }

    public static void main ( String [] args ){
        //调用native方法时开始链接,链接失败抛出异常
        new NoLibException().nativeMethod();
    }
}
           

抛出异常如下,在加载NoLibException时没有链接native函数库,在调用时链接报错(了解异常抛出的时间点很重要)

JVM基础(一) ClassLoader的工作机制

- 还有一个常见的错误就是ClassCastException,JVM在做类型检查时会做如下的检查:

- 对于普通的对象,对象必须是目标类对象或者目标类对象的子类

- 对于数组类型,对象ixu是数组类型或java.lang.Object、java.lang.Cloneable、java.io.Serializable

所以在使用强制类型转换时,最好先利用instanceof操作符进行判断,再进行操作

原则的突破:服务接口与OSGi

  • 类似与JNDI和JDBC,因为他们是对命名和目录和数据库的同一接口,但是因为操作系统的不同,数据库的供应商的不同,会造成底层的实现存在不同,但是JNDI和JDBC作为java的标准服务,由BootStrapClassLoader加载,但是在JNDI调用实现代码时,bootstrapClassLoader却没能力加载实现的具体代码。所以使用了线程上下文加载器(Thread context ClassLoader)来代替bootstrapClassLoader对类进行加载,也就是父类请求子类完成加载任务,破坏了双亲委托模型,但是却解决了统一接口和不同底层实现的问题。
  • OSGi实现了代码热替换(HotSwap)、模块热部署(Hot Deployment)
    • 热部署通过自定义额类加载器实现。每一个程序模块Bundle都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连通类加载器一同换掉以实现代码的热替换
    • OSGi环境下,累加再起不是双亲委托模型中树状结构,而是复杂的网状结构
      • 1)将以java.*开头的类委托给父类加载器加载
      • 2)否则,将委派列表名单内额类委托给父类加载器加载
      • 3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
      • 4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
      • 5)否则查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
      • 6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
      • 7)否则,类查找失败