深入JVM字节码探索assert关键字

引言

如果熟悉 C 语言,那么也许会使用过assert.h中的assert函数,在 Java 中的assert关键字也能够提供运行时断言这一功能,不过不同之处在于,Java 的断言可以在运行中决定是否开启,因此不必重新编译字节码。

本文主要对assert这个关键字在 JVM 字节码层面的实现原理进行分析,讨论其运行时配置断言启用与禁用的方式。

目录

  1. assert关键字基础
  2. assert实现原理分析
  3. assert运行时配置启用与禁用

1. assert关键字基础

class Main {
    public static void main(String[] args) {
        assert null instanceof Object : "Hello, world!";
    }
}

用法很简单,只需要一个参数或者两个参数,在运行时检查第一个参数的值是否为true

这段程序直接运行不会输出任何内容,因为默认禁用断言,启用断言可以使用-ea选项。

$ javac Main.java
$ java -ea Main
Exception in thread "main" java.lang.AssertionError: Hello, world!
        at Main.main(Main.java:3)

2. assert实现原理分析

首先看一看上面这个类的字节码:

class Main {
  static final boolean $assertionsDisabled;

  Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field $assertionsDisabled:Z
       3: ifne          23
       6: aconst_null
       7: instanceof    #3                  // class java/lang/Object
      10: ifne          23
      13: new           #4                  // class java/lang/AssertionError
      16: dup
      17: ldc           #5                  // String Hello, world!
      19: invokespecial #6                  // Method java/lang/AssertionError."<init>":(Ljava/lang/Object;)V
      22: athrow
      23: return

  static {};
    Code:
       0: ldc           #7                  // class Main
       2: invokevirtual #8                  // Method java/lang/Class.desiredAssertionStatus:()Z
       5: ifne          12
       8: iconst_1
       9: goto          13
      12: iconst_0
      13: putstatic     #2                  // Field $assertionsDisabled:Z
      16: return
}

人工反编译:

class Main {
    static final boolean $assertionsDisabled;
    public static void main(String[] args) {
        if (!$assertionsDisabled) {
            if (!(null instanceof Object)) {
                throw new AssertionError("Hello, world!");
            }
        }
    }
    static {
        $assertionsDisabled = !Main.class.desiredAssertionStatus() ? true : false;
    }
}

在每个使用了assert语句的类中,都被添加了一个包私有布尔常量$assertionsDisabled,在每一个assert语句处都要检查这个值是否为false,若为false则判断assert关键字的第一个表达式的值是否为false,若为false则以第二个表达式的值作为构造函数的参数抛出一个AssertionError,若这个assert语句仅有一个表达式则以无参构造函数实例化一个AssertionError并抛出。

AssertionError这个异常的构造函数有许多种重载,对于除了()V(Ljava/lang/String;Ljava/lang/Throwable;)V(Ljava/lang/String;)V之外的 7 种重载,它们会通过String类的静态方法valueOf将参数转变为字符串然后再调用构造函数的(Ljava/lang/String;)V重载。特别的是,对于(Ljava/lang/Object;)V这个重载,会判断参数是否为Throwable实例,如果是Throwable实例则会通过ThrowableinitCause实例方法为cause赋值。

3. assert运行时配置启用与禁用

Java语言规范中提出了一个类似这样的示例:

class Main {
    public static void main(String[] args) throws Exception {
        Class.forName("Main$Test");
    }
    static class Test extends Foobar {
        static void test() {
            assert null instanceof Object : "Hello, world!";
        }
    }
    static class Foobar {
        static {
            Test.test();
        }
    }
}

在上面这个示例中,无论是否使用-ea选项,这个断言都会被触发。

首先看一下堆栈:

java Main
Exception in thread "main" java.lang.AssertionError: Hello, world!
        at Main$Test.test(Main.java:7)
        at Main$Foobar.<clinit>(Main.java:12)
        at java.base/java.lang.Class.forName0(Native Method)
        at java.base/java.lang.Class.forName(Class.java:315)
        at Main.main(Main.java:3)

在 Main#main 中通过 java.lang.Class#forName 加载 Main$Test 类,由于 Main$Foobar 是 Main$Test 的超类,因此要加载 Main$Foobar 类并在 Main$Test#<clinit> 被调用之前先调用 Main$Foobar#<clinit>,但是在 Main$Foobar#<clinit> 却中调用了 Main$Test#test,但是此时尚未调用Main$Test#<clinit>,所以被 Main$Test#test 所访问到的 Main$Test#$assertionsDisabled 在此时尚未被初始化,其值为默认值false,无论是否使用-ea选项。

当一个使用了断言的类被加载时,在这个类的<clinit>中将调用java.lang.Class#desiredAssertionStatus并根据它的返回值为$assertionsDisabled初始化。

