javaJava学习笔记程序员

JVM浅析之一:类的加载

2016-09-16  本文已影响153人  无为无悔

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

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

类的加载


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

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

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是源自这段代码,下面再按照调用方法的顺序一一解析双亲委派机制。

protected final Class<?> findLoadedClass(String name) {
    if (!checkName(name))
        return null;
    return findLoadedClass0(name);
}

// 从实例缓存查找是以native方法实现
private native final Class<?> findLoadedClass0(String name);

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);

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,我们在日常部署项目的时候经常遇到,遇到时要区分他们并且可以快速的定位,避免异常直接中断系统的发生。

上一篇 下一篇

猜你喜欢

热点阅读