Java -- Class装载机制及ClassLoader

面向对象的语言如cpp、Java、C#等,都很好的支持了类 - class,在class的基础之上也实现了各个语言的面向对象的很多如多态、泛型等功能。cpp属于编译语言,类是”静态”的概念,会被编译器翻译成二进制的机器码,变成静态的符号。而像Java这种编译器编译+虚拟机解释的语言,虚拟机在编译后字节码中,在语义上更加灵活的支持class,比如我们可以通过代码对class进行操作,使程序能实现更丰富的功能(比如HotCode,字节码修改等)。

后续的内容将围绕Java语言的Class进行一些介绍,欢迎拍砖和讨论:

  1. 初识Java加载机制
  2. Class文件结构
  3. ClassLoader
  4. 自定义ClassLoader
  5. 定制代码热替换的ClassLoader

(本文涉及的环境为HotSpot JDK v1.7.0_67 64-Bit, Mac OSX)


  • 初识 Java Class

    最简单的在Java中使用一个Class的方式就是new,可以把定义好的Java类实例化出来使用。当解释器看到这个类的时候,如果是第一次看到这个类的符号,不知道它有哪些成员,哪些函数,就要去加载并解析这个类,才能在代码里被引用。分为如下步骤:

    这里写图片描述

    1. 加载
      获得包名和类名之后,JVM要通过文件、URL的方式读入class的完整字节序列(如字节数组),放入内存交给ClassLoader(类加载器),当遇到如下主动访问时,会触发该class的装载

      • main入口类
      • 类实例创建的时候,比如通过new、反射、clone()等
      • 调用类的静态方法、静态字段(final除外)
      • 调用java.lang.reflect反射类方法和字段
      • 初始化子类,自动初始化父类
    2. 验证
      根据读入的字节序列,判断该序列的合法性规范性

      • 检查class文件头合法性(幻数、版本信息)
      • 对class结构语义检查,是否有父类,是否覆盖了final方法等
    3. 解析class
      把字节序列转换成可识别的Class

    4. 初始化
      调用函数(classinit)、初始化块,初始化class


  • Class文件结构

    经过上述初始化后,该class就可以被使用和访问了。那么以我们最经常使用的方式,class以文件形式在磁盘被读入内存解析。下面我们来看一下具体的class文件结构是什么样子的

    • 写一个最简单的Java类
    public class Test {
        private int myValue = 4096;
        public void myFunc(long a) {
            long c = a;
        }
    } 
    • 生成的Test.class文件内容如下

    Java -- Class装载机制及ClassLoader_第1张图片

     - cafebabe
     固定的文件头幻数(MagicHeader)
    
     - 00 00 00 33
     此版本号及主版本号
        J2SE 8 = 52   (0x34 hex)
        J2SE 7 = 51   (0x33 hex)
        J2SE 6.0 = 50 (0x32 hex)
        J2SE 5.0 = 49 (0x31 hex)
        JDK 1.4 = 48  (0x30 hex)
        (引自wiki百科)
    
     - 00 1b
     常量池元素个数(代表后面包含27-1=26个元素符号)
    
     - 0a 00 08 00 15
     对应一个常量符号,0a代表常量类型(MethodRef),08和15分别为指向的常量索引
     此处不同版本的jdk存储常量的顺序可能不一致,各种常量类型对应的长度也不一致,有兴趣的读者可参阅官方文档
     http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
    

    我们也可以通过jdk的工具 javap -v -constants -c -private Test 方便的查看常量池

    Java -- Class装载机制及ClassLoader_第2张图片

     - 00 21 常量池最后一个符号式 `java/lang/Object` 后面是访问控制字节(.!这两个字符开始)
     代表public class
    
     - 00 07 00 08 
     this_class和super_class,数字对应常量池中得索引即7和8
    
     - 字段及方法的列表
     (此处不再赘述)
    

  • Class loader

Class loader是加载并解析class的关键模块,在JVM中,有多种Class loader:

BootStrap Classloader 启动类加载器(如rt.jar),cpp实现,Java中不可见
Extension Classloader 扩展类加载器,/lib/ext和-Djava.ext.dirs的加载等
App Classloader 也称为System ClassLoader,负责classpath下类加载,可通过ClassLoader.getSystemClassLoader()获得
自定义Classloader 应用自定义类加载器

