JVM面纱初试探(二)
首先分享下官网网址,所有的开发说明官网才是权威,网上传播的多少会有些曲解https://www.oracle.com/index.html和https://docs.oracle.com/javase/8/,讲解JVM前我们先准备一份JAVA文件
一个java文件交给JVM的过程分为编译和加载两个过程,下面先讲解编译过程编译过程
一、源码文件到类文件
class Person{ private String name="LuJiaXing"; private int age; private final double salary=100; private static String address; private final static String hobby="abc"; public void say(){ System.out.println("jvm....."); } public static int calc(int op1,int op2){ op1=3; int result=op1+op2; return result; } public static void main(String[] args){ System.out.println(calc(1,2)); } }
通过命令我们 生成class文件
打开我们生成的class文件会发现生成一个16进制的表示方式,当然你也可以理解为是一个二进制的表式方式
当然上面的16进制表示方式我们现在还是看不懂,但我们可以进入官网查找JDK的帮助文档进行查看https://docs.oracle.com/javase/specs/jvms/se8/html/index.html,在开发文档第4章节中详细解析了class文件格
整个 .class 文件本质上就是一张表,由上表所示的数据项构成,由于官网上直译的不太准,所以我在网上找了一份中文的表格,表格如下
上表其实可以划分7个部分,.class字节码文件包括:
1.魔数与class文件版本
2.常量池
3.访问标志
4.类索引、父类索引、接口索引
5.字段表集合
6.方法表集合
7.属性表集合
下面将详解.class的7个部分
a.魔数(Magic Number):.class 文件的第 1 - 4 个字节,它唯一的作用就是确定这个文件是否是一个能被虚拟机接受的 class 文件,其固定值是:0xCAFEBABE(咖啡宝贝)。如果一个 class 文件的魔术不是 0xCAFEBABE,那么虚拟机将拒绝运行这个文件
b.次版本号(minor version):.class 文件的第 5 - 6 个字节,即编译生成该 .class 文件的 JDK 次版本号
c.主版本号(major version):.class 文件的第 7 - 8个字节,即编译生成该 .class 文件的 JDK 主版本号,根据官网的截图我们可以知道JDK1.1的十进制是45,由此我们就可以根据科学计算器算出自己的JDK版本号
d.常量池:紧接着版本号之后的是常量池的入口,常量池可以理解为 class 文件之中的资源仓库,它是占用 class 文件空间最大的数据项之一。常量池是一个集合,它由两部分组成:常量池计数器和常量池
1.常量池计数器(constant_pool_count) 是一个 u2 的无符号数
2.常量池(constant_pool):紧跟在常量池计数器后面的内容就是该 .class 文件的常量池内容了,常量池中存放的数据一般分为两种类型:字面量和符号引用;
字面量:是指文本字符串、声明为 final 的常量值等
符号引用: 是一个更偏向于编译原理方面的概念,主要包括三类常量:1). 类和接口的全限定名,2).字段的名称和描述符,3). 方法的名称和描述符
在常量池中的常量共有 14 种类型,每个常量都是一个表,每一个表都有各自的组成结构。这 14 个常量有一个公共的特点,就是每个常量开始是一个用 u1 类型的无符号数表示的标志位(tag,取值见下表),表示此常量属于哪种常量类型
首先是常量计数器(constant_pool_coun),数值是:0x 0038,表示此class 文件中共有 55 个常量;cp_info_constant_pool[1]:偏移地址是 0x000A,内容是:0x0a00 0d00 23。0x0A 标志位表示是一个 CONSTANT_Methodref_info 常量,0x00 0d 是一个索引,指向常量池中第 13 个常量所表示的信息;0x00 23 是一个索引,指向常量池第 35 个常量所表示的信息。CONSTANT_Methodref_info 常量的结构如下所示:
e.访问标志:常量池之后是 u2 类型的访问标志位(access_flags),这个访问标志位用于标识类或者接口层次的访问信息,包括:这个 Class 是类还是接口、是否定义为public类型、是否定义为abstract类型,如果是类的话,是否被 final 关键字修饰。具体的标志位以及标志的含义见下表
f.类索引、父类索引、接口索引-------类索引:u2 数据类型,用于确定这个类的全限定名。父类索引:u2 数据类型,用于确定这个类的父类的全限定名。接口索引:u2 数据类型的集合,用于描述类实现了哪些接口,这些被实现的接口将按照 implements 语句 后的顺序从左至右排列在接口索引集合中。接口索引集合分为两部分,第一部分表示接口计数器(interfaces_count),是一个 u2 类型的数据,第二部分是接口索引表表示接口信息,紧跟在接口计数器之后。若一个类实现的接口为 0,则接口计数器的值为 0,接口索引表不占用任何字节。
后面的字段表集合什么的官网上都有详细解说,可以根据官网的说明一步一步解析就可以看懂这16进制的class文件
二、反编译验证
经过上面讲解相信如果相看字节码文件大家也可以看的懂了,但是上面字节码那么多,如果一点一点验证查找的看会恶心死,那有没有简单的方式进行查看呢,还真有,那就是反编译
编译指令:javap -v -p Person.class
编译后的文件;下面颜色标记的部分分别是magic、版本号、常量池、字段表集合、方法表集合
G:\jvm>javap -v -p Person.class Classfile /G:/jvm/Person.class Last modified 2022-1-28; size 785 bytes MD5 checksum c47cf28574c5b2ebe838241fa8b3893e Compiled from "Person.java" class Person minor version: 0 major version: 52 flags: ACC_SUPER Constant pool: #1 = Methodref #13.#35 // java/lang/Object."<init>":()V #2 = String #36 // LuJiaXing #3 = Fieldref #12.#37 // Person.name:Ljava/lang/String; #4 = Double 100.0d #6 = Fieldref #12.#38 // Person.salary:D #7 = Fieldref #39.#40 // java/lang/System.out:Ljava/io/PrintStream; #8 = String #41 // jvm..... #9 = Methodref #42.#43 // java/io/PrintStream.println:(Ljava/lang/String;)V #10 = Methodref #12.#44 // Person.calc:(II)I #11 = Methodref #42.#45 // java/io/PrintStream.println:(I)V #12 = Class #46 // Person #13 = Class #47 // java/lang/Object #14 = Utf8 name #15 = Utf8 Ljava/lang/String; #16 = Utf8 age #17 = Utf8 I #18 = Utf8 salary #19 = Utf8 D #20 = Utf8 ConstantValue #21 = Utf8 address #22 = Utf8 hobby #23 = String #48 // abc #24 = Utf8 <init> #25 = Utf8 ()V #26 = Utf8 Code #27 = Utf8 LineNumberTable #28 = Utf8 say #29 = Utf8 calc #30 = Utf8 (II)I #31 = Utf8 main #32 = Utf8 ([Ljava/lang/String;)V #33 = Utf8 SourceFile #34 = Utf8 Person.java #35 = NameAndType #24:#25 // "<init>":()V #36 = Utf8 LuJiaXing #37 = NameAndType #14:#15 // name:Ljava/lang/String; #38 = NameAndType #18:#19 // salary:D #39 = Class #49 // java/lang/System #40 = NameAndType #50:#51 // out:Ljava/io/PrintStream; #41 = Utf8 jvm..... #42 = Class #52 // java/io/PrintStream #43 = NameAndType #53:#54 // println:(Ljava/lang/String;)V #44 = NameAndType #29:#30 // calc:(II)I #45 = NameAndType #53:#55 // println:(I)V #46 = Utf8 Person #47 = Utf8 java/lang/Object #48 = Utf8 abc #49 = Utf8 java/lang/System #50 = Utf8 out #51 = Utf8 Ljava/io/PrintStream; #52 = Utf8 java/io/PrintStream #53 = Utf8 println #54 = Utf8 (Ljava/lang/String;)V #55 = Utf8 (I)V { private java.lang.String name; descriptor: Ljava/lang/String; flags: ACC_PRIVATE private int age; descriptor: I flags: ACC_PRIVATE private final double salary; descriptor: D flags: ACC_PRIVATE, ACC_FINAL ConstantValue: double 100.0d private static java.lang.String address; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC private static final java.lang.String hobby; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL ConstantValue: String abc Person(); descriptor: ()V flags: Code: stack=3, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String LuJiaXing 7: putfield #3 // Field name:Ljava/lang/String; 10: aload_0 11: ldc2_w #4 // double 100.0d 14: putfield #6 // Field salary:D 17: return LineNumberTable: line 1: 0 line 2: 4 line 4: 10 public void say(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #8 // String jvm..... 5: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 9: 0 line 10: 8 public static int calc(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=2 0: iconst_3 1: istore_0 2: iload_0 3: iload_1 4: iadd 5: istore_2 6: iload_2 7: ireturn LineNumberTable: line 12: 0 line 13: 2 line 14: 6 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=1, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: iconst_1 4: iconst_2 5: invokestatic #10 // Method calc:(II)I 8: invokevirtual #11 // Method java/io/PrintStream.println:(I)V 11: return LineNumberTable: line 17: 0 line 18: 11 } SourceFile: "Person.java"
三、类加载机制
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
首先,在代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制。下图是类加载机制的图解
(1)装载
a.先找到类文件所在位置
通过类装载器ClassLoader.find录找根类(通过不同的类装载器装载不同的东西)这里面就可以聊会类装载器ClassLoader
类装载器定义:在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。通过一个类的全限定名获取定义此类的二进制字节流
分类:
类加载器的双亲委派加载机制(重点):当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
破坏双亲委派加载机制:重写ClassLoader类的loadClass()方法
b.类文件的信息交给JVM
C.类文件所有的对象class交给jvm
(2)链接
a 验证:确保被加载的类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
b准备:为类的静态变量分配内存,并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
这里还需要注意如下几点: · 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。 · 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。 · 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。 · 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
假设上面的类变量value被定义为: public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。回忆上一篇博文中对象被动引用的第2个例子,便是这种情况。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中
c.解析:把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
(3)初始化
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
– 创建类的实例,也就是new的方式
– 访问某个类或接口的静态变量,或者对该静态变量赋值
– 调用类的静态方法
– 反射(如Class.forName(“com.shengsiyuan.Test”))
– 初始化某个类的子类,则其父类也会被初始化
– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
四. jvm的运行时数据区
A 方法区
方法区只有一个,线程共享的内存区域【线程不安全】,生命周期与虚拟机一样长
B 堆
线程共享的内存区域【线程不安全】,生命周期与虚拟机一样长
C 栈
生命周期与线程一样长,每个方法被当前线程调用时就代表一个栈帧