深入JVM内核(六)

JAVA 2015-12-11

今天来介绍JVM中的类装载器

首先简单介绍一下class类的装载验证流程:首先加载,然后链接,链接又包含(验证 ,准备,解析),然后再进行初始化。 下面具体介绍

1.加载过程是装载类的第一个阶段,在这个过程取得类的二进制流并转为方法区数据结构 同时在Java堆中生成对应的java.lang.Class对象。 2.链接验证过程的目的是为了保证Class流的格式是正确的,它包含了四种验证: a.文件格式的验证(是否以0xCAFEBABE开头,版本号是否合理) b.元数据验证(是否有父类,继承了final类,非抽象类实现了所有的抽象方法) c.字节码验证(运行检查,栈数据类型和操作码数据参数吻合,跳转指令指定到合理的位置) d.符号引用验证(常量池中描述类是否存在,访问的方法或字段是否存在且有足够的权限) 3.链接准备阶段负责分配内存,并为类设置初始值 (方法区中),例如public static int v=1;在准备阶段中,v会被设置为0;在初始化的中才会被设置为1;对于static final类型,在准备阶段就会被赋上正确的值;public static final int v=1 4.链接解析阶段符号引用替换为直接引用 5.初始化过程执行类构造器(针对static变量,赋值语句以及static{}语句),子类的调用前保证父类的被调用同时是线程安全的。 那么什么是类装载器ClassLoader呢? 首先ClassLoader是一个抽象类,然后ClassLoader的实例将读入Java字节码将类装载到JVM中,并且ClassLoader可以定制,满足不同的字节码流获取方式,ClassLoader负责类装载过程中的加载阶段。 下面让我们看看JDK中ClassLoader默认的设计模式 ClassLoader的重要方法

public Class<?> loadClass(String name) throws ClassNotFoundException 载入并返回一个Class protected final Class<?> defineClass(byte[] b, int off, int len) 定义一个类,不公开调用 protected Class<?> findClass(String name) throws ClassNotFoundException loadClass回调该方法,自定义ClassLoader的推荐做法
protected final Class<?> findLoadedClass(String name) 寻找已经加载的类

1.分类设计模式 BootStrap ClassLoader (启动ClassLoader) Extension ClassLoader (扩展ClassLoader) App ClassLoader (应用ClassLoader/系统ClassLoader) Custom ClassLoader(自定义ClassLoader) 每个ClassLoader都有一个Parent作为父亲

2.协调工作设计模式 1.png 下面是Java中的ClassLoader类中的loadClass方法:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                 // ClassNotFoundException thrown if class not found
                 // from the non-null parent class loader
                }

                if (c == null) {
        // If still not found, then invoke findClass in order
        // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name); 
       // this is the defining class loader; record the stats                       sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

加载某个类时,当前classloader都会先委托parent class loader尝试加载。这种处理方式,有几个方面的考虑:一是安全问题,如果攻击者模拟实现了对应的类如java.lang.String, 当通过loadClass来加载类时,会首先到parent classloader中查找,很明显可以在bootstrap class loader中找到,这样可以尽可能保护最关键的代码。一是冗余问题,通过这种方式可以最大限度的降低重复加载。每个层次的classloader负责对应路径下的类库加载,而对于应用实现来说就可以集中在应用系统中。这里可以考虑一下web container的实现,如tomcat。每个jsp页面都会最终被编译成一个class文件,并放到work路径中,所以每个web容器都会自建一个classloader来加载指定路径中的class文件。 下面再看两段代码

 package com.test;
public class HellowLoader {
    public void print(){
    System.out.println("I am in apploader");
    }
}

package com.test;
import com.test.HellowLoader;
public class FindClassOrder {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        HellowLoader loader=new HellowLoader();
        loader.print();
    }
}

直接运行FindClassOrder类输出是I am in apploader 如果加上参数 -Xbootclasspath/a:D:/tmp/clz,输出是I am in bootloader 此时AppLoader中不会加载HelloLoader,I am in apploader 在classpath中却没有加载,说明类加载是从上往下的。 如果强制在apploader中加载

package com.test;
import java.lang.reflect.Method;
public class Test {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ClassLoader cl=FindClassOrder.class.getClassLoader();
        byte[] bHelloLoader=loadClassBytes("geym.jvm.ch6.findorder.HelloLoader");
        Method md_defineClass=ClassLoader.class.getDeclaredMethod("defineClass", byte[].class,int.class,int.class);
        md_defineClass.setAccessible(true);
        md_defineClass.invoke(cl, bHelloLoader,0,bHelloLoader.length);
        md_defineClass.setAccessible(false);        
        HellowLoader loader = new HellowLoader();
        System.out.println(loader.getClass().getClassLoader());
        loader.print();
    }
}