Java -- Class装载机制及ClassLoader_第3张图片

除了Bootstrap Class Loader外,其它的类加载器都是java.lang.ClassLoader类的子类

各个加载器通过双亲委托的机制来相互配合:

1.  当系统需要一个类时,加载器自底向上进行寻找
2.  当系统加载一个类时,加载器自顶向下加载(可以防止开发者覆盖Java核心类库)

见java.lang.ClassLoader类loadClass方法

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 检查该class是否已经加载过了,有则直接返回Class引用
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 使用双亲委托机制,让父加载器优先加载,避免JVM核心类库被下层加载器覆盖!
                if (parent != null) {
                    // 如果存在父加载器,则让父加载器进行加载,委托给双亲
                    c = parent.loadClass(name, false);
                } else {
                    // 如果不存在父加载器,则是Bootstrap加载器,直接加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 未找到则继续
            }
            if (c == null) {
               long t1 = System.nanoTime();
               // 父加载器未找到的话,则由当前加载器进行加载,findClass为虚方法,需要ClassLoader对应子类进行实现
               c = findClass(name);
               sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
               sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
               sun.misc.PerfCounter.getFindClasses().increment();
           }
         }
         if (resolve) {
             // 解析class
             resolveClass(c);
         }
         return c;
    }
}  

  • 自定义ClassLoader

JVM默认的ClassLoader规范,严格的定义了ClassLoader的加载顺序。使得类加载是清晰、明确的单向加载、隔离的行为。一般获取ClassLoader的方式有以下几种:

  • ClassLoader.getSystemClassLoader().getParent()
    获得Extension ClassLoader

  • Thread.getContextClassLoader 包含线程上下文的classloader
    classloader是线程相关的,具有父子关系的线程间,默认共享一个classloader,也可通过Thread.setContextClassLoader打破

  • ClassLoader.getSystemClassLoader()
    获取App Classloader

  • this.class.getClassLoader()
    获取加载当前类的Classloader

在双亲委托模式下,下层加载器可以使用父加载器加载的类,即应用使用JDK核心类库。但反过来是不允许的,核心类库不能直接访问应用类。这种方式在一些情况下会造成一些麻烦。例如在使用JDBC时,应用要主动额外的加载数据库Driver类,虽然在应用看来这显得没有必要:

try {
    Class.forName("com.mysql.jdbc.Driver"); //加载mysql driver驱动
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

// 获取驱动对应的Connection
Connection conn = DriverManager.getConnection(url,user,password);

分析一下getConnection()方法

 private static Connection getConnection(String url, java.util.Properties info, Class caller) 
                                         throws SQLException {
      // 首先获取caller的ClassLoader,caller类指向调用getConnection的Class
      // 这里通过调用类的ClassLoader或Thread.getContextClassLoader()进行加载
      ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
      synchronized (DriverManager.class) {
          if (callerCL == null) {
              callerCL = Thread.currentThread().getContextClassLoader();
          }
      }

      ...

      for(DriverInfo aDriver : registeredDrivers) {
          // 当前的ClassLoader是否可以加载driver
          if(isDriverAllowed(aDriver.driver, callerCL)) {
              try {
                  ...
                  Connection con = aDriver.driver.connect(url, info);
                  ...
              } catch (SQLException ex) {
              }
          } 
      }
      ...
  }

那么,我们如何在必要时打破双亲委托的模型?最简单的我们可以实现一个自定义的ClassLoader

public class YuShanFangMain {

    public static class YuShanFang {
        public String bonjour = "say hello";
    }

    /**
     * 自定义的ClassLoader的子类,忽略双亲委托机制,简单的直接返回Class
     */
    public static class YuShanFangClassLoader extends ClassLoader {

        private Map> supportedClass = new HashMap<>();

        public YuShanFangClassLoader() {
            super();
            supportedClass.put("com.dev.YuShanFangMain.YuShanFang", YuShanFang.class);
        }

        /* 重写loadClass,根据名字寻找class */
        @Override
        public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
            Class c = findLoadedClass(name);
            if (c == null) {
                c = findClass(name);
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

        /* 重写findClass,根据名字找到class */
        @Override
        protected Class findClass(String name) throws ClassNotFoundException {
            System.out.println(String.format("YuShanFang ClassLoader look up the class %s", name));
            return supportedClass.get(name);
        }
    }

    public static void main(String[] args) {

        // 自定义的ClassLoader简单的实现loadClass、findClass即可
        ClassLoader ysfClassLoader = new YuShanFangClassLoader();

        try {
            Class ysf = ysfClassLoader.loadClass("com.dev.YuShanFangMain.YuShanFang");
            System.out.println(ysf.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

  • 定制代码热替换的ClassLoader

通过上述我们可以看到实现一个自定义的ClassLoader足够简单,也带来很多灵活性。在这种模式下,我们可以自己开发高级功能,比如局部的代码热替换(运行中更改一个类的定义和行为),类似HotCode的功能。下面我们实现一个简陋版本的HotCode,模拟代码热替换

public static class YuShanFangClassLoader extends ClassLoader {
        /**
         * 简单起见我们检测一个class文件的变化,我们只访问这一个类
         */
        private String hotCodeClass = "HotCodeClass";
        private String fileName = String.format("/tmp/%s.class", hotCodeClass);
        private static ConcurrentMap> codeCache = new ConcurrentHashMap<>();

        @Override
        public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {

            // 这里我们要去掉findLoaderClass, 不使用代码类的缓存
            // Class c = findLoadedClass(name);

            Class c = findClass(name);
            if (c == null)
                c = super.loadClass(name, resolve);
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

        protected Class findClass(String name) throws ClassNotFoundException {
            Class clazz = null;

            /**
             * 1. 判断是否是此类加载器关心的类,如果不是则跳过加载 加载类时,
             *    会遇到父类、父接口等,比如任何类的默认父类Object,
             *    不能加载这些类。交由上层类加载器加载
             */
            if (!name.equals(hotCodeClass))
                return clazz;

            /**
             * 2. 检测代码文件时间戳(也可以是MD5)是否发生变化
             */
            File codeFile = new File(fileName);

            String identifier = String.format("%s.%d", fileName, codeFile.lastModified());
            clazz = codeCache.get(identifier);

            if (clazz != null) 
                return clazz;

            /**
             * 3. 发生变化后重读class文件字节码
             */
            try (FileChannel channel = new FileInputStream(codeFile).getChannel()) {
                ByteBuffer buffer = ByteBuffer.allocate((int) (codeFile.length() * 2));
                while (true) {
                    int n = channel.read(buffer);
                    if (n == 0 || n == -1)
                        break;
                }
                byte[] bitsOfClass = new byte[buffer.position()];
                System.arraycopy(buffer.array(), 0, bitsOfClass, 0, buffer.position());
                buffer.array();

                /**
                 * 4. 解析字节码, defineClass是ClassLoader的默认解析器实现
                 *    这里我们复用即可
                 */
                clazz = defineClass(name, bitsOfClass, 0, bitsOfClass.length);


                /**
                 * 5. 缓存
                 */
                if (clazz != null) {
                    codeCache.put(identifier, clazz);
                }

            } catch (IOException | SecurityException e) {
                e.printStackTrace();
            }
            return clazz;
        }
    }
while (true) {
    try {
        // 注意,每次要使用心得ClassLoader实例,否则ClassLoader会认为
        // 加载的类为重复定义的!最简单的删除ClassLoader里已注册的Class
        // 的方法是让GC回收这个ClassLoader !!
        ClassLoader ysfClassLoader = new YuShanFangClassLoader();
        Class ysf = ysfClassLoader.loadClass("HotCodeClass");
        Object object = ysf.newInstance();
        Method method = ysf.getDeclaredMethod("func", int.class);
        System.out.println(method.invoke(object, 5));
    } catch (Exception e) {
        e.printStackTrace();
    }
    Thread.sleep(1000L);
}

HotCodeClass实现如下

public class HotCodeClass {
    public int func(int a) {
        return a + a;
        //return a * a;
    }
}

程序输出如下:

25
25
25
25
10
10
10
10


很多web容器就是为了实现隔离,防止命名污染等,都是通过自定义不同的ClassLoader来实现,比如Tomcat。所以,灵活的运用ClassLoader可以带来很多高级功能,实现动态的加载管理。对高级功能有兴趣的读者可留言讨论!

你可能感兴趣的:(java,JVM)