Class与ClassLoader深入浅析

字数 3773阅读 66

先来一张java中JVM内的类加载器的层次体系结构,总整体上观看JVM中所包含的classLoader有哪些,以及它们之间的关系:

class_loader.jpg

说到classLoader,从名字也可以知道它的作用。不就是加载类(class)的么,那么如何加载?何时加载?谁来加载呢等等问题,下面我就要来说说,实践实践。当然了,我还是先从class这个在OOP中属于基石核心的概念来说说,并看看它是在java平台是如何来的,具体长什么样哈?

Class文件结构?

java平台的强大跨平台特性就是靠着这个东东啊。只要在java虚拟机上运行开发的语言,像java,jruby,groovy等语言,都是运行在JVM基础之上的。那么在源代码形式下,通过各自语言的编译器,按照java虚拟机规范(JVM要求在class文件中使用许多强制性的语法和结构化约束)处理,就能得到一个通用的,机器无关的执行平台的字节码文件(*.class)。就如下图所示:

javac_class.png

那么,这个万能的class结构是什么样的呢?在java语言的jdk中是如何表示的?
由于JVM底层是c语言支持的,class文件当然要通过符合C语言的数据结构来构造了。class的数据结构如下:
无符号数数据基本数据类型,以u1,u2,u4,u8来分别代表一个字节(八位bit,也就是八个坑,每个坑只能放0和1),2个字节,4个字节和8个字节的无符号数。
其中还包含表的概念,表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有的表习惯性的以”_info”结尾。整个class文件本质上就是一张表。

Class {
    u4              magic;                  // 1 个  魔数                      
    u2              minor_version;          // 1    副版本号
    u2              major_version;          // 1    主版本号
    u2              constant_pool_count;    // 1    常量池计数器
    cp_info         constant_pool;          // constant_pool_count - 1 常量池数据区
    u2              access_flags;           // 1    访问标识
    u2              this_class;             // 1    类索引
    u2              super_class;            // 1    父类索引
    u2              interfaces_count;       // 1    接口计数器
    u2              interfaces;             // interfaces_count  接口信息数据区
    u2              fields_count;           // 1    字段计数器
    field_info      fields;                 //fields_count  字段信息数据区
    u2              methods_count;          // 1    方法计数器
    method_info     methods;                // methods_count 方法信息数据区
    u2              attributes_count;       // 1    属性计数器
    attribute_info  attributes;             // attributes_count 属性信息数据区
}

可以看到,在OOP中class对象所涉及到的属性,方法都有自己对应的表(field_info,method_info),其他的一些是其他信息的记录。

class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按照顺序紧凑的排列在Class文件中,中间没有任何的分隔符。
这使得整个Class文件中存储的内容集合全部都是程序运行的必要数据,没有空隙存在。

那当java文件经过编译得到的class的文件是什么样的?
我们若是想要看懂class文件中常量池内容,首先需要先知道常量池中14种常量池的结构,如下图:

constant_pool_structrue_1.png

constant_pool_structrue_2.png

理论上知道了class内部结构,那么就可以自己编写一个简单java程序,并用javac命令将该java源代码文件编译成字节码,在用编辑器打开该字节码16进制可以看到:
(现在我们看看class的二进制文件:(用了简陋画图工具制作的,我使用JDK1.8来操作。看懂就就行(▔^▔)/ ))

class_file.png

卧槽,画个图,洪荒之力都出来了。好累….好了,关于java中class文件内容就说到这。class文件中内容包含挺多数据的,这些数据就决定了当类加载器加载该类,形成实例对象后具备那些功能啦。那么反编译工具如何得到java源文件内容的问题也就明白了,还有其他像一些动态加载和代理增强功能实现,都是可以直接通过字节码进行操作实现的。
想看更详细内容可以看这篇:JVM详解博客

class文件如何被加载到JVM?