参数-Xbootclasspath/a:D:/tmp/clz,输出:I am in apploader 说明在查找类的时候,先在底层的Loader查找,是从下往上的。Apploader能找到,就不会去上层加载器加载。 3.问题设计模式 2.png

4.解决设计模式 其中Thread. setContextClassLoader()方法是上下文加载器,是一个角色,用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题,基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例。 我们来看看javax.xml.parsers.FactoryFinder如何在启动类加载器加载AppLoader的类

static private Class getProviderClass(String className, ClassLoader cl,
        boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
    try {
        if (cl == null) {
            if (useBSClsLoader) {
                return Class.forName(className, true, FactoryFinder.class.getClassLoader());
            } else {
                cl = ss.getContextClassLoader();
                if (cl == null) {
                    throw new ClassNotFoundException();
                }
                else {
                    return cl.loadClass(className); //使用上下文ClassLoader
                }
            }
        }
        else {
            return cl.loadClass(className);
        }
    }
    catch (ClassNotFoundException e1) {
        if (doFallback) {
            // Use current class loader - should always be bootstrap CL
            return Class.forName(className, true, FactoryFinder.class.getClassLoader());
        }
…..

上下文ClassLoader可以突破双亲模式的局限性

双亲模式可以被破坏,虽然双亲模式是默认的模式,但不是必须这么做,例如Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent;OSGi的ClassLoader形成网状结构,根据需要自由加载Class。 破坏双亲模式例子- 先从底层ClassLoader加载

OrderClassLoader的部分实现

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // First, check if the class has already been loaded
    Class re=findClass(name);
    if(re==null){
        System.out.println(“无法载入类:”+name+“ 需要请求父加载器");
        return super.loadClass(name,resolve);
    }
    return re;
}
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(className);
if (null == clazz) {
    try {
        String classFile = getClassFile(className);
        FileInputStream fis = new FileInputStream(classFile);
        FileChannel fileC = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel outC = Channels.newChannel(baos);
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
         省略部分代码
        fis.close();
        byte[] bytes = baos.toByteArray();

        clazz = defineClass(className, bytes, 0, bytes.length);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
return clazz;
}
OrderClassLoader myLoader=new OrderClassLoader("D:/tmp/clz/");
Class clz=myLoader.loadClass("geym.jvm.ch6.classloader.DemoA");
System.out.println(clz.getClassLoader());

System.out.println("==== Class Loader Tree ====");
ClassLoader cl=myLoader;
while(cl!=null){
    System.out.println(cl);
    cl=cl.getParent();
}

程序的输出为:

java.io.FileNotFoundException: D:\tmp\clz\java\lang\Object.class (系统找不到指定的路径。)
at java.io.FileInputStream.open(Native Method)
.....
at geym.jvm.ch6.classloader.ClassLoaderTest.main(ClassLoaderTest.java:7)
无法载入类:java.lang.Object需要请求父加载器
geym.jvm.ch6.classloader.OrderClassLoader@18f5824
==== Class Loader Tree ====
geym.jvm.ch6.classloader.OrderClassLoader@18f5824
sun.misc.Launcher$AppClassLoader@f4f44a
sun.misc.Launcher$ExtClassLoader@1d256fa

DemoA在ClassPath中,但由OrderClassLoader加载 因为先从OrderClassLoader加载,找不到Object,之后使用appLoader加载Object

如果OrderClassLoader不重载loadClass(),只重载findClass,那么程序输出为

sun.misc.Launcher$AppClassLoader@b23210
==== Class Loader Tree ====
geym.jvm.ch6.classloader.OrderClassLoader@290fbc
sun.misc.Launcher$AppClassLoader@b23210
sun.misc.Launcher$ExtClassLoader@f4f44a

此时DemoA由AppClassLoader加载

类加载器中还存在热替换现象,所谓热替换就是当一个class被替换后,系统无需重启,替换的类立即生效,例如

public class CVersionA {
    public void sayHello() {
        System.out.println("hello world! (version A)");
    }
}

DoopRun 不停调用CVersionA . sayHello()方法,因此有输出 hello world!(version A) 在DoopRun 的运行过程中,替换CVersionA 为:

public class CVersionA {
    public void sayHello() {
        System.out.println("hello world! (version B)");
    }
}

替换后, DoopRun 的输出变为:hello world! (version B)


本文由 Tony 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