JVM类加载的符号解析
典型的一个类中,主要是static字段,类字段,static方法,类方法这四种存在。
至于static字段和类字段的初始化赋值语句,看似有点特别,其实在编译后归入<clinit>
方法和<init>
方法。
对于一个方法,形式参数实际上在调用方法之前会被压栈,进入操作数栈,这是栈帧的一部分。
方法的局部变量,如果是对象,对象本身的数据存储在堆里,栈帧的局部变量表中保存的是对象的reference(引用)。在解析之前是符号引用,解析之后变成直接引用(就是一个方法或者对象的首地址)。
方法符号引用
在class文件中,对于方法调用时是符号引用,并没有解析为方法地址。静态方法和对象方法的符号规则并没有区别,他们的区别在于调用的操作指令,一个是invokestatic,另一个是invokevirtual。后者在调用指令之前会把this对象压入操作数栈。在解析类方法或者接口方法符号的时候,符号中包含类名,但是为了实现多态应该不是根据这个类名,而是查找this所指对象的Class对象定位到的符号表,根据符号所包含类名找到对应方法表,找到特定方法签名,如果找不到则递归寻找父类的方法表,还有更详细的安全检查,接口方法也是根据this去查找,找不到时也会做更多检查。
如果是invokestatic的符号,那么只会根据符号中的类名知道的Class查找方法表。
类或者接口的符号引用
很奇怪对于代码
#62 = Class #63 // java/util/Set
#63 = Utf8 java/util/Set
……
29: ldc #62 // class java/util/Set
31: astore 5
33: aload 5
35: invokevirtual #33 // Method java/lang/Class.getName:()Ljava/lang/String;
offset=29的指令ldc只是把#62所代表的字符串常量压栈(操作数栈,后文不加说明皆是此意)
offset=31的指令astore只是把栈顶弹出保存到index=5的局部变量表
offset=33就是在载入变量压栈
offset=35的指令是调用虚方法了,此时栈顶代表的含义必须是对象方法所需的this参数,但是此时栈顶貌似只是字符串常量啊?
问题就在这里,从字节码看起来局部变量保存的也是字符串常量,也不符合源码中变量类型是Class对象引用。实际上这里JVM悄悄做了符号解析,JLS没有规定JVM必须在加载字节码时对常量池中符号进行解析或者执行到相关字节码时进行解析。但是真正执行到时,必须是符号已经被解析。
题外话,这样也说明Class cls = String.class这样的语句,cls变量其实是解析出来的地址。
而Class clssss = obj.getClass()是调用Java方法,传递真实引用到局部变量。
说到常量池,JLS中说明JVM运行时的常量池是不同的类型各自一个,但是不同class文件中字符串字面量到运行时只有一份,这是由于JVM做了preemptive optimization。使用的就是String.intern
方法,这个是native方法说明也是JVM提供的feature。
主要是因为多个class中的常量池会有相同的字符串,因此,设计为不可变,合并到JVM统一的常量池有利于节省空间(实验验证,至少不同类中的字符串字面量是合并的,不确定常量池中的符号是否合并去重,不过既然可以做到使得不同class中的字符串字面量合并,那么也肯定可以使得常量池中符号所指的常量的合并)。
https://stackoverflow.com/questions/40640566/jvm-architecture-runtime-constant-pool-in-method-area-is-per-class
This is a preemptive optimization that the JLS superimposes.
From JLS 7, §3.10.5 (formatting mine)
Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.28) - are "interned" so as to share unique instances, using the method String.intern.
However, note that this is only true of String literals and constant expressions. Dynamically constructed strings (e.g. x + y for Strings x and y) are not automatically interned to share the same unique instances. As a result, you will still have to use .equals in general unless you can guarantee that your operands are constant expressions.
字段符号解析
9: sipush 888
12: putfield #17 // Field df:I
常量池索引17代表的是字段符号,看起来putfield指令直接把栈顶的数字888保存到了#17的常量池,其实当然不可能,putfield的真实含义就是保存到一个field,字段的符号引用在执行之前已经被解析为字段地址。如果是putstatic,那么JVM可以根据类的字段变量表解析出字段符号,如果是对象字段,也可以。
本文希望是,理解JVM解析符号的概念,为什么要解析符号?有哪些类型的符号需要解析?JVM解析符号的大致过程是什么?