此处参考java.lang.Class#desiredAssertionStatus的源码:

/**
 * Returns the assertion status that would be assigned to this
 * class if it were to be initialized at the time this method is invoked.
 * If this class has had its assertion status set, the most recent
 * setting will be returned; otherwise, if any package default assertion
 * status pertains to this class, the most recent setting for the most
 * specific pertinent package default assertion status is returned;
 * otherwise, if this class is not a system class (i.e., it has a
 * class loader) its class loader's default assertion status is returned;
 * otherwise, the system class default assertion status is returned.
 * <p>
 * Few programmers will have any need for this method; it is provided
 * for the benefit of the JRE itself.  (It allows a class to determine at
 * the time that it is initialized whether assertions should be enabled.)
 * Note that this method is not guaranteed to return the actual
 * assertion status that was (or will be) associated with the specified
 * class when it was (or will be) initialized.
 *
 * @return the desired assertion status of the specified class.
 * @see    java.lang.ClassLoader#setClassAssertionStatus
 * @see    java.lang.ClassLoader#setPackageAssertionStatus
 * @see    java.lang.ClassLoader#setDefaultAssertionStatus
 * @since  1.4
 */
public boolean desiredAssertionStatus() {
    ClassLoader loader = getClassLoader0();
    // If the loader is null this is a system class, so ask the VM
    if (loader == null)
        return desiredAssertionStatus0(this);

    // If the classloader has been initialized with the assertion
    // directives, ask it. Otherwise, ask the VM.
    synchronized(loader.assertionLock) {
        if (loader.classAssertionStatus != null) {
            return loader.desiredAssertionStatus(getName());
        }
    }
    return desiredAssertionStatus0(this);
}

// Retrieves the desired assertion status of this class from the VM
private static native boolean desiredAssertionStatus0(Class<?> clazz);

首先会获取这个类的类加载器,假如这个类加载器为nullBootstrap Class Loader,则返回desiredAssertionStatus0的结果,这个结果对于上面的 Main$Test 来说,取决于是否使用了-ea参数,但是由于 Main$Test 的类加载器并不是null,所以desiredAssertionStatus不会在这里直接返回。

类加载器的classAssertionStatus是一个Map<String, Boolean>,如果不在代码中通过java.lang.ClassLoadersetClassAssertionStatussetPackageAssertionStatussetDefaultAssertionStatus专门设置特定的断言开关,也没有调用过java.lang.ClassLoader#clearAssertionStatus,则这个Map将始终为null,因此 Main$Test 的desiredAssertionStatus所返回的结果仍旧取决于是否使用了-ea选项。

最后参考一下java.lang.ClassLoader#desiredAssertionStatus的源码:

/**
 * Returns the assertion status that would be assigned to the specified
 * class if it were to be initialized at the time this method is invoked.
 * If the named class has had its assertion status set, the most recent
 * setting will be returned; otherwise, if any package default assertion
 * status pertains to this class, the most recent setting for the most
 * specific pertinent package default assertion status is returned;
 * otherwise, this class loader's default assertion status is returned.
 * </p>
 *
 * @param  className
 *         The fully qualified class name of the class whose desired
 *         assertion status is being queried.
 *
 * @return  The desired assertion status of the specified class.
 *
 * @see  #setClassAssertionStatus(String, boolean)
 * @see  #setPackageAssertionStatus(String, boolean)
 * @see  #setDefaultAssertionStatus(boolean)
 *
 * @since  1.4
 */
boolean desiredAssertionStatus(String className) {
    synchronized (assertionLock) {
        // assert classAssertionStatus   != null;
        // assert packageAssertionStatus != null;

        // Check for a class entry
        Boolean result = classAssertionStatus.get(className);
        if (result != null)
            return result.booleanValue();

        // Check for most specific package entry
        int dotIndex = className.lastIndexOf('.');
        if (dotIndex < 0) { // default package
            result = packageAssertionStatus.get(null);
            if (result != null)
                return result.booleanValue();
        }
        while(dotIndex > 0) {
            className = className.substring(0, dotIndex);
            result = packageAssertionStatus.get(className);
            if (result != null)
                return result.booleanValue();
            dotIndex = className.lastIndexOf('.', dotIndex-1);
        }

        // Return the classloader default
        return defaultAssertionStatus;
    }
}

这个方法将通过在classAssertionStatuspackageAssertionStatus这两个Map中查找对应的类名以及各级包名的断言开关,没有找到相关的设置则直接返回defaultAssertionStatus,我们可以用java.lang.ClassLoadersetClassAssertionStatussetPackageAssertionStatussetDefaultAssertionStatus设置某个类、某个包或者某个类加载器的断言启用与禁用,这种设定生效的优先级将高于-ea选项。

参考文献

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

推荐阅读更多精彩内容