在class文件通过源代码编译生成后,我们也知道class文件内具体的二进制数据内容代表什么了,下面应该了解了解class文件是如何被加载到JVM虚拟机中的。类的加载当然与classLoader密不可分了。在java虚拟机中,类加载全过程包括:加载,验证,准备,解析和初始化五个阶段。在加载阶段,虚拟机需要完成以下3件事情:

1. 通过一个类的全限定名来获取定义此类的二进制字节流。(可以从本地文件,网络中,运行时动态生成,数据库文件等等地方获取类字节流)
2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

我在此说的就是这个加载阶段过程。

JVM类加载器种类:

  • Bootstrap classLoader(引导类加载器):
    该类负责将存在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(eg: rt.jar,名字不符合规范的即使放在lib中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用。负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等。

  • Extension ClassLoader(拓展类加载器):
    这个加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载\lib\ext目录中,或者被java.ext.dirs系统环境变量指定路径中的所有类库,开发者可以直接使用扩展类加载器。

  • .Application ClassLoader(应用系统程序类加载器):
    这个类加载器由sun.misc.Launcher$AppClassLoader实现,由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以一般称为系统类加载器。它负责加载用户类路径(CLASSPATH)上所指定的类库,开发者可以直接使用这个类加载器。通常我们自己写的java类不就是通过该加载器获取*.class中内容,并加载对应的Class对象的么

  • Custom ClassLoader(自定义类加载器):
    想要自己实现一个类加载器,只需要继承ClassLoader类即可。关于自定义类加载器有什么作用,如何具体的实现自定义类加载器,需要在另外的文章中说了。

在这个加载阶段,对于一个非数组类加载阶段(或者说加载阶段中获取二进制字节流动作)是我们开发人员可控性最强的,因为该阶段既可以使用系统提供的引导类加载器完成,也可以有我们开发人员自定义的类加载器去完成,也就是说开发人员可以自定义类加载去控制字节流的获取方式。

对于数组类而言,情况有所不同,数组类本身不通过类加载创建,它是由Java虚拟机直接创建的。但是数组类里面的数据类型的加载就与类加载器有关了:

  1. 若是数组的组件类型(ComponentType)是引用类型,那么就采用常规类加载器加载这个类,该数组将在该组件类型的类加载器的类名称空间上被标识。
  2. 若是组件类型不是引用类型(eg: int[]),java虚拟机将会把该数组标记为与引导类加载器关联(Bootstarp classLoader)用来确定一个类的唯一性。

ClassLoader加载class过程

类加载器层次关系是双亲委派模型,该模型要求除了顶层的类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承关系来实现,而都是使用组合关系来复用父加载器的代码。本来想写写双亲委派的具体好处,和劣势的,因为篇幅太大,就另起文章来说到说到。

      Bootstrap classLoader
             /\
            /||\
       Extenssion ClassLoader
             /\
            /||\
      Application ClassLoader
        /|         |\
User ClassLoader    User ClassLoader(自定义类加载器)

看看JDK中关于系统类加载器代码:java.lang.ClassLoader.java:

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();    //初始化获取sun.misc.Launcher中的AppClassLoader类加载器
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
// The class loader for the system
// @GuardedBy("ClassLoader.class")
private static ClassLoader scl;
// Set to true once the system class loader has been set
// @GuardedBy("ClassLoader.class")
private static boolean sclSet;
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();      //加载sun.misc,Launcher类
        if (l != null) {
            Throwable oops = null;
            scl = l.getClassLoader();                               //加载sun.misc,Launcher类中AppClassLoader
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;      //若为真,表示系统类加载器加载成功完成
    }
}

上面可以看到,在ClassLoader类中,initSystemClassLoader方法会加载sun.misc.Launcher中的AppClassLoader属性值,
就是获取应用类加载器。
那么该类的作用就是将CLASSPATH中java库所有二进制类字节流加载到方法区(运行常量池,类型信息,字段信息,方法信息,类加载器引用等)中

而引导类加载器Bootstrap ClassLoader是JVM启动的时候,就会自动创建该实例。

