Java基础02-类的加载过程02-解析
注:基于《Java高并发编程详解-汪文君》、《深入理解JVM高级特性与最佳实践-周志明》,以学习为目的,加上自己的理解、验证,作为自己的笔记,做到不复制,不粘贴。
回顾解析
类加载连接阶段包括验证,准备,解析。验证,准备阶段之后,就可以进入到解析阶段,所谓解析,就是在常量池中寻找类,接口,字段和方法的符号引用,并且将这些符号引用装换成直接引用的过程。
回顾符号引用
符号引用包括了下面三类常量
1.类和接口的全限定名
2.字段的名称和描述符
3.方法的名称和描述符
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候能够没有歧义的定位到目标即可。
符号引用于虚拟机实现的内存布局没有关系,引用的目标并不一定已经加载到内存中。
各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须是一致的,这是因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
回顾直接引用
直接引用可以是直接指向目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。
直接引用是和虚拟机实现的内存布局相关的。同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。但是如果有了直接引用,那么引用的目标必定已经在内存中存在。
javap命令
javap是jdk自带的一个命令,通过它可以查看一个java类反汇编、常量池、变量表、指令代码行表等等信息。
示例:
首先看这样的一段代码:
1 package resolve; 2 3 public class Resolve { 4 5 static { 6 System.out.println("I will be initialized."); 7 } 8 9 private static int i; 10 private double d; 11 12 public static void print() { 13 14 } 15 16 private boolean trueOrFalse() { 17 return false; 18 } 19 20 }
1 package resolve; 2 3 public class ClassResolve { 4 5 public static void main(String[] args) { 6 Resolve resolve = new Resolve(); 7 System.out.println(resolve); 8 } 9 }
使用方式
接着我们就以上面的代码为例,使用javap命令,结果如下。
1 D:\JavaDev\workspace\JavaProject\blogTest\target\classes\resolve>javap -verbose ClassResolve.class 2 Classfile /D:/JavaDev/workspace/JavaProject/blogTest/target/classes/resolve/ClassResolve.class 3 Last modified 2019-6-29; size 673 bytes 4 MD5 checksum ca3094c7a548769953d80dc7970ec911 5 Compiled from "ClassResolve.java" 6 public class resolve.ClassResolve 7 minor version: 0 8 major version: 49 9 flags: ACC_PUBLIC, ACC_SUPER 10 Constant pool: 11 #1 = Methodref #8.#25 // java/lang/Object."<init>":()V 12 #2 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream; 13 #3 = Fieldref #7.#28 // resolve/ClassResolve.resolve:Lresolve/Resolve; 14 #4 = Methodref #29.#30 // java/io/PrintStream.println:(Ljava/lang/Object;)V 15 #5 = Class #31 // resolve/Resolve 16 #6 = Methodref #5.#25 // resolve/Resolve."<init>":()V 17 #7 = Class #32 // resolve/ClassResolve 18 #8 = Class #33 // java/lang/Object 19 #9 = Utf8 resolve 20 #10 = Utf8 Lresolve/Resolve; 21 #11 = Utf8 <init> 22 #12 = Utf8 ()V 23 #13 = Utf8 Code 24 #14 = Utf8 LineNumberTable 25 #15 = Utf8 LocalVariableTable 26 #16 = Utf8 this 27 #17 = Utf8 Lresolve/ClassResolve; 28 #18 = Utf8 main 29 #19 = Utf8 ([Ljava/lang/String;)V 30 #20 = Utf8 args 31 #21 = Utf8 [Ljava/lang/String; 32 #22 = Utf8 <clinit> 33 #23 = Utf8 SourceFile 34 #24 = Utf8 ClassResolve.java 35 #25 = NameAndType #11:#12 // "<init>":()V 36 #26 = Class #34 // java/lang/System 37 #27 = NameAndType #35:#36 // out:Ljava/io/PrintStream; 38 #28 = NameAndType #9:#10 // resolve:Lresolve/Resolve; 39 #29 = Class #37 // java/io/PrintStream 40 #30 = NameAndType #38:#39 // println:(Ljava/lang/Object;)V 41 #31 = Utf8 resolve/Resolve 42 #32 = Utf8 resolve/ClassResolve 43 #33 = Utf8 java/lang/Object 44 #34 = Utf8 java/lang/System 45 #35 = Utf8 out 46 #36 = Utf8 Ljava/io/PrintStream; 47 #37 = Utf8 java/io/PrintStream 48 #38 = Utf8 println 49 #39 = Utf8 (Ljava/lang/Object;)V 50 { 51 static resolve.Resolve resolve; 52 descriptor: Lresolve/Resolve; 53 flags: ACC_STATIC 54 55 public resolve.ClassResolve(); 56 descriptor: ()V 57 flags: ACC_PUBLIC 58 Code: 59 stack=1, locals=1, args_size=1 60 0: aload_0 61 1: invokespecial #1 // Method java/lang/Object."<init>":()V 62 4: return 63 LineNumberTable: 64 line 3: 0 65 LocalVariableTable: 66 Start Length Slot Name Signature 67 0 5 0 this Lresolve/ClassResolve; 68 69 public static void main(java.lang.String[]); 70 descriptor: ([Ljava/lang/String;)V 71 flags: ACC_PUBLIC, ACC_STATIC 72 Code: 73 stack=2, locals=1, args_size=1 74 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 75 3: getstatic #3 // Field resolve:Lresolve/Resolve; 76 6: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 77 9: return 78 LineNumberTable: 79 line 8: 0 80 line 9: 9 81 LocalVariableTable: 82 Start Length Slot Name Signature 83 0 10 0 args [Ljava/lang/String; 84 85 static {}; 86 descriptor: ()V 87 flags: ACC_STATIC 88 Code: 89 stack=2, locals=0, args_size=0 90 0: new #5 // class resolve/Resolve 91 3: dup 92 4: invokespecial #6 // Method resolve/Resolve."<init>":()V 93 7: putstatic #3 // Field resolve:Lresolve/Resolve; 94 10: return 95 LineNumberTable: 96 line 5: 0 97 } 98 SourceFile: "ClassResolve.java"
说明:
从第10行可以看到在常量池中所有常量,例如:#6表示第6个常量。另外,所有utf8都是表示符号引用。再看看下面这个含义:
#27 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
第27个常量的名称和类型,分别是第35个常量和第36常量所表示的内容。即:
45 #35 = Utf8 out
46 #36 = Utf8 Ljava/io/PrintStream;
从74行可以看到,在常量池中通过getstatic这个指令获取PrintStream,同样getStatic也适用于获取Resolve,然后通过invokevirtual指令将resolve传递给PrintStream的println方法,在字节码的执行过程中,getStatic被执行之前,就需要进行解析。
虚拟机规范定义了,anewarray,checkcast,getField,getStatic,instanceof,invokeinterface,invokespecial,invokestactic,invokevirtual,multianwarray,new,putfield,putstatic这13个操作符号引用的字节码指令之前,就必须对所有的符号提前进行解析。
注:在对类接口,字段,类方法和接口方法的解析中,分别对应于常量中的CONSTANT_Class_info、 CONSTANT_Field_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodred_info这四种类型的常量。
那么最后就来说一说每一种类型的解析。
类接口解析
1.假如上面的代码中的Resolve类不是一个数组类型,则在加载的过程中,需要先完成对Resolve类的加载,同样需要经历所有的类加载阶段。
2.如果Resolve是一个数组类型,则JVM就不需要完成对Resole的加载,只需要在JVM中生成一个能够代表该类型的数组对象,并且在堆内存中开辟一片连续的地址空间。
3.在类接口的解析完成之后,还需要进行符号引用的验证。
字段的解析
顾名思义,就是解析所访问类或者接口中的字段,在解析类或者变量的时候,如果该字段不存在,或者出现错误,就会抛出异常,不在进行下面的解析。
1.如果Resolve类本身就包含某一个字段,就直接返回这个字段的引用,当然,也要对该字段所属的类提前进行类加载。
2.若果Resolve类中不存在该字段,则会根据继承关系自下而上,查找父类或者接口的字段,如果找到,即可返回。同样,需要提前对找到的字段进行类的加载过程。
3.如果Resolve类中没有字段,一直找到了最上层的java.lang.Object还是没有找到,则表示查找失败,也就不再进行任何解析,会抛出NoSuchFieldError异常。
注:
这个规则解释了为什么子类重写了父类的字段之后能够生效的原因。因为找到子类的字段就直接初始化并且返回了。
未完。