今天一个朋友问了我一个问题,我研究了一哈觉得挺有意思想分享一下(花里胡哨但没什么卵用的冷知识+1)


问题

他在阅读《深入理解Java虚拟机》时发现文中这么描述:1.png

但是他在做实验的时候发现,只用final不用static修饰的变量依然可以有ConstantValue来赋值,他觉得书中的描述前后相悖.


实验

我做了一个实验,首先定义了一个static final int S_F_NUM=999,一个final int F_NUM=777

public class finalTest {
    static final int S_F_NUM = 999;
    static int S_NUM = 888;
    final int F_NUM = 777;
    int x = 555;
}

使用javac编译器编译后使用Javap查看,发现

2.png

确实S_F_NUM和F_NUM同时都带有ContstantValue属性,难道书中说的真的是不准确的吗?但是当我继续往下翻看指令时(下图)我发现F_NUM的777的值是在类实例化发生的初始化时才sipush进去的,而S_F_NUM的999则并不需要。

3.png

使用bytecode查看class(下图),发现果然777的赋值是走了〈init〉方法

4.png

这么看来,难道在javac对虚拟机规范的实现中,final修饰的变量在编译时虽然也会带有ConstantValue属性,但是此时的赋值却依然走了init来进行初始化,ConstantValue这时其实是没有作用的.只有static final修饰的变量才会由ConstantValue属性来进行初始化。


验证

为了验证上文现象得出的结论不如让我们去康康源码.

首先Javac的编译流程大致是这样的:源代码-(经过词法分析)->Token流-(经过语法分析)->抽象语法树-(经过语义分析)->标注语法树-(通过Gen代码生成)->Class字节码.
前面的过程忽略不计,在生成抽象语法树之后,语义分析需要组织符号表(需要自上而下遍历抽象语法树,将遇到的符号定义填充到符号表中),其中对变量进行处理的是MemberEnter类中的visitVarDef()方法,我们在visitVarDef()方法中会发现

VarSymbol v = new VarSymbol(0, tree.name, tree.vartype.type, enclScope.owner);
v.flags_field = chk.checkFlags(tree.pos(), tree.mods.flags, v, tree);
tree.sym = v;
if (tree.init != null) {
    v.flags_field |= HASINIT;
        if ((v.flags_field & FINAL) != 0 &&
                needsLazyConstValue(tree.init)) {
                Env<AttrContext> initEnv = getInitEnv(tree, env);
                initEnv.info.enclVar = v;
                v.setLazyConstValue(initEnv(tree, initEnv), attr, tree);
        }
}

只有抽象语法树节点带有Final时,此时变量对应的VarSymbol对象才会执行setLazyConstValue()方法

public void setLazyConstValue(final Env<AttrContext> env,
                                      final Attr attr,
                                      final JCVariableDecl variable)
        {
            setData(new Callable<Object>() {
                public Object call() {
                    return attr.attribLazyConstantValue(env, variable, type);
                }
            });
        }

setData()才会去设置ConstantValue属性,在后面getConstValue()的时候才能够取到值,而其他变量则走另外一套逻辑

try {
            v.getConstValue(); // ensure compile-time constant initializer is evaluated
            deferredLintHandler.flush(tree.pos());
            chk.checkDeprecatedAnnotation(tree.pos(), v);

            if (tree.init != null) {
                if ((v.flags_field & FINAL) == 0 ||
                    !memberEnter.needsLazyConstValue(tree.init)) {
                    // Not a compile-time constant
                    // Attribute initializer in a new environment
                    // with the declared variable as owner.
                    // Check that initializer conforms to variable's declared type.
                    Env<AttrContext> initEnv = memberEnter.initEnv(tree, env);
                    initEnv.info.lint = lint;
                    // In order to catch self-references, we set the variable's
                    // declaration position to maximal possible value, effectively
                    // marking the variable as undefined.
                    initEnv.info.enclVar = v;
                    attribExpr(tree.init, initEnv, v.type);
                }
            }
            result = tree.type = v.type;
        }
        finally {
            chk.setLint(prevLint);
        }

这就是Javac编译阶段阶段对Final做的处理(javac源码怕不是有10w行,翻源码翻的俺头疼

那为什么最后又只有static final生效了呢?首先,在Hotspot实现中,ClassLoader::load_class()负责定位字节码文件的位置,读取该文件的工作由类文件解析器ClassFileParser完成,我在ClassFileParser中找到了处理field的逻辑ClassFileParser::parse_field_attributes:

Symbol* attribute_name = _cp->symbol_at(attribute_name_index);
    if (is_static && attribute_name == vmSymbols::tag_constant_value()) {
      // ignore if non-static
      if (constantvalue_index != 0) {
        classfile_parse_error("Duplicate ConstantValue attribute in class file %s", CHECK);
      }
      check_property(
        attribute_length == 2,
        "Invalid ConstantValue field attribute length %u in class file %s",
        attribute_length, CHECK);
      constantvalue_index = cfs->get_u2(CHECK);
      if (_need_verify) {
        verify_constantvalue(constantvalue_index, signature_index, CHECK);
      }
    }

从代码中我们不难看出虽然在处理symbol表的时候两者都加了ConstantValue,但是在解析的时候JVM对其进行了限制所以只有static final的ConstantValue才是生效的。

周志明大大在《深入》中的描述没错,但是描述的不全容易让人产生疑惑。果然纸上得来终觉浅,绝知此事要躬行

Last modification:January 12th, 2021 at 07:00 am
大家一起分享知识,分享快乐