那看看sun.misc.Launcher源码,就可以知道拓展类加载器和应用类加载器的具体加载过程:(该Launcher类在rt.jar包中,该包放置所有J2SE的必要类)

当JVM启动创建Bootstrap ClassLoader时候,就会加载rt.jar包中所有二进制字节流类信息,到JVM中的方法区中。
然后,通过这些全限定的类字节文件,就可以创建对应的类对象实例,并将实例保存到Heap中。

public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    private static Launcher launcher = new Launcher();
    public static Launcher getLauncher() {
        return launcher;
    }
    private ClassLoader loader;
    
    //ClassLoader.getSystemClassLoader会调用此方法
    public ClassLoader getClassLoader() {
        return loader;
    }
    public Launcher() {
        // 1. 创建ExtClassLoader 
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader");
        }
        // 2. 用ExtClassLoader作为parent去创建AppClassLoader 
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader");
        }
        // 3. 设置AppClassLoader为ContextClassLoader
        Thread.currentThread().setContextClassLoader(loader);
        //...
    }
    static class ExtClassLoader extends URLClassLoader {
        private File[] dirs;
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();
            return new ExtClassLoader(dirs);
        }
        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);
            this.dirs = dirs;
        }
        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            //...
            return dirs;
        }
    }
    /**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {
        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);
            URL[] urls = (s == null) ? new URL[0] : pathToURLs(path);
            return new AppClassLoader(urls, extcl);
        }
        AppClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent, factory);
        }
        
        /**
         * Override loadClass so we can checkPackageAccess.
         */
        public synchronized Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            int i = name.lastIndexOf('.');
            if (i != -1) {
                SecurityManager sm = System.getSecurityManager();
                if (sm != null) {
                    //
                    sm.checkPackageAccess(name.substring(0, i));
                }
            }
            return (super.loadClass(name, resolve));
        }
    }
}

可以看到,当获取AppClassLoader的时候,就loader字段赋值时候,是通过传递extcl变量,通过父类加载器去完成的。
引导类是加载java运行是必要的类库文件。

系统类加载器则是我们开发中经常写的源代码编译成字节码,加载字节码类信息到JVM方法区域中的工具。

因为不同的加载器的命名空间会对相互加载的类的访问性,可见性等都会有影响。每个线程其实也绑定着一个上下文的类加载器:

  1. 同一个命名空间内的类是相互可见的。
  2. 子加载器的命名空间包含所有的父加载器的命令空间,因此子加载器加载的类能看见父加载器加载的类。(eg:AppClassLoader可以看见所有BootstrapClassLoader加载的类,就像java.lang.*下所有的包,我们自定义的类都能使用该包空间下的所有的类。)
  3. 由父加载器加载的类不能看见子加载器加载的类。(这也算是双亲委托加载的一个弊端了,若是父加载器想要加载子加载器加载的类如何实现?)
  4. 如果两个加载器之间没有直接或者间接的父子关系。那么它们各自加载的类是互不可见的。(像很多三方框架和服务器,eg:tomcat中,自定义由类的类加载器来加载不同的命名空间的类,那么这些类互不可见,完全解耦,互不干扰和影响。注意这与具备父子关系的父加载器想访问子加载器加载的类是不同的)
  5. 当两个不同命名空间内的类互不可见时候,其实还是可以采用java的反射机制来访问实例的属性和方法。

class文件中的信息在JVM如何存储的?

Class对象比较特殊,它虽然是对象,但是存放在方法区里面。根据我目前有限知识,参考了其他书籍可知。当JVM启动实例化引导类加载器和拓展类,应用程序加载器时候,就会把包括JDK中重要库包(eg:rt.jar,resources.jar、charsets.jar等)和我们自己写的java代码编译过后,保存这class具体信息的字节码文件中的类信息,都加载到JVM中的方法区中了。

包括将类中所有信息(运行时常量,类型信息,字段信息,方法信息,属性信息,类加载器引用类信息,对应class实例引用)都放置到方法区。

