前面已经初步实现了线程私有的运行时数据区,主要包括操作数栈和局部变量表。接下来将继续丰富运行时数据区的内容——线程共享的运行时数据区,包括方法区和运行时常量池。本节的代码集中在 heap 包下。同时也会对一些概念做出解释。因为自己之前在这些概念上的错误认识,走了很多弯路。
方法区 之前我们已经可以找到 class 文件,并把其内容加载到内存中,对其解析成一个 ClassFile 的结构,但是 ClassFile 中的内容仍然无法直接在方法区使用,还需要进一步的转换。其实转换的内容不多,我们通过代码来直观的对已一下 ClassFile 和 Zclass 中的区别。(为避免和 JDK 中已有的类名冲突,这里将我们自定义的 class
,method
,field
等前面都加一个 z
以作区分。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Zclass { private int accessFlags; public String thisClassName; public String superClassName; public String[] interfaceNames; private RuntimeConstantPool runtimeConstantPool; Zfield[] fileds; Zmethod[] methods; ZclassLoader loader; Zclass superClass; Zclass[] interfaces; int instanceSlotCount; int staticSlotCount; Slots staticVars; }
1 2 3 4 5 6 7 8 9 10 11 12 public class ClassFile { private int minorVersion; private int majorVersion; public ConstantPool constantPool; private int accessFlags; private int thisClass; private int superClass; private int [] interfaces; private MemberInfo[] fields; private MemberInfo[] methods; private AttributeInfo[] attributes; }
对比由 ClassFile -> Zclass 的转换可以发现:eg:ClassFile 中 superClass 的值是一个 int 的索引值,其指向常量池的一个常量,该常量由通过一个索引指向常量池中的 utf8-constant,从而用来表示当前 class 的父类的名字(字符串)。而在 Zclass 中,可以看到有一个 superClass 的成员变量,其类型为 Zclass 类型,而不再是简单的字符串。再来看在类中定义的成员变量,在 ClassFile 中成员变量是保存在MemberInfo 中,这也是简单的字符描述(变量类型,变量名字),在 Zclass 中保存的是新定义的 Zfield 类型的变量,这些都是需要转换的地方。
所以转换的本质是什么: ** class 文件中全是字符串,而现在加载到内存中了,就不能简单的用一个字符串来描述了,而是一个指向内存的一个实体**
所以针对于 ClassFile 到 Zclass 的转换,具体要转换的有三部分:
类信息本身,这里定义为 Zclass
字段信息,这里定义为 Zfield,表示在类中定义的变量(静态+非静态),Zfield 中有一个成员变量 Zclass,表明当前字段属于哪个类
方法信息,这里定义为 Zmethod,表示在类中定义的方法(静态+非静态),Zmethod 中有一个成员变量 Zclass,表明当前方法属于哪个类
而转换为 Zclass 的类,就是放在方法区的,其实叫方法区是有干扰的,让人以为方法区只是存放类中的方法的,并不是这样的。方法区存放的是一个 class 文件的描述,包括该类的权限,该类实现了哪些接口,该类的父类是谁,该类有哪些字段,以及字段的权限,该类有哪些方法,以及方法的权限等。 在本 JVM 中,方法区的实现是使用了一个 HashMap,其中 key 的 class 的全限定名,value 为加载进来的 zclass 对象。
读完上面的描述,你可能还是一头雾水,暂时先放到一边,因为单将方法区无法将清楚,其还要配合下面的运行时常量,才能查看其全貌。
运行时常量池 常量池,这个概念之前就遇到过,那是在解析 class 文件时,class 文件中就有一个常量池的概念,现在又遇到了常量池,不过不是一回事。
这里再次解释一下 class 文件中的常量池的概念。我们将 java 代码通过 javac 编译为 class 文件,接下来再运行 java 程序的时候就完全不需要 java 文件了,只需要 class 文件即可,但是 class 文件是保存在本地磁盘上的文件,里面全是0101的字节码,或者说是字符串,那各个字符串之间如何产生联系呢?那就是通过常量池,并给常量池安排了序号,每个序号都对应一个常量(字符串),然后 class 文件规定了一个格式,每个 class 文件都要按照一定的次序排放,类名,访问权限,成员变量,成员方法等(具体可回顾分析class文件 )。如何描述类名,成员变量,成员方法?就是在 class 文件中对应的位置保存一个整数,用来指向 class 文件中另一块区域——常量池中的一个常量(字符串),用字符串来描述这是什么。 在强调一遍:class 文件是在本地磁盘上的文件,其内部是死的。
那么接下来将 class 文件读到内存中,其常量池也要进行相应的转换。如果是基本类型的常量那么原封不动,但如果表示的是一个类的引用,在 class 文件中使用的还是字符串,用来说明是哪个类,而在运行期间,就要真正的加载这个类(保存在方法区),然后指向加载的这个类,而不是之前简单的字符串了。
到这里,应该对两个常量池的作用有一个清楚的了解,自己之前就是把两个常量池当成一个,直接拿来用了!所以在描述常量池时,我会用 class 文件中的常量池
和 运行时常量池
来做区分。
所以,相对于 class 文件中的常量池,运行时常量池是在内存中的,是活的。由 class 文件中的常量池向运行时常量池的转换如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public RuntimeConstantPool (Zclass clazz, ConstantPool classFileConstantPool) { this .clazz = clazz; ConstantInfo[] classFileConstantInfos = classFileConstantPool.getInfos(); int len = classFileConstantInfos.length; this .infos = new RuntimeConstantInfo [len]; for (int i = 1 ; i < len; i++) { ConstantInfo classFileConstantInfo = classFileConstantInfos[i]; switch (classFileConstantInfo.getType()) { case ConstantInfo.CONSTANT_Integer: ConstantIntegerInfo intInfo = (ConstantIntegerInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <Integer>(intInfo.getVal(), ConstantInfo.CONSTANT_Integer); break ; case ConstantInfo.CONSTANT_Float: ConstantFloatInfo floatInfo = (ConstantFloatInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <Float>(floatInfo.getVal(), ConstantInfo.CONSTANT_Float); break ; case ConstantInfo.CONSTANT_Long: ConstantLongInfo longInfo = (ConstantLongInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <Long>(longInfo.getVal(), ConstantInfo.CONSTANT_Long); i++; break ; case ConstantInfo.CONSTANT_Double: ConstantDoubleInfo doubleInfo = (ConstantDoubleInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <Double>(doubleInfo.getVal(), ConstantInfo.CONSTANT_Double); i++; break ; case ConstantInfo.CONSTANT_String: ConstantStringInfo stringInfo = (ConstantStringInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <String>(stringInfo.getString(), ConstantInfo.CONSTANT_String); break ; case ConstantInfo.CONSTANT_Class: ConstantClassInfo classInfo = (ConstantClassInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <ClassRef>(new ClassRef (this , classInfo), ConstantInfo.CONSTANT_Class); break ; case ConstantInfo.CONSTANT_Fieldref: ConstantFieldRefInfo fieldRefInfo = (ConstantFieldRefInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <FieldRef>(new FieldRef (this , fieldRefInfo), ConstantInfo.CONSTANT_Fieldref); break ; case ConstantInfo.CONSTANT_Methodref: ConstantMethodRefInfo methodRefInfo = (ConstantMethodRefInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <MethodRef>(new MethodRef (this , methodRefInfo), ConstantInfo.CONSTANT_Methodref); break ; case ConstantInfo.CONSTANT_InterfaceMethodref: ConstantInterfaceMethodRefInfo interfaceMethodRefInfo = (ConstantInterfaceMethodRefInfo) classFileConstantInfo; this .infos[i] = new RuntimeConstantInfo <InterfaceMethodRef>(new InterfaceMethodRef (this , interfaceMethodRefInfo), ConstantInfo.CONSTANT_InterfaceMethodref); break ; default : break ; } } }
这里主要是对以下四种常量进行转换:
类符号引用
字段符号引用
方法符号引用
接口方法符号引用
这里再区分一个概念,拿字段符号引用来说,什么是字段符号引用,和上面讲到的方法区中要转换的字段又有什么关系? 以下面一个简单的例子说明:
1 2 3 4 5 6 class A { String str; void f () { System.out.println("xxx" ); } }
这个例子中 str 就是之前方法区要转换的字段。System.out 是字段符号引用,因为该 out 字段并非类 A 中定义的,而是在 System 类中定义的。
二者相同点:都是字段,那么是字段就要有字段名,类型,所在的类
不同的是: 方法区要转换的字段,是在当前类声明的成员变量,所以其所在的类就是当前类。 字段引用:FieldRef 类没有直接的一个字段指向其所在的类,而是有一个所在类的名字,那么在使用字段引用(FieldRef)时这就需要以下两步解决:
用类加载器根据类名将对应的类加载进来
在从上一步加载到的类中,查找其定义的对应的字段,从而获取到真正的字段
是在方法中的 code 属性中用到的。f 方法对应的字节码为:
1 2 3 0 getstatic #2 <java/lang/System.out> 3 invokevirtual #3 <java/io/PrintStream.println> 6 return
其中的 getstatic 指令,后跟的索引2,指向的就是运行时常量池中的一个字段引用。那么 getstatic 指令要做的是从运行时常量池获取到一个 FieldRef,然后根据其类名加载类——System,因为 out 是类 System 中定义的一个变量,所以从 System 的 filed[] 数组中根据字段名找到对应的字段 field, 最后将该 field 压入操作数栈。
在上面的 invokevirtual 指令中,其后跟的索引3指向的是运行时常量池中的一个方法引用(MethodRef),其方法名为println,该方法在 java/io/PrintStream 中定义,也就是 System.out 的类型。和字段引用解析类似,也是先加载其所在的类java/io/PrintStream到方法区,然后根据方法名找到对应的方法,最重要的是拿到方法中的 code 字节码,从而进行该方法的调用!
总结 这一节主要介绍了线程共享的运行时数据区,该数据区包含两个重要的概念:方法区和运行时常量池。同时还着重强调了两组概念:
class 文件中的常量池 和 运行时常量池
类中定义的字段、方法 和 字段引用,方法引用
本文作者:
Zachaxy
版权声明:
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。