JVM浅析之一:类的加载

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容