就像一个大的加工类工厂一样,将包装了许多原料的麻包,麻袋通过机器,人工(就相当与类加载器)精密处理,将不同的物料分配到仓房中的不同指定位置,以备后面需要材料的时候,进行出库等等。

存放在JVM方法区中的并不包含类字节流对应的对象实例。只是存放类的信息和一些class实例引用。而对象实例大多通常都是创建在heap堆区域中了。

class对象如何获取和创建?

类初始化节点是类加载过程最后一步,除了上面说的可以自定义类加载器对二进制字节流自行加载外,其余过程都是JVM来主导和控制运行的。到了初始化阶段,才是真正执行类中定义的java代码。(或者是class字节码文件中已保存到JVM方法区中的内容)

当一系列的类准备,加载,验证,解析,初始化后呢,在java内存中也就有了对应类的Class代表的对象
在jdk的源码包:java.lang.Class中也说到java系统程序在运行时,一直对所有的对象进行所谓的运行时类型标识(静态的编译类型,动态的运行时类型),这项信息保存了每个对象所属于的类,JVM通常就可以使用运行时类型标识信息来选择正确的方法调用执行,用来保存每个对象的类型标识信息的类就是Class类。(在JVM类加载器加载类字节流的时候,也就会自动创建所谓的类型标识,将方法区域中类的信息都映射保存到Class的类对象实例中)

Class类没有公共的构造器,Class对象是当JVM中类加载器加载很多类字节流和显示调用defineClass方法时自动创建好的。
java应用程序中的每个实例对象通过obj.getClass()都能得到其对应的Class类。每个类都有一个Class对象,当程序运行时候,JVM首先检查要加载的类对应的Class对象是否已经加载。如果该Class对象没有初始化加载,那么JVM会根据全限定类名对.class文件在类路径上进行查找,并将.class文件内容装载到Class对象中

每个数组也会被映射到一个对应的Class对象实例,并且所有具有相同元素类型和维度的数组都共享该Class对象。一般某个类的Class对象被载入内存,那么就可以用来创建这个类的所有对象。(MyObject o = new MyObject())

  • 如何可以得到Class对象?
  1. 调用Object类的getClass()方法可以得到对应的Class对象。
Myobject o;
Class c1 = o.getClass();
  1. .使用Class类中的静态forName()方法得到与全限定名字符串对应的Class对象。也就是通过反射来获取。(JVM类加载器加载,并封装class信息到Class对象中)
Class c2 = Class.forName("xx.xx.MyObject");
  1. 如果T时一个java类型(基本类型,引用类型),那么可以通过T.class就代表了匹配的类对象。
Class cl1 = Student.class;
Class cl2 = int.class;
Class cl3 = String[].class;

那么java.lang.Class有哪些常用的方法?

  1. getName()
    一个Class对象描述了一个特定类的属性,Class类中最常用的方法getName已String形式返回Class 对象所表示的实体(类、接口、数组类、基本类型或 void)名称。
  2. newInstance()
    Class还有一个有用的方法可以为类创建一个实例,这个方法叫做newInstance()。例如:
    x.getClass.newInstance(),创建了一个同x一样类型的新实例。newInstance()方法调用默认构造器(无参数构造器)初始化新建对象。
  3. getClassLoader()
    返回加载该类字节码的类加载器。

总的大体来说,java程序运行与OS交互图如下:(参考自网络)


all.jpg

好了,对于class说得差不多了。从一开始的java源代码,经过编译成*.class字节码文件,也详细分析了字节码里二进制对应的含义。然后到JVM类加载器加载这些class字节码文件,生成java.lang.Class对象。这个流程也流畅的完结了,若是有不对的地方,再修改。(仅供抛砖引玉.......)

参考:
Java虚拟机规范(Java SE 7)中文版(Java_Virtual_Machine_Specification_Java_SE_7)
[深入理解Java虚拟机:JVM高级特性与最佳实践].周志明
亦山博客

推荐阅读更多精彩内容