美文网首页javaJava学习笔记程序员
JVM浅析之一:类的加载

JVM浅析之一:类的加载

作者: 无为无悔 | 来源:发表于2016-09-16 12:38 被阅读153次

    我们都知道,Java的类包含属性和方法,类先要进行实例化,然后才是方法的调用,在这之前,还需要了解一个类是如何被JVM识别以及它在JVM中的生命周期是怎样的。

    一个程序的实现需要经过编译、链接、执行三个步骤,Java也不例外,一个类或者接口需要经过加载、链接、初始化三个步骤,类的加载意味着一个类或接口以字节码的形式加入到JVM容器中;链接是对字节码进行解析,并把JVM识别的字节码组织起来,以达到可以运行的准备状态,链接又包含三个步骤,验证、准备和解析,首先要验证类的基本结构是否正确,然后为类分配合适的资源,最后将类的符号引用转换成直接引用;类的初始化是将类的变量赋予正确的值。以下详细介绍每一步的实现。

    类的加载


    类的加载,是一个类或接口(.class文件)以字节码的形式加入到JVM容器中(在堆创建一个java.lang.class对象,保存的是类信息,跟所有静态变量、常量都放在方法区),类加载机制使得 Java 类可以被动态加载到 Java 虚拟机中并执行。

    所有类的加载都是由类加载器来完成(ClassLoader),JVM的启动也不例外,它是由多种类加载器来进行加载的,注意他们是有顺序的,从上到下可以理解为父子关系:

    • Bootstrap class loader:也叫作根加载器,它负责Java核心类库的加载(JAVAHOME/lib/rt.jar),正是由于它的底层特性,它也是由底层语言编写;
    • Extentions class loader:也叫扩展类加载器,它负责Java扩展库的加载(JAVAHOME/lib/ext)
    • System class loader:也叫系统(应用)类加载器,根据应用的CLASSPATH来加载相应目录下类和接口
    • 用户自定义类加载器:理论上系统提供的类加载器就可以完成大部分需求,应对某些需求,例如字节码的加密传输,还需要定义自己的类加载器来解密,首先继承java.lang.ClassLoader类,一般只需要重写loadClass()和findClass()两个方法。
    1. 类加载的双亲委派机制

    Java有一套安全的类加载机制,因此我们没必要重写loadClass方法来改变类加载顺序,Java默认使用双亲委派机制来对类进行加载。

    如果某个类加载器收到了一个类加载请求,如果发现此类未被加载过,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类)时parent.loadClass(String)findBootstrapClassOrNull(String),此时获得的父加载器为NULL,子加载器才会尝试着自己去加载。

    子类加载器的loadClass()方法实现,即整个加载顺序可以粗略的概括为

    // 先确定类有没有已经被加载了
    class = findLoadedClass(className);
    // 如没有加载,则委派双亲加载
    class = parent.loadClass(className);
    // 上层也未找到该类,则委派根加载器
    class = findBootstrapClassOrNull(className);
    // 如根加载器也找不到,则自己调用findClass(className)来查找类
    class = findClass(className)
    

    类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来,下次再请求加载该类的时候,类加载器会直接使用缓存的类实例(调用findLoadedClass(String)方法),而不会尝试再次加载。

    因此,双亲委派的好处就是,避免类的重复加载,子类只需要请上层父类确认即可,并且一个类只能由一个类加载器来加载,因此,JVM中确定了类的唯一性,同一个类不可能存在两份,具有一定的安全特性,避免恶意代码篡改核心类库。

    2. 源码角度熟悉类加载器

    下面研究下java.lang.ClassLoader

    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
                    // 做Java Web比较常见,.class文件路径不对或者未生成就会抛出这个异常
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
    
                if (c == null) {
                    // 如果父类也未能找到,则调用自己的findClass来查找
                    // 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;
        }
    }
    
    

    通过阅读loadClass()源码,我们了解到ClassNotFoundException是源自这段代码,下面再按照调用方法的顺序一一解析双亲委派机制。

    • 查找当前类加载器的class对象,也就是从自己已加载的实例缓存中查找
    protected final Class<?> findLoadedClass(String name) {
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }
    
    // 从实例缓存查找是以native方法实现
    private native final Class<?> findLoadedClass0(String name);
    
    • 通过parent.loadClass(),这是一个递归过程

    • 通过根加载器加载

    
    private Class<?> findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;
    
        return findBootstrapClass(name);
    }
    
    // 这里调用根类也是native方法
    // return null if not found
    private native Class<?> findBootstrapClass(String name);
    
    
    • 自己查找
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 这里并没有实现,因此我们自定义classLoader时需要overwrite
        // 注意这里也需要做异常的处理
        throw new ClassNotFoundException(name);
    }
    
    • 创建链接
    protected final void resolveClass(Class<?> c) {
        resolveClass0(c);
    }
    
    private native void resolveClass0(Class<?> c);
    
    • 字节码转换成.class文件,以上过程是为了定位,这一步完成类的整个解析和加载,会抛出以下三个异常:

    1) ClassFormatError:文件格式不合法

    2) NoClassDefFoundError:二进制转换后类名与原文件名不符

    3)SecurityException:加载的class是受保护的、采用不同签名的,或者类名是以java.开头的

    protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        int len = b.remaining();
    
        // Use byte[] if not a direct ByteBufer:
        if (!b.isDirect()) {
            if (b.hasArray()) {
                return defineClass(name, b.array(),
                                   b.position() + b.arrayOffset(), len,
                                   protectionDomain);
            } else {
                // no array, or read-only array
                byte[] tb = new byte[len];
                b.get(tb);  // get bytes out of byte buffer.
                return defineClass(name, tb, 0, len, protectionDomain);
            }
        }
    
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }
    
    private native Class<?> defineClass0(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd);
    
    private native Class<?> defineClass1(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd, String source);
    
    private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
                                         int off, int len, ProtectionDomain pd,
                                         String source);
    

    通过本文,了解到类加载的基本原理,无外乎就几个主要的方法,loadClass(),findClass()以及defineClass(),两个比较常见的异常ClassNotFoundException和NoClassDefFoundError,注意他们分别继承自java.lang.Exception以及java.lang.Error,我们在日常部署项目的时候经常遇到,遇到时要区分他们并且可以快速的定位,避免异常直接中断系统的发生。

    相关文章

      网友评论

        本文标题:JVM浅析之一:类的加载

        本文链接:https://www.haomeiwen.com/subject/fyovettx.html