导图社区 JVM
超全超细的JVM学习笔记,介绍了加载子系统、运行时数据区、类与对象、垃圾收集、直接内存、执行引擎这几个方面的内容。
编辑于2021-09-23 16:10:19JVM
字节码与类的加载
字节码
字节码介绍
引言
Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条。官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html熟悉虚拟机的指令对于动态字节码生成、反编译Class文件、Class文件修补都有着非常重要的价值。因此,阅读字节码作为了解Java虚拟机的基础技能,需要熟练掌握常见指令。
执行模型如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解do{ 自动计算PC寄存器的值加1; 根据PC寄存器的指示位置,从字节码流中取出操作码; if(字节码存在操作数)从字节码流中取出操作数; 执行操作码所定义的操作;}while(字节码长度>0);在做值相关操作时:一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,可能是对象的引用)被压入操作数栈。一个指令,也可以从操作数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系统调用等等操作。
Java 字节码由单字节(byte)的指令组成,理论上最多支持 256 个操作码(opcode)。实际上 Java 只使用了200左右的操作码, 还有一些操作码则保留给调试操作。
根据指令的性质,主要分为四个大类:1.栈操作指令,包括与局部变量交互的指令2.程序流程控制指令3.对象操作指令,包括方法调用指令4.算术运算以及类型转换指令
字节码的运行时结构:JVM 基于栈的计算模型每个线程都有一个独属于自己的线程栈(JVM Stack),用于存储栈帧(Stack Frame)。每一次方法调用,JVM 都会自动创建一个栈帧。每当为 Java 方法分配栈桢时,JVM 需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。
第1章:class文件结构
01-概述
字节码文件的跨平台性
Java的前端编译器
透过字节码指令看代码细节
02-虚拟机的基石:Class文件
03-Class文件结构
Class文件结构总览
Class文件字节码结构组织示意图
01-魔数:Class文件的标志
02-Class文件版本号
03-常量池:存放所有常量
一个Java类定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。
常量池中主要两类常量:字面量:如文本字符串,Java中声明为final的常量值等符号引用:如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等
常量池的总体结构:Java类所对应的常量池主要由常量池数量与常量池表这两部分组成:常量池数量紧跟在主版本号之后,占据两个字节常量池表则紧跟在常量池数量之后常量池表与一般的数组不同的是,常量池表中不同的元素的类型、结构和长度都是不同的;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标识位,占据一个字节。JVM在解析产量池时,会根据这个u1类型来获取元素的具体类型。
1-常量池计数器
2-常量池表
2.1 字面量和符号引用
2.2 常量类型和结构
04-访问标识
05-类索引、父类索引、接口索引集合
06-字段表集合
1-字段计数器
2-字段表
根据上面的例子,我们来实际分析一下,如下图:
07-方法表集合
1-方法计数器
2-方法表
LocalVariableTable
local_variable_table_length 局部变量个数
start_pc 其含义为这个局部变量的生命周期开始的字节码偏移量
local_variable_table 其含义为这个局部变量作用范围覆盖的长度。两者结合起来就是这个局部变量在字节码之中的作用域范围。
08-属性表集合
1-属性计数器
2-属性表
或者(查看官网)
小结
04-使用javap指令解析Class文件
1-解析字节码的作用
2-javac -g操作
3-javap的用法
注意:①-v相当于-c -l ②-v也不会输出私有的字段、方法等信息,所以如果想输出私有的信息,那需要在-v后面加上-p才行
4-使用举例
1、代码:public class JavapTest { private int num; boolean flag; protected char gender; public String info; public static final int COUNTS = 1; static { String url = "www.atguigu.com"; } { info = "java"; } public JavapTest() { } private JavapTest(boolean falg) { this.flag = flag; } private void methodPrivate() { } int getNum(int i) { return num + i; } protected char showGender() { return gender; } public void showInfo() { int i = 100; System.out.println(info + i); } } 2、字节码文件分析:Classfile /C:/Users/mingming/Desktop/JavapTest.class // 字节码文件所属的路径 Last modified 2021-2-24; size 1393 bytes // 最后修改时间,字节码文件的大小 MD5 checksum 2c764244fa3a95bfb346c9e416a7a3f6 // MD5散列值 Compiled from "JavapTest.java" // 源文件的名称public class io.renren.JavapTest minor version: 0 // 副版本 major version: 52 // 主版本 flags: ACC_PUBLIC, ACC_SUPER // 访问标识*************************** 常量池********************************Constant pool: #1 = Methodref #16.#48 // java/lang/Object."":()V #2 = String #49 // java #3 = Fieldref #15.#50 // io/renren/JavapTest.info:Ljava/lang/String; #4 = Fieldref #15.#51 // io/renren/JavapTest.flag:Z #5 = Fieldref #15.#52 // io/renren/JavapTest.num:I #6 = Fieldref #15.#53 // io/renren/JavapTest.gender:C #7 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream; #8 = Class #56 // java/lang/StringBuilder #9 = Methodref #8.#48 // java/lang/StringBuilder."":()V #10 = Methodref #8.#57 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #11 = Methodref #8.#58 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; #12 = Methodref #8.#59 // java/lang/StringBuilder.toString:()Ljava/lang/String; #13 = Methodref #60.#61 // java/io/PrintStream.println:(Ljava/lang/String;)V #14 = String #62 // www.atguigu.com #15 = Class #63 // io/renren/JavapTest #16 = Class #64 // java/lang/Object #17 = Utf8 num #18 = Utf8 I #19 = Utf8 flag #20 = Utf8 Z #21 = Utf8 gender #22 = Utf8 C #23 = Utf8 info #24 = Utf8 Ljava/lang/String; #25 = Utf8 COUNTS #26 = Utf8 ConstantValue #27 = Integer 1 #28 = Utf8 #29 = Utf8 ()V #30 = Utf8 Code #31 = Utf8 LineNumberTable #32 = Utf8 LocalVariableTable #33 = Utf8 this #34 = Utf8 Lio/renren/JavapTest; #35 = Utf8 (Z)V #36 = Utf8 falg #37 = Utf8 MethodParameters #38 = Utf8 methodPrivate #39 = Utf8 getNum #40 = Utf8 (I)I #41 = Utf8 i #42 = Utf8 showGender #43 = Utf8 ()C #44 = Utf8 showInfo #45 = Utf8 #46 = Utf8 SourceFile #47 = Utf8 JavapTest.java #48 = NameAndType #28:#29 // "":()V #49 = Utf8 java #50 = NameAndType #23:#24 // info:Ljava/lang/String; #51 = NameAndType #19:#20 // flag:Z #52 = NameAndType #17:#18 // num:I #53 = NameAndType #21:#22 // gender:C #54 = Class #65 // java/lang/System #55 = NameAndType #66:#67 // out:Ljava/io/PrintStream; #56 = Utf8 java/lang/StringBuilder #57 = NameAndType #68:#69 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #58 = NameAndType #68:#70 // append:(I)Ljava/lang/StringBuilder; #59 = NameAndType #71:#72 // toString:()Ljava/lang/String; #60 = Class #73 // java/io/PrintStream #61 = NameAndType #74:#75 // println:(Ljava/lang/String;)V #62 = Utf8 www.atguigu.com #63 = Utf8 io/renren/JavapTest #64 = Utf8 java/lang/Object #65 = Utf8 java/lang/System #66 = Utf8 out #67 = Utf8 Ljava/io/PrintStream; #68 = Utf8 append #69 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #70 = Utf8 (I)Ljava/lang/StringBuilder; #71 = Utf8 toString #72 = Utf8 ()Ljava/lang/String; #73 = Utf8 java/io/PrintStream #74 = Utf8 println #75 = Utf8 (Ljava/lang/String;)V******************************字段表集合的信息**************************************{ private int num; // 字段名 descriptor: I // 字段表集合的信息 flags: ACC_PRIVATE // 字段的访问标识 boolean flag; descriptor: Z flags: protected char gender; descriptor: C flags: ACC_PROTECTED public java.lang.String info; descriptor: Ljava/lang/String; flags: ACC_PUBLIC public static final int COUNTS; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 1 // 常量字段的属性:ConstantValue******************************方法表集合的信息************************************** public io.renren.JavapTest(); // 无参构造器方法信息 descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: ldc #2 // String java 7: putfield #3 // Field info:Ljava/lang/String; 10: return LineNumberTable: line 16: 0 line 14: 4 line 18: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lio/renren/JavapTest; private io.renren.JavapTest(boolean); // 单个参数构造器方法信息 descriptor: (Z)V flags: ACC_PRIVATE Code: stack=2, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: ldc #2 // String java 7: putfield #3 // Field info:Ljava/lang/String; 10: aload_0 11: aload_0 12: getfield #4 // Field flag:Z 15: putfield #4 // Field flag:Z 18: return LineNumberTable: line 19: 0 line 14: 4 line 20: 10 line 21: 18 LocalVariableTable: Start Length Slot Name Signature 0 19 0 this Lio/renren/JavapTest; 0 19 1 falg Z MethodParameters: Name Flags falg private void methodPrivate(); descriptor: ()V flags: ACC_PRIVATE Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 24: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lio/renren/JavapTest; int getNum(int); descriptor: (I)I flags: Code: stack=2, locals=2, args_size=2 0: aload_0 1: getfield #5 // Field num:I 4: iload_1 5: iadd 6: ireturn LineNumberTable: line 26: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lio/renren/JavapTest; 0 7 1 i I MethodParameters: Name Flags i protected char showGender(); descriptor: ()C flags: ACC_PROTECTED Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #6 // Field gender:C 4: ireturn LineNumberTable: line 29: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lio/renren/JavapTest; public void showInfo(); descriptor: ()V // 方法的描述符:方法的形参列表、返回值类型 flags: ACC_PUBLIC // 方法的访问标识 Code: // 方法的Code属性 stack=3, locals=2, args_size=1 // stack:操作数栈的最大深度 locals:局部变量表的长度 args_size:方法接受参数的个数// 偏移量 操作码 操作数 0: bipush 100 2: istore_1 3: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 6: new #8 // class java/lang/StringBuilder 9: dup 10: invokespecial #9 // Method java/lang/StringBuilder."":()V 13: aload_0 14: getfield #3 // Field info:Ljava/lang/String; 17: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: iload_1 21: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 24: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 30: return// 行号表:指明字节码指令的偏移量与java源代码中代码的行号的一一对应关系 LineNumberTable: line 32: 0 line 33: 3 line 34: 30 // 局部变量表:描述内部局部变量的相关信息 LocalVariableTable: Start Length Slot Name Signature 0 31 0 this Lio/renren/JavapTest; 3 28 1 i I static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=1, args_size=0 0: ldc #14 // String www.atguigu.com 2: astore_0 3: return LineNumberTable: line 11: 0 line 12: 3 LocalVariableTable: Start Length Slot Name Signature}SourceFile: "JavapTest.java" // 附加属性:指明当前字节码文件对应的源程序文件名3、jclasslib展示的内容:
5-总结
指令分类
加载与存储指令
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。
局部变量压栈指令
将一个局部变量加载到操作数栈:xload、xload_(其中x为i、l、f、d、a,n为0到3)
常量入栈指令
将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst、lconst_<1>、fconst、dconst
出栈装入局部变量表指令
将一个数值从操作数栈存储到局部变量表:xstore、xstore_(其中x为i、l、f、d、a,n为0到3);xastore(其中x为i、1、f、d、a、b、c、s)
扩充局部变量表的访问索引的指令
wide
关于iload_0和iload 0的举列
iload_0:将局部变量表中索引为0位置上的数据压入操作数栈中。iload 0:将局部变量表中索引为0位置上的数据压入操作数栈中。iload 4:将局部变量表中索引为4位置上的数据压入操作数栈中。
备注:操作码1个字节,操作数2个字节,弄成有默认的iload_0只占1个字节能节省空间,但最多只有0-3
注意说明
上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload)。这些指令助记符实际上代表了一组指令(例如iload代表了iload0、iload1、iload2和iload3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。除此之外,它们的语义与原生的通用指令完全一致(例如iload0的语义与操作数为0时的iload指令语义完全一致)。在尖括号之间的字母指定了指令隐含操作数的数据类型,<n>代表非负的整数,<i>代表是int类型数据,<l>代表long类型,<f>代表float类型,<d>代表doub1e类型。操作byte、char、short和boolean类型数据时,经常用int类型的指令来表示。
再谈操作数栈与局部变量表
操作数栈(Operand Stacks)
我们知道,Java字节码是Java虚拟机所使用的指令集。因此,它与Java虚拟机基于栈的计算模型是密不可分的。在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。具体来说便是:执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。
图示
以加法指令iadd为例。假设在执行该指令前,栈顶的两个元素分别为int值1和int值2,那么iadd指令将弹出这两个int,并将求得的和int值3压入栈中。由于iadd指令只消耗栈顶的两个元素,因此,对于离栈顶距离为2的元素,即图中的问号,iadd指令并不关心它是否存在,更加不会对其进行修改。
算术指令
引言大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点类型数据进行运算的指令。
byte、short、char和boolean类型说明在每一大类中,都有针对Java虚拟机具体数据类型的专用算术指令。但没有直接支持byte、short、char和boolean类型的算术指令,对于这些数据的运算,都使用int类型的指令来处理。此外,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。
加法指令:iadd、ladd、fadd、dadd减法指令:isub、lsub、fsub、dsub乘法指令:imul、lmul、fmul、dmul除法指令:idiv、ldiv、fdiv、ddiv求余指令:irem、lrem、frem、drem //remainder:余数取反指令:ineg、lneg、fneg、dneg //negation:取反自增指令:iinc比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp位运算指令,又可分为:位移指令:ishl、ishr、iushr、lshl、lshr、lushr按位或指令:ior、lor按位与指令:iand、land按位异或指令:ixor、lxor
运算时的溢出 与算数模式
运算时的溢出数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数。其实Java虚拟机规范并无明确规定过整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0时会导致虚拟机抛出异常ArithmeticException。
NaN值使用当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用NaN值来表示。而且所有使用NaN值作为操作数的算术操作,结果都会返回NaN;(Infinity无穷大)
运算模式向最接近数舍入模式:JVM要求在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的;(类似四舍五入)向零舍入模式:将浮点数转换为整数时,采用该模式,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果;(类似取整)
++i 与 i++
案例一
代码
public static void main(String[] args) { int i = 0; for (int j = 0; j < 50; j++) { i = i++; } System.out.println(i);}
字节码
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iconst_0 3: istore_2 4: iload_2 5: bipush 50 7: if_icmpge 21 10: iload_1 11: iinc 1, 1 14: istore_1 15: iinc 2, 1 18: goto 4 21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 24: iload_1 25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 28: return
分析
pc 0~1两个指令,为下标为1的局部变量赋值为0,也就是局部变量ipc 2~3两个指令,为下表为2的局部变量赋值为0,也就是局部变量jpc 4~7三个指令,取局部变量j的值与50比较,如果j>=50,跳转到pc=21的指令处,如果不满足则顺序往下执行pc 10~14三个指令,是i=i++这行代码编译后的指令。在jvm中,局部变量表和操作数栈是两个不同的存储数据的内存区域。iload_1表示将局部变量表中下标为1的变量,也就是变量i的值复制一份,加载到操作数栈顶,innc 1,1 指令则将局部变量表中变量i的值加1再写回局部变量表中变量i的位置,istore_1则将栈顶的数据覆盖局部变量表中变量i的位置,所以执行完这3个命令后,变量i的值并没有发生变化。用伪代码来表示这三个指令的逻辑就是这样int stack_top = local_variable[1];//把下标为1的局部变量加载到栈顶local_variable[1] = local_variable[1] + 1;//下标为1的局部变量自增1local_variable[1] = stack_top;//用栈顶的值覆盖下标为1的局部变量pc 15指令iinc 2,1 将变量j自增1pc 18指令goto 4,程序重新从pc=4的地方开始执行pc 21~25三个指令,就是打印下标为1的局部变量,也就是打印变量i所以,从pc10~14三个指令,可以看出变量i=i++这行代码不会改变变量i的值,因此最后打印结果是0。
案例二
代码
public static void main(String[] args) { int i = 0; for (int j = 0; j < 50; j++) { i = ++i; } System.out.println(i);}
字节码
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iconst_0 3: istore_2 4: iload_2 5: bipush 50 7: if_icmpge 21 10: iinc 1, 1 13: iload_1 14: istore_1 15: iinc 2, 1 18: goto 4 21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 24: iload_1 25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 28: return
分析
除了指令10~14,对应的代码就是 i=++i,其他部分跟上面的字节码指令一样,所以我们只看不一样的部分。pc 10 innc 1,1 这里先执行自增指令,将下标为1的局部变量i的值自增1pc 13 iload_1 将下标为1的局部变量i的值加载到操作数栈顶pc 14istore_1 将操作数栈顶的值覆盖下标为1的局部变量i的值用伪代码来表示这段逻辑就是这样local_variable[1] = local_variable[1] + 1;//下标为1的局部变量自增1int stack_top = local_variable[1];//把下标为1的局部变量加载到栈顶local_variable[1] = stack_top;//用栈顶的值覆盖下标为1的局部变量所以,从pc10~14三个指令,可以看出变量 i=++i 这行代码会使i的值增加1,因此最后打印结果是50。与i++对应的指令不同的地方是,++i会先执行innc 1,1指令,这条指令会是i的值增加1,然后再参与计算。而i++会先将i的值保存到另外一个地方,然后再对i自增1,但是i=i++的赋值(也就是=)会用已保存的i的旧值覆盖i的新值,所以i=i++,i的值并不会变。
案例三
代码
public static void main(String[] args) { int i = 0; int result = i++ + ++i + i++; System.out.println(i); System.out.println(result); }
字节码
0 iconst_0 1 istore_1 2 iload_1 3 iinc 1 by 1 6 iinc 1 by 1 9 iload_110 iadd11 iload_112 iinc 1 by 115 iadd16 istore_217 getstatic #2 <java/lang/System.out>20 iload_121 invokevirtual #3 <java/io/PrintStream.println>24 getstatic #2 <java/lang/System.out>27 iload_228 invokevirtual #3 <java/io/PrintStream.println>31 return
分析
结果等于4
案例四
代码
public static void main(String[] args) { int i = 0; i++; ++i; System.out.println(i); }
子主题
0 iconst_0 1 istore_1 2 iinc 1 by 1 5 iinc 1 by 1 8 getstatic #2 <java/lang/System.out>11 iload_112 invokevirtual #3 <java/io/PrintStream.println>15 return
分析
++i与i++ 在没有赋值的情况下的字节码是一样的
比较控制指令
说明
比较指令的作用是比较栈顶两个元素的大,并将比较结果入栈。比较指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp。与前面讲解的指令类似,首字符d表示double类型,f表示float,l表示long。对于double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。指令dcmpl和dcmpg也是类似的,根据其命名可以推测其含义,在此不再赘述。指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。
举列
指令fcmpg和fcmp1都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0;若v1>v2则压入1;若v1<v2则压入-1。(相当于v1 > v2,v1先入栈)两个指令的不同之处在于,如果遇到NaN值,fcmpg会压入1,而fcmpl会压入-1。左边/底部更大压栈进入1
类型转换指令
引言
1.类型转换指令可以将两种不同的数值类型进行相互转换。2.这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
宽化类型转换
转换规则
Java虚拟机直支持以下数值的宽化类型转换(widening numeric conversion,小范围类型向大范围类型的安全转换)。也就是说,并不需要指令执行,包括: 从int类型到long、float或者double类型。对应的指令为:i2l、i2f、i2d 从long类型到float、double类型。对应的指令为:l2f、l2d 从float类型到double类型。对应的指令为:f2d简化为:int–>long–>float–>double
案例
代码
public static void main(String[] args) { int i = 10; long l = i; // i2l float f = i; // i2f double d = i; // i2d float f1 = l; // l2f double d1 = l; // l2d double d2 = f1; // f2d }
字节码
0 bipush 10 2 istore_1 3 iload_1 4 i2l 5 lstore_2 6 iload_1 7 i2f 8 fstore 410 iload_111 i2d12 dstore 514 lload_215 l2f16 fstore 718 lload_219 l2d20 dstore 822 fload 724 f2d25 dstore 1027 return
精度损失问题
1.宽化类型转换是不会因为超过目标类型最大值而丢失信息的,例如,从int转换到long,或者从int转换到double,都不会丢失任何信息,转换前后的值是精确相等的。2.从int、long类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失——可能丢失掉几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值。3.尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java虚拟机抛出运行时异常。
补充说明
补充说明从byte、char和short类型到int类型的宽化类型转换实际上是不存在的。对于byte类型转为int,虚拟机并没有做实质性的转化处理,只是简单地通过操作数栈交换了两个数据。而将byte转为long时,使用的是i2l,可以看到在内部byte在这里已经等同于int类型处理,类似的还有short类型,这种处理方式有两个特点: 一方面可以减少实际的数据类型,如果为short和byte都准备一套指令,那么指令的数量就会大增,而虚拟机目前的设计上,只愿意使用一个字节表示指令,因此指令总数不能超过256个,为了节省指令资源,将short和byte当做int处理也在情理之中。 另一方面,由于局部变量表中的槽位固定为32位,无论是byte或者short存入局部变量表,都会占用32位空间。从这个角度说,也没有必要特意区分这几种数据类型。
窄化类型转换(强制转换)
转换规则
Java虚拟机也直接支持以下窄化类型转换: 从int类型至byte、short或者char类型。对应的指令有:i2b、i2s、i2c 从long类型到int类型。对应的指令有:l2i 从float类型到int或者long类型。对应的指令有:f2i、f2l 从double类型到int、long或者float类型。对应的指令有:d2i、d2l、d2f
精度损失问题
1.窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度。2.尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况,但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常
补充说明
当将一个浮点值窄化转换为整数类型T(T限于int或long类型之一)的时候,将遵循以下转换规则: 如果浮点值是NaN,那转换结果就是int或long类型的0。 如果浮点值不是无穷大的语浮点值使用IEEE754的向零舍入模式取整,获得整数值v,如果v在目标类型T(int或long)的表示范围之内,那转换结果就是v。否则,将 根据v的符号,转换为T所能表示的最大或者最小正数当将一个double类型窄化转换为float类型时,将遵循以下转换规则:通过向最接近数舍入模式舍入一个可以使用float类型表示的数字。最后结果根据下面这3条规则判断: 如果转换结果的绝对值太小而无法使用float来表示,将返回float类型的正负零。 如果转换结果的绝对值太大而无法使用float来表示,将返回float类型的正负无穷大。 对于double类型的NaN值将按规定转换为float类型的NaN值。
对象的创建与访问指令
引言
Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。
创建指令
1.虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令: 创建类实例的指令: 创建类实例的指令:new 它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。 创建数组的指令: 创建数组的指令:newarray、anewarray、multianewarray。 newarray:创建基本类型数组 anewarray:创建引用类型数组 multianewarray:创建多维数组上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也非常高。
为什么要dup呢,dup是将栈顶数值复制一份并送入至栈顶。因为invokespecial会消耗掉一个当前类的引用,因而需要复制一份。
字段访问指令
对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、putfieldget 压进操作数栈,get/putstatic命令不需要读取局部变量表的实列
示例
代码
static public int a = 0; public int b = 0; public static void main(String[] args) { test asds = new test(); asds.b = 1; test.a = 2; }
字节码
0 new #3 <com/aeon/dmc/code/generator/test/test> 3 dup 4 invokespecial #4 <com/aeon/dmc/code/generator/test/test.<init>> 7 astore_1 8 aload_1 9 iconst_110 putfield #2 <com/aeon/dmc/code/generator/test/test.b>13 iconst_214 putstatic #5 <com/aeon/dmc/code/generator/test/test.a>17 return
数组操作指令
数组操作指令主要有:xastore和xaload指令。具体为: 一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload 将一个操作数栈的值存储到数组元素中的指令:rbastore、castore、sastore、iastore、lastore、fastore、dastore、aastore 取数组长度的指令:arraylength该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈。
说明
1.指令xaload表示将数组的元素压栈,比如saload、caload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈。2.xastore则专门针对数组操作,以iastore为例,它用于给一个int数组的给定索引赋值。在iastore执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore会弹出这3个值,并将值赋给数组中指定索引的位置。
示例
代码
public static void main(String[] args) { int[] intArray = new int[10]; intArray[3] = 20; System.out.println(intArray[1]); boolean[] arr = new boolean[10]; arr[1] = true; // bastore double[] arrDouble = new double[2]; System.out.println(arrDouble.length); // arraylength }
字节码
0 bipush 10 2 newarray 10 (int) 4 astore_1 5 aload_1 6 iconst_3 7 bipush 20 9 iastore10 getstatic #2 <java/lang/System.out>13 aload_114 iconst_115 iaload16 invokevirtual #3 <java/io/PrintStream.println>19 bipush 1021 newarray 4 (boolean)23 astore_224 aload_225 iconst_126 iconst_127 bastore28 iconst_229 newarray 7 (double)31 astore_332 getstatic #2 <java/lang/System.out>35 aload_336 arraylength37 invokevirtual #3 <java/io/PrintStream.println>40 return
类型检查指令
说明检查类实例或数组类型的指令:instanceof、checkcast。 指令cheqkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常。| 指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈。
示例
//4.类型检查指令 public String checkcast(Object obj) { if (obj instanceof String) { return (String) obj; } else { return null; } // 0 aload_1 // 1 instanceof #10 <java/lang/String> // 4 ifeq 12 (+8) // 7 aload_1 // 8 checkcast #10 <java/lang/String> //11 areturn //12 aconst_null //13 areturn }
方法调用指令
引言方法调用指令:invokevirtual、invokeinterface、invokespecial、invokestatic、invokedynamic以下5条指令用于方法调用: invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。 invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。 invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。 invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的。 invokedynamic:调用动态绑定的方法,这个是JDK1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
示例
代码
public class MethodInvokeReturnTest { //方法调用指令:invokespecial public void invokel() { //情况1:类实例构造器方法:<init>() Date date = new Date(); Thread t1 = new Thread(); //情况2:父类的方法 super.toString(); //情况3:私有方法 methodPrivate(); // 0 new #2 <java/util/Date> // 3 dup // 4 invokespecial #3 <java/util/Date.<init>> // 7 astore_1 // 8 new #4 <java/lang/Thread> //11 dup //12 invokespecial #5 <java/lang/Thread.<init>> //15 astore_2 //16 aload_0 //17 invokespecial #6 <java/lang/Object.toString> //20 pop //21 aload_0 //22 invokespecial #7 <T1/MethodInvokeReturnTest.methodPrivate> //25 return } private void methodPrivate() { } //方法调用指令:invokestatic public void invoke2() { methodstatic(); //0 invokestatic #8 <T1/MethodInvokeReturnTest.methodstatic> //3 return } private static void methodstatic() { } //方法调用指令:invokeinterface public void invoke3() { Thread t1 = new Thread(); ((Runnable) t1).run(); // invokeinterface #9 <java/lang/Runnable.run> count 1 Comparable<Integer> com = null; com.compareTo(123); // invokeinterface #11 <java/lang/Comparable.compareTo> count 2 }}
代码2
package T1;public class InterfaceMethodTest { public static void main(String[] args) { AA bb = new BB(); // invokespecial #3 <T1/BB.<init>> bb.method2(); // invokeinterface #4 <T1/AA.method2> AA.method1(); // invokestatic #5 <T1/AA.method1> }}interface AA { public static void method1() { } public default void method2() { }}class BB implements AA {}
方法返回指令
引言方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。包括ireturn(当返回值是boolean、byte、char、short和int 类型时使用)、lreturn、freturn、dreturn和areturn另外还有一条return 指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
举例:通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。如果当前返回的是synchronized方法,那么还会执行一个隐含的monitorexit指令,退出临界区。最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。
示例
代码
public class MethodReturnTest { public float returnFloat() { int i = 10; return i; // 0 bipush 10 //2 istore_1 //3 iload_1 //4 i2f //5 freturn }}
操作数栈管理指令
引言
如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。这类指令包括如下内容: 将一个或两个元素从栈顶弹出,并且直接废弃:pop,pop2; 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup,dup2,dup_×1,dup2_×1,dup_×2,dup2_×2; 将栈最顶端的两个slot数值位置交换:swap。Java虚拟机没有提供交换两个64位数据类型( long、doub1e)数值的指令。 指令nop,是一个非常特殊的指令,它的字节码为exee。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等。这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。
说明
不带x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2。dup的系数代表要复制的Slot个数。dup开头的指令用于复制1个Slot的数据。例如1个int或1个reference类型数据dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个float类型数据带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_×1,dup2_×1,dup_×2,dup2_×2。对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此 dup_×1插入位置:1+1=2,即栈顶2个slot下面 dup_×2插入位置:1+2=3,即栈顶3个slot下面 dup2_×1插入位置:2+1=3,即栈顶3个Slot下面 dup2_×2插入位置:2+2=4,即栈顶4个Slot下面pop:将栈顶的1个slot数值出栈。例如1个short类型数值pop2:将栈顶的2个slot数值出栈。例如1个double类型数值,或者2个int类型数值
示例
代码1
public void print() { Object obj = new Object();// String info=obj.toString(); // astore_2 obj.toString(); // pop } public void foo() { bar(); // pop2 } public long bar() { return 0; }
代码2
控制转移指令
引言
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为1)比较指令、2)条件跳转指令、3)比较条件转指令、4)多条件分支跳转指令、5)无条件跳转指令等。数值类型的数据,才可以谈大小!(byte\short\char\int;long\float\double)boolean、引用数据类型不能比较大小。
条件跳转指令
介绍
条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。条件跳转指令有:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。
注意
与前面运算规则一致: 对于boolean、byte、char、short类型的条件分支比较操作,都是使用int类型的比较指令完成 对于long、float、double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转由于各类型的比较最终都会转为int类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强
代码举例
//1.条件跳转指令 public void compare1() { int a = 0; if (a == 0) { a = 10; } else { a = 20; } } // 0 iconst_0 // 1 istore_1 // 2 iload_1 // 3 ifne 12 (+9) 当不为0时跳转到12 // 6 bipush 10 // 8 istore_1 // 9 goto 15 (+6) //12 bipush 20 //14 istore_1 //15 return // ifnonnull public boolean compareNul1(String str){ if(str==null){ return true; }else { return false; } // 0 aload_1 //1 ifnonnull 6 (+5) // 不为null时跳转到6 //4 iconst_1 //5 ireturn //6 iconst_0 //7 ireturn }
比较条件跳转指令
介绍
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。这类指令有:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括short和byte类型),以字符“a”开头的指令表示对象引用的比较。这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下条语句。
举例
//2.比较条件跳转指令 public void ifCompare1() { int i = 10; int j = 20; System.out.println(i > j); } public void ifCompare2() { short s1 = 9; byte b1 = 10; System.out.println(s1 > b1); // 都是if_ixxx } public void ifCompare3() { Object obj1 = new Object(); Object obj2 = new Object(); // new 指向堆内存地址不一样 System.out.println(obj1 == obj2); //false System.out.println(obj1 != obj2);//true }
多条件分支跳转
介绍
多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch。从助记符上看,两者都是switch语句的实现,它们的区别: tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高 指令lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch如下图所示。
图示
举例
//3.多条件分支跳转 public void swtichl(int select) { int num; switch (select) { case 1: num = 10; break; case 2: num = 20;// break; case 3: num = 30; break; default: num = 40; } // 1 tableswitch 1 to 3 1: 28 (+27) // 2: 34 (+33) // 3: 37 (+36) // default: 43 (+42) //34 bipush 20 //36 istore_2 //37 bipush 30 //39 istore_2 //40 goto 46 (+6) // 当没有break结束时,继续往下执行break或者default System.out.println(num); } public void swtich2(int select) { int num; switch (select) { case 100: num = 10; break; case 500: num = 20; break; case 200: num = 30; break; default: num = 40; } // 1 lookupswitch 3 // 100: 36 (+35) // 200: 48 (+47) // 500: 42 (+41) // default: 54 (+53) // 自动排序好 } //idk7新特:性引入string类型 public void swtich3(String season) { switch (season) { case "SPRING": break; case "SUMMER": break; case "AUTUMN": break; case "WINTER": break; } // 5 invokevirtual #8 <java/lang/String.hashCode> // 8 lookupswitch 4 // -1842350579: 52 (+44) // -1837878353: 66 (+58) // -1734407483: 94 (+86) // 1941980694: 80 (+72) // default: 105 (+97) // 52 aload_2 // 53 ldc #9 <SPRING> // 55 invokevirtual #10 <java/lang/String.equals> // 对于String,先进行hashcode对比,相同了再equals }
无条件跳转圈
介绍
目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。指令jsr、jsr_w、ret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机逐渐废弃,故不在这里介绍这两个指令。
异常处理指令
过程一:异常对象的生成过程–>throw(手动/自动)–>指令:athrow过程二:异常的处理:抓抛模型。try-catch-finally -->使用异常表
异常
抛出异常指令
athrow指令
1.在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。2.除了使用throw语句显示抛出异常情况之外,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。
举例
public void throwZero(int i) { if (i == 0) { throw new RuntimeException("参数错误"); } }
异常的处理
在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成的。
异常表
描述
如果一个方法定义了一个try-catch或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如:起始位置·结束位置程序计数器记录的代码处理的偏移地址被捕获的异常类在常量池中的索引当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标
举例
public void trycatch() { try { File file = new File("d:/hello. txt"); FileInputStream fis = new FileInputStream(file); String info = "hello!"; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (RuntimeException e) { e.printStackTrace(); } }
举例2
//思考:如下方法返回结果为多少? public static String func() { String str = "hello"; try { return str; } finally { str = "atguigu"; } }
默认执行了try和finally,正常输出hello,当有异常时再执行一次finally
注意
常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。
同步控制指令
引言
java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的。
方法级的同步
方法级的同步:是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法;当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置。 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。
图示
这段代码和普通的无同步作的代码没有什么不同,没有使用monitorenter和monitorexit进行同步区控制。这是因为,对于同步方法而言,当虚拟机通过方法的访问标示符判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁。因此,对于同步方法而言,monitorenter和monitorexit指令是隐式存在的,并未直接出现在字节码中。
方法内指定指令序列的同步
介绍
同步一段指令集序列: 通常是由java中的synchronized语句块来表示的。jvm的指令集有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。当线程退出同步块时,需要使用monitorexit声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的。下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。
图示
注意
同步代码块,就算是报错时都会让执行monitorexit释放同步锁,并athrow
举例
private int i = 0; private Object obj = new Object(); public void subtract() { synchronized (obj) { i--; } }
类加载相关
类加载过程
引言
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作
类生命周期
触发类的加载
(1) 类中有入口方法 main(2) new 一个类实例(3) 使用反射机制加载实例化一个类(4) 类的静态方法被调用(5) 加载子类,但是父类还没有加载,就会触发父类的类加载。
类的加载详细过程
Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作
1. 加载
1. 加载的理解
所谓加载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。所谓类模板对象,其实就是Java类在]VM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样]VM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射。
2. 加载完成的操作
通过一个类的全限定名来获取定义此类的二进制字节流
从本地系统中直接加载
从ZIP包中读取
从网络中获
运行时计算生成,动态代理
Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
由其他文件生成,典型场景是JSP应用,即由JSP文件生成对应的Class类
从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)
流程的获取方式大致为这几种
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
3. 类模型与Class实例的位置
类模型的位置加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDKl.8之前:永久代;J0Kl.8及之后:元空间)。Class实例的位置类将.class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。
图示
外部可以通过访问代表order类的Class对象来获取Order的类数据结构。
4. 注意
Class类的构造方法是私有的,只有JVM能够创建。java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过class类提供的接口,可以获得目标类所关联的.class文件中具体的数据结构:方法、字段等信息。该 Java.lang.Class对象是单实例的 ,无论这个类创建了多少个对象,他的Class对象时唯一的。 而 加载并获取该Class对象可以通过三种途径:Class.forName(类的全路径)、实例对象.class(属性)、实例对象getClass()
举例
/** * 通过cLass类,获得了java.Lang.string类的所有方法信息,并打印方法访问标识符、描述符 */public class LoadingTest { public static void main(String[] args) { try { Class clazz = Class.forName("java.lang.String"); //获取当前运行时类声明的所有方法 Method[] ms = clazz.getDeclaredMethods(); for (Method m : ms) { //获取方法的修饰符 String mod = Modifier.toString(m.getModifiers()); System.out.print(mod + ""); //获取方法的返回值类型 String returnType = (m.getReturnType()).getSimpleName(); System.out.print(returnType + ""); //获取方法名 System.out.print(m.getName() + "("); //获取方法的参数列表 Class<?>[] ps = m.getParameterTypes(); if (ps.length == 0) System.out.print(')'); for (int i = 0; i < ps.length; i++) { char end = (i == ps.length - 1) ? ')' : ','; //获取参教的类型 System.out.print(ps[i].getSimpleName() + end); } } } catch (ClassNotFoundException e) { e.printStackTrace(); } }}
5. 数组类的加载
创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称A)的过程: 1.如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组A的元素类型; 2.JVM使用指定的元素类型和数组维度来创建新的数组类。如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public。比如int[]是基本类型,String[]和Object[]是引用类型
2. 验证
1. 验证说明
当类加载到系统后,就开始链接操作,验证是链接操作的第一步。它的目的是保证加载的字节码是合法、合理并符合规范的。验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证,以及符号引用验证等。 其中格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。 格式验证之外的验证操作将会在方法区中进行。链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查。(磨刀不误砍柴工)
2. 验证内容
具体说明
1.格式验证:是否以魔数0XCAFEBABE开头,主版本和副版本号是否在当前Java虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等。2.Java虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如:是否所有的类都有父类的存在(在Java里,除了object外,其他类都应该有父类)是否一些被定义为final的方法或者类被重写或继承了非抽象类是否实现了所有抽象方法或者接口方法3.Java虚拟机还会进行字节码验证,字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:在字节码的执行过程中,是否会跳转到一条不存在的指令函数的调用是否传递了正确类型的参数变量的赋值是不是给了正确的数据类型等栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是,100%准确地判断一段字节码是否可以被安全执行是无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。在前面3次检查中,已经排除了文件格式错误、语义错误以及字节码的不正确性。但是依然不能确保类是没有问题的。4.校验器还将进行号引用的验证。Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此,在验证阶段,虚拟机就会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出NoC1assDefFoundError,如果一个方法无法被找到,则会抛出NoSuchMethodError。此阶段在解析环节才会执行。
文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
1. 是否以魔数0xCAFEBABE开
2. 主、次版本号是否在当前虚拟机处理范围之内
3. 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
6. Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
7. 。。。
这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流
元数据验证
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
1. 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
2. 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
4. 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息
5. 。。。
字节码验证
第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
1. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
例如不会出现类似这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中
2. 保证跳转指令不会跳转到方法体以外的字节码指令上
3. 保证方法体中的类型转换是有效的
例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的
4. 。。。
符号引用验证
最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验
1. 符号引用中通过字符串描述的全限定名是否能找到对应的类
2. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
3. 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
4. 。。。
对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要(因为对程序运行期没有影响)的阶段。如果所运行的全部代码(包括自己编写的及第三方包中的代码)都已经被反复使用和验证过,那么在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
图示
3. 准备
说明
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值注意:Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是e,故对应的,boolean的默认值就是false。1.为类变量分配内存并且设置该类变量的默认初始值,即零值2.这里不包含用final修饰的static,因为final在编译的时候就会分配好了默认值,准备阶段会显式初始化3.注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
1. int
0
2. long
0.0L
3. short
(short)0
4. char
'\u0000'
5. byte
(byte)0
6. boolean
false
7. float
0.0f
8. double
0.0d
9. reference
null
分配0值情况
注意
这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。
基本数帮类型:非Final修饰的变量,在准备环节进行默认初始化时值。Final修饰以后,在准备环节直接进行显示慰值。引用类型String:如果使用字面量的方式定义一个字符的常量的话,也是在解析环节直接进行显示财值。
4. 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
说明
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。 假设:一个类有一个静态变量,该静态变量是一个自定义的类型,那么经过解析后,该静态变量将是一个指针,指向该类在方法区的内存地址。
将常量池内的符号引用转换为直接引用的过程事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
链接阶段的验证阶段和加载阶段几乎同时进行
子主题
类或接口解析
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要以下3个步骤
1. 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就宣告失败
2. 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象
3. 如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常
字段解析
要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index[插图]项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索
1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
2. 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
3. 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常
类方法解析
类方法解析的第一个步骤与字段解析一样,也需要先解析出类方法表的class_index[插图]项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索
1. 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常
2. 如果通过了第1步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
3. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
4. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出java.lang.AbstractMethodError异常
5. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError
如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常
接口方法解析
接口方法也需要先解析出接口方法表的class_index[插图]项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索
1. 与类方法解析不同,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常
2. 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
3. 否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围会包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
4. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常
由于接口中的所有方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常
举例说明
输出操作System.out.println()对应的字节码:|invokevirtual#24<java/io/PrintStream.println>
图示
以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。
总结
所调解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot VM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行。
5. 初始化
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的
1.初始化阶段就是执行类构造器方法<clinit>()的过程2.此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法3.<clinit>()方法中的指令按语句在源文件中出现的顺序执行4.<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())5.若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕6.虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
从运行程序开始介绍类加载的关键流程
(1) 在 Windows 平台下开始运行一个 Java 程序,首先 java.exe 会调用底层 vm.dll 文件创建一个 Java 虚拟机,Java 虚拟机的底层是由 C++ 来实现的;(2) 接着创建引导类加载器,引导类加载器也是 C++ 实现的,引导类加载器会负责加载 jre/lib 目录下的类库;(3)引导类加载器加载 sun.misc.Launcher 类,这是 JVM 启动类,在创建这个类的同时会创建其它的类加载器,例如扩展类加载器、应用类加载器、自定义的类加载器。(4) 需要加载一个类时通过 launcher.getClassLoader() 方法获取类加载器,系统默认的类加载器是 应用类加载器(AppClassLoader) ;(5) 调用类加载器的 loadClass(String name) 方法对 类进行加载,这其中就会实现双亲委派机制;(6) 类加载完成后,调用 main 入口方法,执行程序;(7) Java 程序执行结束后 销毁 JVM。
类加载分类
隐式加载
创建类对象
使用类的静态域
创建子类对象
使用子类的静态域
在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件
显式加载
ClassLoader.loadClass(className),只加载和连接、不会进行初始化
Class.forName(String name, boolean initialize,ClassLoader loader); 使用loader进行加载和连接,根据参数initialize决定是否初始化。
类加载机制:双亲委派
如果一个类加载器收到了类加载请求,它不会首先加载这个类,而是将请求委派给父类加载器去完成所有的加载请求最终都委派给顶层的引导类加载器,只有当父类加载器无法完成加载请求(也就是搜索范围内无该类)子加载器才会尝试自己去加载这个类双亲委派过程:当一个类加载器收到类加载任务时,立即将任务委派给它的父类加载器去执行,直至委派给最顶层的启动类加载器为止。如果父类加载器无法加载委派给它的类时,将类加载任务退回给它的下一级加载器去执行;除了启动类加载器以外,每个类加载器拥有一个父类加载器,用户的自定义类加载器的父类加载器是AppClassLoader;双亲委派模型可以保证全限名指定的类,只被加载一次;双亲委派模型不具有强制性约束,是Java设计者推荐的类加载器实现方式;
类加载器分类
启动类加载器/Bootstrap ClassLoader
在HotSpot虚拟机中,Bootstrap ClassLoader用C++语言编写并嵌入JVM内部,主要负载加载JAVA_HOME/lib目录中的所有类,或者加载由选项-Xbootcalsspath指定的路径下的类;例如 charsets.jar rt.jar
拓展类加载器/ExtClasLoader
ExtClassLoader继承ClassLoader类,负载加载JAVA_HOME/lib/ext目录中的所有类型,或者由参数-Xbootclasspath指定路径中的所有类型;
应用程序类加载器/AppClassLoader
ExtClassLoader继承ClassLoader类,负责加载用户类路径ClassPath下的所有类型,一般情况下为程序的默认类加载器;
自定义加载器
Java虚拟机规范将所有继承抽象类java.lang.ClassLoader的类加载器,定义为自定义类加载器;
类加载器层次结构
为什么采用双亲委派模型?
——使得Java类随着类加载器不同而具备带优先级的层次关系,如java.lang.Object(位于rt.jar内),无论那个类加载器要加载该类,最终都委派给顶层引导类加载器,因此Object类在程序的各种类加载环境中都是同一个类。——如果没有双亲委派,用户自定义重名的类,将会使得系统带有多个同名的类,使得基础的Java类型体系混乱双亲委派模型对于保证Java程序的稳定运行十分重要,它实现却很简单首先检查是否被加载过,若没有加载则调用父类加载器的loadClass方法,若父类加载器为空,则默认使用引导类加载器作为父类加载器,如果加载失败,则调用自身的findClass()方法加载——破坏双亲委派情形:使用JNDI服务、代码模块热部署
(1) 避免重复加载。当父加载器已经加载过该类,就没有必要再让子加载器进行加载,保证加载类的唯一性。(三个加载器都遍历判断是否加载过,如果都没有加载过才会对该类进行加载) (2) 沙箱安全机制。自己写的 java.lang.String 类不会被加载,这样可以防止核心 API 库被随意篡改。
源码分析
1.图解类加载过程1).图中绿色的线代表向parent类加载器查找是否有被加载的过程2).蓝色的线代表未加载的类应该由哪一种类加载器加载的过程
2.JDK1.8源码分析类加载过程
3.扩展:打破双亲委派机制从上述代码中可以看出,只要重写ClassLoader的loadClass(),在loadClass()中不调用parent的loadClass(),是可以打破双亲委派机制的.打破双亲委派机制在实际应用中,主要体现在热部署
Java 执行代码的大致流程
假设要执行A类的main方法1.启动虚拟机 (C++负责创建) 【windows : bin/java.exe调用 jvm.dll Linux : java 调用 libjvm.so 】2.创建一个引导类加载器实例 (C++实现)3.C++ 调用Java代码,创建JVM启动器,实例sun.misc.Launcher 【这货由引导加载器负责加载创建其他类加载器】4.JVM-白话聊一聊JVM类加载和双亲委派机制源码解析_JVM教程_035.JVM-白话聊一聊JVM类加载和双亲委派机制源码解析_Java开发_046.sun.misc.Launcher.getLauncher() 获取运行类自己的加载器ClassLoader --> 是AppClassLoader , 通过上图源码可知7.获取到ClassLoader后调用loadClass(“A”)方法加载运行的类A8.加载完成执行A类的main方法9.程序运行结束10.JVM销毁
图示
JVM启动时,C++会实例化JVM启动器实例sun.misc.Launcherprivate static Launcher launcher = new Launcher(); Launcher构造方法内部, 创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。
打破双亲委派机制
类型
1.双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐2.双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()3.双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
破坏双亲委派机制1
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前一—即JDKl.2面世以前的“远古”时代。由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。上节我们已经分析过loadClass()方法,双亲委派的具体逻辑就实现在这里面,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。
破坏双亲委派机制2
第二次破坏双亲委派机制:线程上下文类加载器双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK l.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?(SPI:在Java平台中,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI)为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,**这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,**但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。
破坏双亲委派机制3
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等IBM公司主导的JSR-291(即OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(osGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bund1e连同类加载器一起换掉以实现代码的热替换。在oSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:*将以java.开头的类,委派给父类加载器加载。否则,将委派列表名单内的类,委派给父类加载器加载。否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。否则,查找Dynamic Import列表的Bundle,委派给对应Bund1e的类加载器加载。否则,类查找失败。说明:只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的
Tomcat
JDBC
原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了.
JDK8和JDK9中类加载器对比
由于JDK9引入的Java模块化系统(Java Platform Module System,JPMS)为了保证兼容性,JDK 9并没有从根本上动摇从JDK 1.2以来运行了二十年之久的三层类加载器架构以及双亲委派模型。但是为了模块化系统的顺利施行,模块化下的类加载器仍然发生了一些应该被注意到变动,主要包括以下几个方面:1.扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代。2.平台类加载器和应用程序类加载器都不再派生自java.net.URLClassLoader,如果有程序直接依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能会在JDK 9及更高版本的JDK中崩溃。3.启动类加载器现在是在Java虚拟机内部和Java类库共同协作实现的类加载器,尽管有了BootClassLoader这样的Java类,但为了与之前的代码保持兼容,所有在获取启动类加载器的场景(譬如Object.class.getClassLoader())中仍然会返回null来代替,而不会得到BootClassLoader的实例。
沙箱安全机制
自定义加载器
好处
隔离加载类在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。修改类加载的方式类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载扩展加载源比如从数据库、网络、甚至是电视机机顶盒进行加载防止源码泄漏Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。
场景
实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。这个方面的集大成者是JavaEE和OSGI、JPMS等框架。应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型。
注意
在一般情况下,使用不同的类加载器去加载不同的功能模块,会提高应用程序的安全性。但是,如果涉及Java类型转换,则加载器反而容易产生不美好的事情。在做Java类型转换时,只有两个类型都是由同一个加载器所加载,才能进行类型转换,否则转换时会发生异常。
实现方式
Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。在自定义ClassLoader的子类时候,我们常见的会有两种做法:方式一:重写loadClass()方法方式二:重写findclass()方法
对比
这两种方法本质上差不多,毕竟loadClass()也会调用findClass(),但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。loadclass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写loadClass()方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作。
说明
其父类加载器是系统类加载器JVM中的所有类加载都会使用java.lang.ClassLoader.loadClass(String)接口(自定义类加载器并重写java.lang.ClassLoader.loadClass(String)接口的除外),连JDK的核心类库也不能例外。
字节码框架
ASM
ASM介绍
ASM是什么
简单来说,ASM是一个操作Java字节码的类库。
ASM的操作对象是什么呢?
回答:ASM所操作的对象是字节码(ByteCode)数据。我们都知道,一个.java文件经过Java编译器(javac)编译之后会生成一个.class文件。 在.class文件中,存储的是字节码(ByteCode)数据,如下图所示。ASM所的操作对象是是字节码(ByteCode),而在许多情况下,字节码(ByteCode)的具体表现形式是.class文件。
ASM是如何处理字节码(ByteCode)数据的?
回答:ASM处理字节码(ByteCode)的方式是“拆分-修改-合并”。
ASM处理字节码(ByteCode)数据的思路是这样的:第一步,将.class文件拆分成多个部分;第二步,对某一个部分的信息进行修改;第三步,将多个部分重新组织成一个新的.class文件。
在Wikipedia上,对ASM进行了如下描述:ASM provides a simple API for decomposing(将一个整体拆分成多个部分), modifying(修改某一部分的信息), and recomposing(将多个部分重新组织成一个整体) binary Java classes (i.e. ByteCode).
ASM能够做什么
通俗的理解
父类:修改成一个新的父类接口:添加一个新的接口、删除已有的接口字段:添加一个新的字段、删除已有的字段方法:添加一个新的方法、删除已有的方法、修改已有的方法
专业的描述
ASM is an all-purpose(多用途的;通用的) Java ByteCode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form.The goal of the ASM library is to generate, transform and analyze compiled Java classes, represented as byte arrays (as they are stored on disk and loaded in the Java Virtual Machine).
Program analysis, which can range from a simple syntactic parsing to a full semantic analysis, can be used to find potential bugs in applications, to detect unused code, to reverse engineer code, etc.Program generation is used in compilers. This includes traditional compilers, but also stub or skeleton compilers used for distributed programming, Just in Time compilers, etc.Program transformation can be used to optimize or obfuscate programs, to insert debugging or performance monitoring code into applications, for aspect oriented programming, etc.
图示
为什么要学习ASM
ASM往往在一些框架的底层起着重要的作用。接下来,我们介绍两个关于ASM的应用场景:Spring和JDK。这两个应用场景例子的目的,就是希望大家了解到ASM的重要性。
图示
Spring当中的ASM
第一个应用场景,是Spring框架当中的AOP。 在很多Java项目中,都会使用到Spring框架,而Spring框架当中的AOP(Aspect Oriented Programming)是依赖于ASM的。具体来说,Spring的AOP,可以通过JDK的动态代理来实现,也可以通过CGLIB实现。其中,CGLib (Code Generation Library)是在ASM的基础上构建起来的,所以,Spring AOP是间接的使用了ASM。(参考自Spring Framework Reference Documentation的8.6 Proxying mechanisms)。
JDK当中的ASM
第二个应用场景,是JDK当中的Lambda表达式。 在Java 8中引入了一个非常重要的特性,就是支持Lambda表达式。Lambda表达式,允许把方法作为参数进行传递,它能够使代码变的更加简洁紧凑。但是,我们可能没有注意到,其实,在现阶段(Java 8版本),Lambda表达式的调用是通过ASM来实现的。在rt.jar文件的jdk.internal.org.objectweb.asm包当中,就包含了JDK内置的ASM代码。在JDK 8版本当中,它所使用的ASM 5.0版本。如果我们跟踪Lambda表达式的编码实现,就会找到InnerClassLambdaMetafactory.spinInnerClass()方法。在这个方法当中,我们就会看到:JDK会使用jdk.internal.org.objectweb.asm.ClassWriter来生成一个类,将lambda表达式的代码包装起来。 LambdaMetafactory.metafactory() 第一步,找到这个方法 InnerClassLambdaMetafactory.buildCallSite() 第二步,找到这个方法 InnerClassLambdaMetafactory.spinInnerClass() 第三步,找到这个方法
ASM的特点
问题:与其它的操作Java字节码的类库相比,ASM有哪些与众不同的地方呢?回答:在实现相同的功能前提下,使用ASM,运行速度更快(运行时间短,属于“时间维度”),占用的内存空间更小(内存空间,属于“空间维度”)。The ASM was designed to be as fast and as small as possible.Being as fast as possible is important in order not to slow down too much the applications that use ASM at runtime, for dynamic class generation or transformation.And being as small as possible is important in order to be used in memory constrained environments, and to avoid bloating the size of small applications or libraries using ASM.简而言之,ASM的特点就是fast和small。
ASM的组成部分
从组成结构上来说,ASM分成两部分,一部分为Core API,另一部分为Tree API。其中,Core API包括asm.jar、asm-util.jar和asm-commons.jar;其中,Tree API包括asm-tree.jar和asm-analysis.jar。从两者的关系来说,Core API是基础,而Tree API是在Core API的这个基础上构建起来的。
Core API
概览
ASM Core API概览,就是对asm.jar、asm-util.jar和asm-commons.jar文件里包含的主要类成员进行介绍。
asm.jar
在asm.jar文件中,一共包含了30多个类,我们会介绍其中10个类。那么,剩下的20多个类,为什么不介绍呢?因为剩下的20多个主要起到“辅助”的作用,它们更多的倾向于是“幕后工作者”;而“登上舞台表演的”则是属于那10个类。在“第二章”当中,我们会主要介绍从“无”到“有”生成一个新的类,其中会涉及到ClassVisitor、ClassWriter、FieldVisitor、FieldWriter、MethodVisitor、MethodWriter、Label和Opcodes类。在“第三章”当中,我们会主要介绍修改“已经存在的类”,使之内容发生改变,其中会涉及到ClassReader和Type类。在这10个类当中,最重要的是三个类,即ClassReader、ClassVisitor和ClassWriter类。这三个类的关系,可以描述成下图:
图示
这三个类的作用,可以简单理解成这样:ClassReader类,负责读取.class文件里的内容,然后拆分成各个不同的部分。ClassVisitor类,负责对.class文件中某一部分里的信息进行修改。ClassWriter类,负责将各个不同的部分重新组合成一个完整的.class文件。在“第二章”当中,主要围绕着ClassVisitor和ClassWriter这两个类展开,因为在这个部分,我们是从“无”到“有”生成一个新的类,不需要ClassReader类的参与。在“第三章”当中,就需要ClassReader、ClassVisitor和ClassWriter这三个类的共同参与。
asm-util.jar
asm-util.jar主要包含的是一些工具类。在下图当中,可以看到asm-util.jar里面包含的具体类文件。这些类主要分成两种类型:Check开头和Trace开头。 以Check开头的类,主要负责检查(Check)生成的.class文件内容是否正确。 以Trace开头的类,主要负责将.class文件的内容打印成文字输出。根据输出的文字信息,可以探索或追踪(Trace).class文件的内部信息。
图示
在asm-util.jar当中,主要介绍CheckClassAdapter类和TraceClassVisitor类,也会简略的说明一下Printer、ASMifier和Textifier类。在“第四章”当中,会介绍asm-util.jar里的内容。
asm-commons.jar
在下图当中,可以看到asm-commons.jar里面包含的具体类文件。
图示
我们会介绍到其中的AdviceAdapter、AnalyzerAdapter、ClassRemapper、GeneratorAdapter、InstructionAdapter、LocalVariableSorter、SerialVersionUIDAdapter和StaticInitMerger类。在“第四章”当中,介绍asm-commons.jar里的内容。另外,一个非常容易混淆的问题就是,asm-util.jar与asm-commons.jar有什么区别呢?在asm-util.jar里,它提供的是通用性的功能,没有特别明确的应用场景;而在asm-commons.jar里,它提供的功能,都是为解决某一种特定场景中出现的问题而提出的解决思路。
ClassVisitor
ClassVisitor介绍
在ASM Core API中,最重要的三个类就是ClassReader、ClassVisitor和ClassWriter类。在进行Class Generation操作的时候,ClassVisitor和ClassWriter这两个类起着重要作用,而并不需要ClassReader类的参与。在本文当中,我们将对ClassVisitor类进行介绍。
图示
ClassVisitor类
1.1 class info
第一个部分,ClassVisitor是一个抽象类。 由于ClassVisitor类是一个abstract类,所以不能直接使用new关键字创建ClassVisitor对象。public abstract class ClassVisitor {}同时,由于ClassVisitor类是一个abstract类,要想使用它,就必须有具体的子类来继承它。比较常见的ClassVisitor子类有ClassWriter类(Core API)和ClassNode类(Tree API)。public class ClassWriter extends ClassVisitor {}public class ClassNode extends ClassVisitor {}三个类的关系如下:org.objectweb.asm.ClassVisitor org.objectweb.asm.ClassWriter org.objectweb.asm.tree.ClassNode
1.2. fields
public abstract class ClassVisitor { protected final int api; protected ClassVisitor cv;}
api字段:它是一个int类型的数据,指出了当前使用的ASM API版本,其取值有Opcodes.ASM4、Opcodes.ASM5、Opcodes.ASM6、Opcodes.ASM7、Opcodes.ASM8和Opcodes.ASM9。我们使用的ASM版本是9.0,因此我们在给api字段赋值的时候,选择Opcodes.ASM9就可以了。cv字段:它是一个ClassVisitor类型的数据,它的作用是将多个ClassVisitor串连起来。
1.3. constructors
public abstract class ClassVisitor { public ClassVisitor(final int api) { this(api, null); } public ClassVisitor(final int api, final ClassVisitor classVisitor) { this.api = api; this.cv = classVisitor; }}
1.4. methods
第四个部分,ClassVisitor类定义的方法有哪些。在ASM当中,使用到了Visitor Pattern(访问者模式),所以ClassVisitor当中许多的visitXxx()方法。虽然,在ClassVisitor类当中,有许多visitXxx()方法,但是,我们只需要关注这4个方法:visit()、visitField()、visitMethod()和visitEnd()。为什么只关注这4个方法呢?因为这4个方法是ClassVisitor类的精髓或骨架,认识了这4个方法,其它的visitXxx()都容易扩展;同时,我们将visitXxx()方法缩小为4个,也能减少我们在学习ASM过程中的认知负担。
public abstract class ClassVisitor { public void visit( final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces); public FieldVisitor visitField( // 访问字段 final int access, final String name, final String descriptor, final String signature, final Object value); public MethodVisitor visitMethod( // 访问方法 final int access, final String name, final String descriptor, final String signature, final String[] exceptions); public void visitEnd(); // ......}
方法的调用顺序
在ClassVisitor类当中,定义了多个visitXxx()方法。这些visitXxx()方法,遵循一定的调用顺序。这个调用顺序,是参考自ClassVisitor类的API文档。
visit[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]( visitAnnotation | visitTypeAnnotation | visitAttribute)*( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod)* visitEnd
ClassWriter
class info
第一个部分,就是ClassWriter的父类是ClassVisitor,因此ClassWriter类继承了visit()、visitField()、visitMethod()和visitEnd()等方法。public class ClassWriter extends ClassVisitor {}
fields
public class ClassWriter extends ClassVisitor { private int version; private final SymbolTable symbolTable; private int accessFlags; private int thisClass; private int superClass; private int interfaceCount; private int[] interfaces; private FieldWriter firstField; private FieldWriter lastField; private MethodWriter firstMethod; private MethodWriter lastMethod; private Attribute firstAttribute; //......}
ClassWriter类使用
使用描述
使用ClassWriter生成一个Class文件,可以大致分成三个步骤:第一步,创建ClassWriter对象。第二步,调用ClassWriter对象的visitXxx()方法。第三步,调用ClassWriter对象的toByteArray()方法。
示例代码
import org.objectweb.asm.ClassWriter;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static byte[] dump () throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(); cw.visitField(); cw.visitMethod(); cw.visitEnd(); // 注意,最后要调用visitEnd()方法 // (3) 调用toByteArray()方法 byte[] bytes = cw.toByteArray(); return bytes; }}
FieldVisitor
通过调用ClassVisitor类的visitField()方法,会返回一个FieldVisitor类型的对象。在本文当中,我们就对FieldVisitor类进行介绍。
FieldVisitor类示例
示例一:字段常量
预期目标
public interface HelloWorld { int intValue = 100; String strValue = "ABC";}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.FieldVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "sample/HelloWorld", null, "java/lang/Object", null); { FieldVisitor fv1 = cw.visitField(ACC_PUBLIC | ACC_FINAL | ACC_STATIC, "intValue", "I", null, 100); fv1.visitEnd(); } { FieldVisitor fv2 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "strValue", "Ljava/lang/String;", null, "ABC"); fv2.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
示例二:visitAnnotation
预期目标
public interface HelloWorld { @MyTag(name = "tomcat", age = 10) int intValue = 100;}
public @interface MyTag { String name(); int age();}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.AnnotationVisitor;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.FieldVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE, "sample/HelloWorld", null, "java/lang/Object", null); { FieldVisitor fv1 = cw.visitField(ACC_PUBLIC | ACC_FINAL | ACC_STATIC, "intValue", "I", null, 100); { AnnotationVisitor anno = fv1.visitAnnotation("Lsample/MyTag;", false); anno.visit("name", "tomcat"); anno.visit("age", 10); anno.visitEnd(); } fv1.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
FieldWriter
FieldWriter类继承自FieldVisitor类。在ClassWriter类里,visitField()方法的实现就是通过FieldWriter类来实现的。在本文当中,我们就是介绍FieldWriter类。
class info
第一个部分,FieldWriter类的父类是FieldVisitor类。需要注意的是,FieldWriter类并不带有public修饰,因此它的有效访问范围只局限于它所处的package当中,不能像其它的public类一样被外部所使用。
final class FieldWriter extends FieldVisitor {}
fields
final class FieldWriter extends FieldVisitor { private final int accessFlags; private final int nameIndex; private final int descriptorIndex; private Attribute firstAttribute;}
MethodVisitor
通过调用ClassVisitor类的visitMethod()方法,会返回一个MethodVisitor类型的对象。在本文当中,我们就对MethodVisitor类进行介绍。
class info
public abstract class MethodVisitor {}
fields
public abstract class MethodVisitor { protected final int api; protected MethodVisitor mv;}
methods
public abstract class MethodVisitor { public void visitCode(); public void visitInsn(final int opcode); public void visitIntInsn(final int opcode, final int operand); public void visitVarInsn(final int opcode, final int var); public void visitTypeInsn(final int opcode, final String type); public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor); public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor, final boolean isInterface); public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle, final Object... bootstrapMethodArguments); public void visitJumpInsn(final int opcode, final Label label); public void visitLabel(final Label label); public void visitLdcInsn(final Object value); public void visitIincInsn(final int var, final int increment); public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels); public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels); public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions); public void visitTryCatchBlock(final Label start, final Label end, final Label handler, final String type); public void visitMaxs(final int maxStack, final int maxLocals); public void visitEnd(); //
方法的调用顺序
(visitParameter)*[visitAnnotationDefault](visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*[ visitCode ( visitFrame | visitXxxInsn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )* visitMaxs]visitEnd
这些方法的调用顺序,可以记忆如下:第一步,调用visitCode()方法,调用一次。第二步,调用visitXxxInsn()方法,可以调用多次。对这些方法的调用,就是在构建方法的“方法体”。第三步,调用visitMaxs()方法,调用一次。第四步,调用visitEnd()方法,调用一次。
MethodWriter
MethodWriter类的父类是MethodVisitor类。在ClassWriter类里,visitMethod()方法的实现就是通过MethodWriter类来实现的。在本文当中,我们就是介绍MethodWriter类。
class info
第一个部分,MethodWriter类的父类是MethodVisitor类。需要注意的是,MethodWriter类并不带有public修饰,因此它的有效访问范围只局限于它所处的package当中,不能像其它的public类一样被外部所使用。
final class MethodWriter extends MethodVisitor {}
fields
final class MethodWriter extends MethodVisitor { private final int accessFlags; private final int nameIndex; private final String name; private final int descriptorIndex; private final String descriptor; private Attribute firstAttribute;}
示例代码
1. 示例一:<init>()方法
预期目标
public class HelloWorld {}
public class HelloWorld { public HelloWorld() { super(); }}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.MethodVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(1, 1); mv1.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
2.示例二:<clinit>方法
预期目标
public class HelloWorld { static { System.out.println("class initialization method"); }}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.MethodVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(1, 1); mv1.visitEnd(); } { MethodVisitor mv2 = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null); mv2.visitCode(); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("class initialization method"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv2.visitInsn(RETURN); mv2.visitMaxs(2, 0); mv2.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
3. 示例三:创建对象
预期目标
public class HelloWorld { static { System.out.println("class initialization method"); }}
public class HelloWorld { public void test() { GoodChild child = new GoodChild("Lucy", 8); }}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.MethodVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(1, 1); mv1.visitEnd(); } { MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null); mv2.visitCode(); mv2.visitTypeInsn(NEW, "sample/GoodChild"); mv2.visitInsn(DUP); mv2.visitLdcInsn("Lucy"); mv2.visitIntInsn(BIPUSH, 8); mv2.visitMethodInsn(INVOKESPECIAL, "sample/GoodChild", "<init>", "(Ljava/lang/String;I)V", false); mv2.visitVarInsn(ASTORE, 1); mv2.visitInsn(RETURN); mv2.visitMaxs(4, 2); mv2.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
4. 示例四:调用方法
预期目标
public class HelloWorld { public void test(int a, int b) { int val = Math.max(a, b); // 对static方法进行调用 System.out.println(val); // 对non-static方法进行调用 }}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.MethodVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(1, 1); mv1.visitEnd(); } { MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(II)V", null, null); mv2.visitCode(); mv2.visitVarInsn(ILOAD, 1); mv2.visitVarInsn(ILOAD, 2); mv2.visitMethodInsn(INVOKESTATIC, "java/lang/Math", "max", "(II)I", false); mv2.visitVarInsn(ISTORE, 3); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitVarInsn(ILOAD, 3); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); mv2.visitInsn(RETURN); mv2.visitMaxs(2, 4); mv2.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
6. 示例六:不同的MethodVisitor交叉使用
说明
假如我们有两个MethodVisitor对象mv1和mv2,如下所示:MethodVisitor mv1 = cw.visitMethod(...);MethodVisitor mv2 = cw.visitMethod(...);同时,我们也知道MethodVisitor类里的visitXxx()方法需要遵循一定的调用顺序:第一步,调用visitCode()方法,调用一次第二步,调用visitXxxInsn()方法,可以调用多次第三步,调用visitMaxs()方法,调用一次第四步,调用visitEnd()方法,调用一次对于mv1和mv2这两个对象来说,它们的visitXxx()方法的调用顺序是彼此独立的、不会相互干扰。一般情况下,我们可以如下写代码,这样逻辑比较清晰:MethodVisitor mv1 = cw.visitMethod(...);mv1.visitCode(...);mv1.visitXxxInsn(...)mv1.visitMaxs(...);mv1.visitEnd();MethodVisitor mv2 = cw.visitMethod(...);mv2.visitCode(...);mv2.visitXxxInsn(...)mv2.visitMaxs(...);mv2.visitEnd();但是,我们也可以这样来写代码:MethodVisitor mv1 = cw.visitMethod(...);MethodVisitor mv2 = cw.visitMethod(...);mv1.visitCode(...);mv2.visitCode(...);mv2.visitXxxInsn(...)mv1.visitXxxInsn(...)mv1.visitMaxs(...);mv1.visitEnd();mv2.visitMaxs(...);mv2.visitEnd();在上面的代码中,mv1和mv2这两个对象的visitXxx()方法交叉调用,这是可以的。换句话说,只要每一个MethodVisitor对象在调用visitXxx()方法时,遵循了调用顺序,那结果就是正确的;不同的MethodVisitor对象,是相互独立的、不会彼此影响。那么,可能有的同学会问:MethodVisitor对象交叉使用有什么作用呢?有没有什么场景下的应用呢?回答是“有的”。在ASM当中,有一个org.objectweb.asm.commons.StaticInitMerger类,类当中有一个MethodVisitor mergedClinitVisitor,它就是一个很好的示例,在后续内容中,我们会介绍到这个类。
预期目标
import java.util.Date;public class HelloWorld { public void test() { System.out.println("This is a test method."); } public void printDate() { Date now = new Date(); System.out.println(now); }}
编码实现(第一种方式,顺序)
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.MethodVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(1, 1); mv1.visitEnd(); } { MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null); mv2.visitCode(); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("This is a test method."); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv2.visitInsn(RETURN); mv2.visitMaxs(2, 1); mv2.visitEnd(); } { MethodVisitor mv3 = cw.visitMethod(ACC_PUBLIC, "printDate", "()V", null, null); mv3.visitCode(); mv3.visitTypeInsn(NEW, "java/util/Date"); mv3.visitInsn(DUP); mv3.visitMethodInsn(INVOKESPECIAL, "java/util/Date", "<init>", "()V", false); mv3.visitVarInsn(ASTORE, 1); mv3.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv3.visitVarInsn(ALOAD, 1); mv3.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false); mv3.visitInsn(RETURN); mv3.visitMaxs(2, 2); mv3.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
编码实现(第二种方式,交叉)
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.MethodVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(1, 1); mv1.visitEnd(); } { // 第1部分,mv2 MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null); // 第2部分,mv3 MethodVisitor mv3 = cw.visitMethod(ACC_PUBLIC, "printDate", "()V", null, null); mv3.visitCode(); mv3.visitTypeInsn(NEW, "java/util/Date"); mv3.visitInsn(DUP); mv3.visitMethodInsn(INVOKESPECIAL, "java/util/Date", "<init>", "()V", false); // 第3部分,mv2 mv2.visitCode(); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("This is a test method."); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // 第4部分,mv3 mv3.visitVarInsn(ASTORE, 1); mv3.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv3.visitVarInsn(ALOAD, 1); mv3.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false); // 第5部分,mv2 mv2.visitInsn(RETURN); mv2.visitMaxs(2, 1); mv2.visitEnd(); // 第6部分,mv3 mv3.visitInsn(RETURN); mv3.visitMaxs(2, 2); mv3.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
Label
介绍
在程序中,有三种基本控制结构:顺序、选择和循环。我们现在已经知道,MethodVisitor类是用于生成方法体的代码。如果没有Label类的参与,那么MethodVisitor类只能生成“顺序”结构的代码;如果有Label类的参与,MethodVisitor类就能生成“选择”和“循环”结构的代码。在本文当中,我们来介绍Label类。如果查看Label类的API文档,就会发现下面的描述,分成了三个部分。第一部分,Label类上是什么(What);第二部分,在哪些用到Label类(Where);第三部分,在编写ASM代码过程中,如何使用Label类(How),或者说,Label类与Instruction的关系。A position in the bytecode of a method.Labels are used for jump, goto, and switch instructions, and for try catch blocks.A label designates the instruction that is just after. Note however that there can be other elements between a label and the instruction it designates (such as other labels, stack map frames, line numbers, etc.).如果是刚刚接触Label类,那么可能对于上面的三部分英文描述没有太多的“感受”或“理解”;但是,如果接触Label类一段时间之后,就会发现它描述的内容很“精髓”。本文的内容也是围绕着这三部分来展开的。
Label类
在Label类当中,定义了很多的字段和方法。为了方便,将Label类简化一下,内容如下:public class Label { int bytecodeOffset; public Label() { // Nothing to do. } public int getOffset() { return bytecodeOffset; }
Label类能够做什么?
在ASM当中,Label类可以用于实现选择(if、switch)、循环(for、while)和try-catch语句。在编写ASM代码的过程中,我们所要表达的是一种代码的跳转逻辑,就是从一个地方跳转到另外一个地方;在这两者之间,可以编写其它的代码逻辑,可能长一些,也可能短一些,所以,Instruction所对应的“索引值”还不确定。Label类的出现,就是代表一个“抽象的位置”,也就是将来要跳转的目标。当我们调用ClassWriter.toByteArray()方法时,这些ASM代码会被转换成byte[],在这个过程中,需要计算出Label对象中bytecodeOffset字段的值到底是多少,从而再进一步计算出跳转的相对偏移量(offset)。
Label代码示例
0. 示例0
预期目标
public class HelloWorld { public void test(boolean flag) { if (flag) { System.out.println("value is true"); } else { System.out.println("value is false"); } }}
代码实现
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "test", "(Z)V", null, null);Label elseLabel = new Label(); // 首先,准备两个Label对象Label returnLabel = new Label();// 第1段mv.visitCode();mv.visitVarInsn(ILOAD, 1);mv.visitJumpInsn(IFEQ, elseLabel);mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("value is true");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitJumpInsn(GOTO, returnLabel);// 第2段mv.visitLabel(elseLabel); // 将第一个Label放到这里mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("value is false");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);// 第3段mv.visitLabel(returnLabel); // 将第二个Label放到这里mv.visitInsn(RETURN);mv.visitMaxs(2, 2);mv.visitEnd();
1. 示例一:if语句
预期目标
public class HelloWorld { public void test(int value) { if (value == 0) { System.out.println("value is 0"); } else { System.out.println("value is not 0"); } }}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.*;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(0, 0); mv1.visitEnd(); } { MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null); Label elseLabel = new Label(); Label returnLabel = new Label(); // 第1段 mv2.visitCode(); mv2.visitVarInsn(ILOAD, 1); mv2.visitJumpInsn(IFNE, elseLabel); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("value is 0"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv2.visitJumpInsn(GOTO, returnLabel); // 第2段 mv2.visitLabel(elseLabel); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("value is not 0"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // 第3段 mv2.visitLabel(returnLabel); mv2.visitInsn(RETURN); mv2.visitMaxs(0, 0); mv2.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
2. 示例二:switch语句
预期目标
public class HelloWorld { public void test(int val) { switch (val) { case 1: System.out.println("val = 1"); break; case 2: System.out.println("val = 2"); break; case 3: System.out.println("val = 3"); break; case 4: System.out.println("val = 4"); break; default: System.out.println("val is unknown"); } }}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.*;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(0, 0); mv1.visitEnd(); } { MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null); Label caseLabel1 = new Label(); Label caseLabel2 = new Label(); Label caseLabel3 = new Label(); Label caseLabel4 = new Label(); Label defaultLabel = new Label(); Label returnLabel = new Label(); // 第1段 mv2.visitCode(); mv2.visitVarInsn(ILOAD, 1); mv2.visitTableSwitchInsn(1, 4, defaultLabel, new Label[]{caseLabel1, caseLabel2, caseLabel3, caseLabel4}); // 第2段 mv2.visitLabel(caseLabel1); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("val = 1"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv2.visitJumpInsn(GOTO, returnLabel); // 第3段 mv2.visitLabel(caseLabel2); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("val = 2"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv2.visitJumpInsn(GOTO, returnLabel); // 第4段 mv2.visitLabel(caseLabel3); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("val = 3"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv2.visitJumpInsn(GOTO, returnLabel); // 第5段 mv2.visitLabel(caseLabel4); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("val = 4"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv2.visitJumpInsn(GOTO, returnLabel); // 第6段 mv2.visitLabel(defaultLabel); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("val is unknown"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // 第7段 mv2.visitLabel(returnLabel); mv2.visitInsn(RETURN); mv2.visitMaxs(0, 0); mv2.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
3. 示例三:for语句
预期目标
public class HelloWorld { public void test() { for (int i = 0; i < 10; i++) { System.out.println(i); } }}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.*;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(0, 0); mv1.visitEnd(); } { MethodVisitor methodVisitor = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null); Label conditionLabel = new Label(); Label returnLabel = new Label(); // 第1段 methodVisitor.visitCode(); methodVisitor.visitInsn(ICONST_0); methodVisitor.visitVarInsn(ISTORE, 1); // 第2段 methodVisitor.visitLabel(conditionLabel); methodVisitor.visitVarInsn(ILOAD, 1); methodVisitor.visitIntInsn(BIPUSH, 10); methodVisitor.visitJumpInsn(IF_ICMPGE, returnLabel); methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); methodVisitor.visitVarInsn(ILOAD, 1); methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); methodVisitor.visitIincInsn(1, 1); methodVisitor.visitJumpInsn(GOTO, conditionLabel); // 第3段 methodVisitor.visitLabel(returnLabel); methodVisitor.visitInsn(RETURN); methodVisitor.visitMaxs(0, 0); methodVisitor.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
4. 示例四:try-catch语句
预期目标
public class HelloWorld { public void test() { try { System.out.println("Before Sleep"); Thread.sleep(1000); System.out.println("After Sleep"); } catch (InterruptedException e) { e.printStackTrace(); } }}
编码实现
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Label;import org.objectweb.asm.MethodVisitor;import static org.objectweb.asm.Opcodes.*;public class HelloWorldGenerateCore { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); // (1) 生成byte[]内容 byte[] bytes = dump(); // (2) 保存byte[]到文件 FileUtils.writeBytes(filepath, bytes); } public static byte[] dump() throws Exception { // (1) 创建ClassWriter对象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // (2) 调用visitXxx()方法 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null); { MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv1.visitCode(); mv1.visitVarInsn(ALOAD, 0); mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv1.visitInsn(RETURN); mv1.visitMaxs(0, 0); mv1.visitEnd(); } { MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null); Label startLabel = new Label(); Label endLabel = new Label(); Label exceptionHandlerLabel = new Label(); Label returnLabel = new Label(); // 第1段 mv2.visitCode(); // visitTryCatchBlock可以在这里访问 mv2.visitTryCatchBlock(startLabel, endLabel, exceptionHandlerLabel, "java/lang/InterruptedException"); // 第2段 mv2.visitLabel(startLabel); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("Before Sleep"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv2.visitLdcInsn(new Long(1000L)); mv2.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false); mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv2.visitLdcInsn("After Sleep"); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // 第3段 mv2.visitLabel(endLabel); mv2.visitJumpInsn(GOTO, returnLabel); // 第4段 mv2.visitLabel(exceptionHandlerLabel); mv2.visitVarInsn(ASTORE, 1); mv2.visitVarInsn(ALOAD, 1); mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false); // 第5段 mv2.visitLabel(returnLabel); mv2.visitInsn(RETURN); // 第6段 // visitTryCatchBlock也可以在这里访问 // mv2.visitTryCatchBlock(startLabel, endLabel, exceptionHandlerLabel, "java/lang/InterruptedException"); mv2.visitMaxs(0, 0); mv2.visitEnd(); } cw.visitEnd(); // (3) 调用toByteArray()方法 return cw.toByteArray(); }}
ClassReader
介绍
ClassReader类和ClassWriter类,从功能角度来说,是完全相反的两个类,一个用于读取.class文件,另一个用于生成.class文件。
class info
第一个部分,ClassReader的父类是Object类。与ClassWriter类不同的是,ClassReader类并没有继承自ClassVisitor类。ClassReader类的定义如下:public class ClassReader {}ClassWriter类的定义如下:public class ClassWriter extends ClassVisitor {}
fields
第二个部分,ClassReader类定义的字段有哪些。我们选取出其中的3个字段进行介绍,即classFileBuffer字段、cpInfoOffsets字段和header字段。public class ClassReader { //第1组,真实的数据部分 final byte[] classFileBuffer; //第2组,数据的索引信息 private final int[] cpInfoOffsets; public final int header;}为什么选择这3个字段呢?因为这3个字段能够体现出ClassReader类处理.class文件的整体思路:第1组,classFileBuffer字段:它里面包含的信息,就是从.class文件中读取出来的字节码数据。第2组,cpInfoOffsets字段和header字段:它们分别标识了classFileBuffer中数据里包含的常量池(constant pool)和访问标识(access flag)的位置信息。我们拿到classFileBuffer字段后,一个主要目的就是对它的内容进行修改,来实现一个新的功能。它处理的大体思路是这样的:.class文件 --> ClassReader --> byte[] --> 经过各种转换 --> ClassWriter --> byte[] --> .class文件第一,从一个.class文件(例如HelloWorld.class)开始,它可能存储于磁盘的某个位置;第二,使用ClassReader类将这个.class文件的内容读取出来,其实这些内容(byte[])就是ClassReader对象中的classFileBuffer字段的内容;第三,为了增加某些功能,就对这些原始内容(byte[])进行转换;第四,等各种转换都完成之后,再交给ClassWriter类处理,调用它的toByteArray()方法,从而得到新的内容(byte[]);第五,将新生成的内容(byte[])存储到一个具体的.class文件中,那么这个新的.class文件就具备了一些新的功能。
accept方法
在ClassReader类当中,有一个accept()方法,这个方法接收一个ClassVisitor类型的参数,因此accept()方法是将ClassReader和ClassVisitor进行连接的“桥梁”。accept()方法的代码逻辑就是按照一定的顺序来调用ClassVisitor当中的visitXxx()方法。
使用
The ASM core API for generating and transforming compiled Java classes is based on the ClassVisitor abstract class.
图示
使用流程
我们可以将整体的处理流程想像成一条河流,那么第一步,构建ClassReader。生成的ClassReader对象,它是这条“河流”的“源头”。第二步,构建ClassWriter。生成的ClassWriter对象,它是这条“河流”的“归处”,它可以想像成是“百川东到海”中的“大海”。第三步,串连ClassVisitor。生成的ClassVisitor对象,它是这条“河流”上的重要节点,可以想像成一个“水库”;可以有多个ClassVisitor对象,也就是在这条“河流”上存在多个“水库”,这些“水库”可以对“河水”进行一些处理,最终会这些“水库”的水会流向“大海”;也就是说多个ClassVisitor对象最终会连接到ClassWriter对象上。第四步,结合ClassReader和ClassVisitor。在ClassReader类上,有一个accept()方法,它接收一个ClassVisitor类型的对象;换句话说,就是将“河流”的“源头”和后续的“水库”连接起来。第五步,生成byte[]。到这一步,就是所有的“河水”都流入ClassWriter这个“大海”当中,这个时候我们调用ClassWriter.toByteArray()方法,就能够得到byte[]内容。
图示
parsingOptions参数
在ClassReader类当中,accept()方法接收一个int类型的parsingOptions参数。public void accept(final ClassVisitor classVisitor, final int parsingOptions)parsingOptions参数可以选取的值有以下5个:0ClassReader.SKIP_CODEClassReader.SKIP_DEBUGClassReader.SKIP_FRAMESClassReader.EXPAND_FRAMES推荐使用:在调用ClassReader.accept()方法时,其中的parsingOptions参数,推荐使用ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES。在创建ClassWriter对象时,其中的flags参数,推荐使用ClassWriter.COMPUTE_FRAMES。示例代码如下:ClassReader cr = new ClassReader(bytes);int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;cr.accept(cv, parsingOptions);ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);为什么我们推荐使用ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES呢?因为使用这样的一个值,可以生成最少的ASM代码,但是又能实现完整的功能。0:会生成所有的ASM代码,包括调试信息、frame信息和代码信息。ClassReader.SKIP_CODE:会忽略代码信息,例如,会忽略对于MethodVisitor.visitXxxInsn()方法的调用。ClassReader.SKIP_DEBUG:会忽略调试信息,例如,会忽略对于MethodVisitor.visitParameter()、MethodVisitor.visitLineNumber()和MethodVisitor.visitLocalVariable()等方法的调用。ClassReader.SKIP_FRAMES:会忽略frame信息,例如,会忽略对于MethodVisitor.visitFrame()方法的调用。ClassReader.EXPAND_FRAMES:会对frame信息进行扩展,例如,会对MethodVisitor.visitFrame()方法的参数有影响。对于这些参数的使用,我们可以在ASMPrint类的基础上进行实验。我们使用ClassReader.SKIP_DEBUG的时候,就不会生成调试信息。因为这些调试信息主要是记录某一条instruction在代码当中的行数,以及变量的名字等信息;如果没有这些调试信息,也不会影响程序的正常运行,也就是说功能不受影响,因此省略这些信息,就会让ASM代码尽可能的简洁。我们使用ClassReader.SKIP_FRAMES的时候,就会忽略frame的信息。为什么要忽略这些frame信息呢?因为frame计算的细节会很繁琐,需要处理的情况也有很多,总的来说,就是比较麻烦。我们解决这个麻烦的方式,就是让ASM帮助我们来计算frame的情况,也就是在创建ClassWriter对象的时候使用ClassWriter.COMPUTE_FRAMES选项。在刚开始学习ASM的时候,对于parsingOptions参数,我们推荐使用ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES的组合值。但是,以后,随着大家对ASM的知识越来越熟悉,或者随着功能需求的变化,大家可以尝试着使用其它的选项值。
示例代码
示例0 演示
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;public class HelloWorldTransformCore { public static void main(String[] args) { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); byte[] bytes1 = FileUtils.readBytes(filepath); //(1)构建ClassReader ClassReader cr = new ClassReader(bytes1); //(2)构建ClassWriter ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //(3)串连ClassVisitor int api = Opcodes.ASM9; ClassVisitor cv = new ClassVisitor(api, cw) { /**/ }; //(4)结合ClassReader和ClassVisitor int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cv, parsingOptions); //(5)生成byte[] byte[] bytes2 = cw.toByteArray(); FileUtils.writeBytes(filepath, bytes2); }}
代码的整体处理流程是如下这样的:.class --> ClassReader --> ClassVisitor1 ... --> ClassVisitorN --> ClassWriter --> .class文件
2.1. 示例一:修改类的版本
预期目标
假如有一个HelloWorld.java文件,经过Java 8编译之后,生成的HelloWorld.class文件的版本就是Java 8的版本,我们的目标是将HelloWorld.class由Java 8版本转换成Java 7版本。public class HelloWorld {}
编码实现
import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.Opcodes;public class ClassChangeVersionVisitor extends ClassVisitor { public ClassChangeVersionVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(Opcodes.V1_7, access, name, signature, superName, interfaces); }}
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;public class HelloWorldTransformCore { public static void main(String[] args) { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); byte[] bytes1 = FileUtils.readBytes(filepath); //(1)构建ClassReader ClassReader cr = new ClassReader(bytes1); //(2)构建ClassWriter ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //(3)串连ClassVisitor int api = Opcodes.ASM9; ClassVisitor cv = new ClassChangeVersionVisitor(api, cw); //(4)结合ClassReader和ClassVisitor int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cv, parsingOptions); //(5)生成byte[] byte[] bytes2 = cw.toByteArray(); FileUtils.writeBytes(filepath, bytes2); }}
2.2. 示例二:修改类的接口
预期目标
在下面的HelloWorld类中,我们定义了一个clone()方法,但存在一个问题,也就是,如果没有实现Cloneable接口,clone()方法就会出错,我们的目标是希望通过ASM为HelloWorld类添加上Cloneable接口。
public class HelloWorld { @Override public Object clone() throws CloneNotSupportedException { return super.clone(); }}
编码实现
import org.objectweb.asm.ClassVisitor;public class ClassCloneVisitor extends ClassVisitor { public ClassCloneVisitor(int api, ClassVisitor cw) { super(api, cw); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, new String[]{"java/lang/Cloneable"}); }}
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;public class HelloWorldTransformCore { public static void main(String[] args) { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); byte[] bytes1 = FileUtils.readBytes(filepath); //(1)构建ClassReader ClassReader cr = new ClassReader(bytes1); //(2)构建ClassWriter ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //(3)串连ClassVisitor int api = Opcodes.ASM9; ClassVisitor cv = new ClassCloneVisitor(api, cw); //(4)结合ClassReader和ClassVisitor int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cv, parsingOptions); //(5)生成byte[] byte[] bytes2 = cw.toByteArray(); FileUtils.writeBytes(filepath, bytes2); }}
小结
我们看到上面的两个例子,一个是修改类的版本信息,另一个是修改类的接口信息,那么这两个示例都是基于ClassVisitor.visit()方法实现的: public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)这两个示例,就是通过修改visit()方法的参数实现的: 修改类的版本信息,是通过修改version这个参数实现的 修改类的接口信息,是通过修改interfaces这个参数实现的其实,在visit()方法当中的其它参数也可以修改: 修改access参数,也就是修改了类的访问标识信息。 修改name参数,也就是修改了类的名称。但是,在大多数的情况下,不推荐修改name参数。因为调用类里的方法,都是先找到类,再找到相应的方法;如果将当前 类的类名修改成别的名称,那么其它类当中可能就找不到原来的方法了,因为类名已经改了。但是,也有少数的情况,可以修改name参数,比如说对代码进行混淆(obfuscate)操作。 修改superName参数,也就是修改了当前类的父类信息。
3.1. 示例三:删除字段
预期目标
删除掉HelloWorld类里的String strValue字段。public class HelloWorld { public int intValue; public String strValue; // 删除这个字段}
子主题
编码实现
import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.FieldVisitor;public class ClassRemoveFieldVisitor extends ClassVisitor { private final String fieldName; private final String fieldDesc; public ClassRemoveFieldVisitor(int api, ClassVisitor cv, String fieldName, String fieldDesc) { super(api, cv); this.fieldName = fieldName; this.fieldDesc = fieldDesc; } @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { if (name.equals(fieldName) && descriptor.equals(fieldDesc)) { return null; } return super.visitField(access, name, descriptor, signature, value); }}
上面代码思路的关键就是ClassVisitor.visitField()方法。在正常的情况下,ClassVisitor.visitField()方法返回一个FieldVisitor对象;但是,如果ClassVisitor.visitField()方法返回的是null,就么能够达到删除该字段的效果。我们之前说过一个形象的类比,就是将ClassReader类比喻成河流的“源头”,而ClassVisitor类比喻成河流的经过的路径上的“水库”,而ClassWriter类则比喻成“大海”,也就是河水的最终归处。如果说,其中一个“水库”拦截了一部分水流,那么这部分水流就到不了“大海”了;这就相当于ClassVisitor.visitField()方法返回的是null,从而能够达到删除该字段的效果。。或者说,换一种类比,用信件的传递作类比。将ClassReader类想像成信件的“发出地”,将ClassVisitor类想像成信件运送途中经过的“驿站”,将ClassWriter类想像成信件的“接收地”;如果是在某个“驿站”中将其中一封邮件丢失了,那么这封信件就抵达不了“接收地”了。
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;public class HelloWorldTransformCore { public static void main(String[] args) { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); byte[] bytes1 = FileUtils.readBytes(filepath); //(1)构建ClassReader ClassReader cr = new ClassReader(bytes1); //(2)构建ClassWriter ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //(3)串连ClassVisitor int api = Opcodes.ASM9; ClassVisitor cv = new ClassRemoveFieldVisitor(api, cw, "strValue", "Ljava/lang/String;"); //(4)结合ClassReader和ClassVisitor int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cv, parsingOptions); //(5)生成byte[] byte[] bytes2 = cw.toByteArray(); FileUtils.writeBytes(filepath, bytes2); }}
3.2. 示例四:添加字段
预期目标
为了HelloWorld类添加一个Object objValue字段。public class HelloWorld { public int intValue; public String strValue; // 添加一个Object objValue字段}
编码实现
import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.FieldVisitor;public class ClassAddFieldVisitor extends ClassVisitor { private final int fieldAccess; private final String fieldName; private final String fieldDesc; private boolean isFieldPresent; public ClassAddFieldVisitor(int api, ClassVisitor classVisitor, int fieldAccess, String fieldName, String fieldDesc) { super(api, classVisitor); this.fieldAccess = fieldAccess; this.fieldName = fieldName; this.fieldDesc = fieldDesc; } @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { if (name.equals(fieldName)) { isFieldPresent = true; } return super.visitField(access, name, descriptor, signature, value); } @Override public void visitEnd() { if (!isFieldPresent) { FieldVisitor fv = cv.visitField(fieldAccess, fieldName, fieldDesc, null, null); if (fv != null) { fv.visitEnd(); } } super.visitEnd(); }}
上面的代码思路:第一步,在visitField()方法中,判断某个字段是否已经存在,其结果存在于isFieldPresent字段当中;第二步,就是在visitEnd()方法中,根据isFieldPresent字段的值,来决定是否添加新的字段。
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;public class HelloWorldTransformCore { public static void main(String[] args) { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); byte[] bytes1 = FileUtils.readBytes(filepath); //(1)构建ClassReader ClassReader cr = new ClassReader(bytes1); //(2)构建ClassWriter ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //(3)串连ClassVisitor int api = Opcodes.ASM9; ClassVisitor cv = new ClassAddFieldVisitor(api, cw, Opcodes.ACC_PUBLIC, "objValue", "Ljava/lang/Object;"); //(4)结合ClassReader和ClassVisitor int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cv, parsingOptions); //(5)生成byte[] byte[] bytes2 = cw.toByteArray(); FileUtils.writeBytes(filepath, bytes2); }}
小总结
对于字段的操作,都是基于ClassVisitor.visitField()方法来实现的:public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value);那么,对于字段来说,可以进行哪些操作呢?有三种类型的操作: 修改现有的字段。例如,修改字段的名字、修改字段的类型、修改字段的访问标识,这些需要通过修改visitField()方法的参数来实现。 删除已有的字段。在visitField()方法中,返回null值,就能够达到删除字段的效果。 添加新的字段。在visitField()方法中,判断该字段是否已经存在;在visitEnd()方法中,如果该字段不存在,则添加新字段。一般情况下来说,不推荐“修改已有的字段”,也不推荐“删除已有的字段”,原因如下:不推荐“修改已有的字段”,因为这可能会引起字段的名字不匹配、字段的类型不匹配,从而导致程序报错。例如,假如在HelloWorld类里有一个intValue字段,而且GoodChild类里也使用到了HelloWorld类的这个intValue字段;如果我们将HelloWorld类里的intValue字段名字修改为myValue,那么GoodChild类就再也找不到intValue字段了,这个时候,程序就会出错。当然,如果我们把GoodChild类里对于intValue字段的引用修改成myValue,那也不会出错了。但是,我们要保证所有使用intValue字段的地方,都要进行修改,这样才能让程序不报错。不推荐“删除已有的字段”,因为一般来说,类里的字段都是有作用的,如果随意的删除就会造成字段缺失,也会导致程序报错。为什么不在ClassVisitor.visitField()方法当中来添加字段呢?如果在ClassVisitor.visitField()方法,就可能添加重复的字段,这样就不是一个合法的ClassFile了。
4.1. 示例五:删除方法
预期目标
删除掉HelloWorld类里的add()方法。public class HelloWorld { public int add(int a, int b) { // 删除add方法 return a + b; } public int sub(int a, int b) { return a - b; }}
编码实现
import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.MethodVisitor;public class ClassRemoveMethodVisitor extends ClassVisitor { private final String methodName; private final String methodDesc; public ClassRemoveMethodVisitor(int api, ClassVisitor cv, String methodName, String methodDesc) { super(api, cv); this.methodName = methodName; this.methodDesc = methodDesc; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { if(name.equals(methodName) && descriptor.equals(methodDesc)) { return null; } return super.visitMethod(access, name, descriptor, signature, exceptions); }}
import lsieun.utils.FileUtils;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;public class HelloWorldTransformCore { public static void main(String[] args) { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); byte[] bytes1 = FileUtils.readBytes(filepath); //(1)构建ClassReader ClassReader cr = new ClassReader(bytes1); //(2)构建ClassWriter ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //(3)串连ClassVisitor int api = Opcodes.ASM9; ClassVisitor cv = new ClassRemoveMethodVisitor(api, cw, "add", "(II)I"); //(4)结合ClassReader和ClassVisitor int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cv, parsingOptions); //(5)生成byte[] byte[] bytes2 = cw.toByteArray(); FileUtils.writeBytes(filepath, bytes2); }}
4.2. 示例六:添加方法
预期目标
为HelloWorld类添加一个mul()方法。public class HelloWorld { public int add(int a, int b) { return a + b; } public int sub(int a, int b) { return a - b; } // TODO: 添加一个乘法}
编码实现
import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.MethodVisitor;public abstract class ClassAddMethodVisitor extends ClassVisitor { private final int methodAccess; private final String methodName; private final String methodDesc; private final String methodSignature; private final String[] methodExceptions; private boolean isMethodPresent; public ClassAddMethodVisitor(int api, ClassVisitor cv, int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) { super(api, cv); this.methodAccess = methodAccess; this.methodName = methodName; this.methodDesc = methodDesc; this.methodSignature = signature; this.methodExceptions = exceptions; this.isMethodPresent = false; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { if (name.equals(methodName) && descriptor.equals(methodDesc)) { isMethodPresent = true; } return super.visitMethod(access, name, descriptor, signature, exceptions); } @Override public void visitEnd() { if (!isMethodPresent) { MethodVisitor mv = super.visitMethod(methodAccess, methodName, methodDesc, methodSignature, methodExceptions); if (mv != null) { // create method body generateMethodBody(mv); } } super.visitEnd(); } protected abstract void generateMethodBody(MethodVisitor mv);}
import lsieun.utils.FileUtils;import org.objectweb.asm.*;import static org.objectweb.asm.Opcodes.*;public class HelloWorldTransformCore { public static void main(String[] args) { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); byte[] bytes1 = FileUtils.readBytes(filepath); //(1)构建ClassReader ClassReader cr = new ClassReader(bytes1); //(2)构建ClassWriter ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //(3)串连ClassVisitor int api = Opcodes.ASM9; ClassVisitor cv = new ClassAddMethodVisitor(api, cw, Opcodes.ACC_PUBLIC, "mul", "(II)I", null, null) { @Override protected void generateMethodBody(MethodVisitor mv) { mv.visitCode(); mv.visitVarInsn(ILOAD, 1); mv.visitVarInsn(ILOAD, 2); mv.visitInsn(IMUL); mv.visitInsn(IRETURN); mv.visitMaxs(2, 3); mv.visitEnd(); } }; //(4)结合ClassReader和ClassVisitor int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cv, parsingOptions); //(5)生成byte[] byte[] bytes2 = cw.toByteArray(); FileUtils.writeBytes(filepath, bytes2); }}
小总结
对于方法的操作,都是基于ClassVisitor.visitMethod()方法来实现的:public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions);与字段操作类似,对于方法来说,可以进行的操作也有三种类型:修改现有的方法。删除已有的方法。添加新的方法。我们不推荐“删除已有的方法”,因为这可能会引起方法调用失败,从而导致程序报错。另外,对于“修改现有的方法”,我们不建议修改方法的名称、方法的类型(接收参数的类型和返回值的类型),因为别的地方可能会对该方法进行调用,修改了方法名或方法的类型,就会使方法调用失败。但是,我们可以“修改现有方法”的“方法体”,也就是方法的具体实现代码。
Class Transformation的原理
Class-Reader/Visitor/Writer
我们使用ClassReader、ClassVisitor和ClassWriter类来进行Class Transformation操作的整体思路是这样的:ClassReader --> ClassVisitor(1) --> ... --> ClassVisitor(N) --> ClassWriter其中,ClassReader类负责“读”Class,ClassWriter负责“写”Class,而ClassVisitor则负责进行“转换”(Transformation)。在Class Transformation过程中,可以有多个ClassVisitor参与。不过要注意,ClassVisitor类是一个抽象类,我们需要写代码来实现一个ClassVisitor类的子类才能使用。
Tree API
图示
ASM与ClassFile
ClassFile
我们都知道,在.class文件中,存储的是ByteCode数据。但是,这些ByteCode数据并不是杂乱无章的,而是遵循一定的数据结构。这个.class文件遵循的数据结构就是由Java Virtual Machine Specification中定义的 The class File Format,如下所示。
结构字段
1. Java ClassFile对于一个具体的.class而言,它是遵循ClassFile结构的。这个数据结构位于Java Virtual Machine Specification的 The class File Format部分。u1: 表示占用1个字节u2: 表示占用2个字节u4: 表示占用4个字节u8: 表示占用8个字节
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count];}
cp_info
field_info
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count];}
method_info
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count];}
Code_attribute
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count];}
ASM学习层次
学习ASM有三个不同的层次:第一个层次,ASM的应用层面。也就是说,我们可以使用ASM来做什么呢?对于一个.class文件来说,我们可以使用ASM进行analysis、generation和transformation操作。第二个层次,ASM的源码层面。也就是,ASM的代码组织形式,它为分Core API和Tree API的内容。第三个层次,Java ClassFile层面。从JVM规范的角度,来理解.class文件的结构,来理解ASM中方法和参数的含义。
图示
常见的字节码类库
Apache Commons BCEL:其中BCEL为Byte Code Engineering Library首字母的缩写。Javassist:Javassist表示Java programming assistantObjectWeb ASM:本课程的研究对象。Byte Buddy:在ASM基础上实现的一个类库。
运行时数据区Running Time Data Area
java运行时内存结构
内存结构概述
结构图示
图示1
图示2
程序计数器
PC 寄存器介绍
1.JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。2.这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。3.它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。4.在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。5.任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned)。6.它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。7.它是唯一一个在Java虚拟机规范中没有规定任何OutofMemoryError情况的区域。
PC 寄存器的作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。
两个常见问题
使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么被设定为私有的?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
注意
pc寄存器只保存执行引擎将要提取的下一条指令的地址,不保留当前指令地址cpu一个核只能执行一个线程,不断地切换线程来执行线程并发交替起来看上去像并行
图示
虚拟机栈
介绍
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。(和第一章的jvm简介相对应)
作用
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。局部变量,它是相比于成员变量来说的(或属性)基本数据类型变量 VS 引用类型变量(类、数组、接口)
栈中可能出现的异常
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError异常。如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个outofMemoryError 异常。
设置栈内存大小
-Xss1m-Xss1k
栈的运行原理
栈运行原理1
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。执行引擎运行的所有字节码指令只针对当前栈帧进行操作。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
栈运行原理2
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。(默认是return;)
栈的内部结构
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈帧的内部结构
局部变量表(Local Variables)
定义
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
局部变量表:Local Variables,被称之为局部变量数组或本地变量表定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
变量槽(Variable Slot)
说明
局部变量表的容量以变量槽为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。
关于Slot的理解
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。局部变量表,最基本的存储单元是Slot(变量槽)局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。long和double则占据两个slot。
JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。(staic方法不能调用this,因为this没办法存在当前方法的局部变量表中)
Slot的重复利用
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕),那这个变量对应的Slot就可以交给其他变量使用。优点 : 节省栈帧空间。缺点 : 影响到系统的垃圾收集行为。(如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。)
举例
public void abc5() { int a = 0; { int b = 0; b = a + 1; } int c = a + 1; }
reference
reference(对象实例的引用)一般来说,虚拟机都能从引用中直接或者间接的查找到对象的以下两点 :①在Java堆中的数据存放的起始地址索引。②所属数据类型在方法区中的存储的类型数据。
数据大小
补充说明
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈(operand Stack)(或表达式栈)
概念
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈比如:执行复制、交换、求和等操作
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。这个时候数组是有长度的,因为数组一旦创建,那么就是不可变的
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。栈中的任何一个元素都是可以任意的Java数据类型– 32bit的类型占用一个栈单位深度– 64bit的类型占用两个栈单位深度操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。|另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
栈顶嗳存技术
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
介绍
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支付方法调用过程中的动态连接(Dynamic Linking)。在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。
为什么需要运行时常量池?因为在不同的方法,都可能调用常量或者方法,所以只需要统一存储常量池即可,节省了空间。常量池的作用:就是为了提供一些符号和常量,便于指令的识别
图示
方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
介绍
作用:存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常执行完成出现未处理的异常,非正常退出无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
退出方法的两种方式
正常完成出口
执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定在字节码指令中,返回指令包含ireturn(当返回值是boolena、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn(引用类型的)另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用
异常完成出口
在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜素到匹配的异常处理器,就会导致方法退出,简称异常完成出口方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
一些附加信息(局部变量表和操作数栈主要影响栈帧的大小)
一些附加信息栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
逃逸分析、栈上分配、同步消除、标量替换
逃逸分析
Java的逃逸分析只发在JIT的即时编译中,为什么不在前期的静态编译中就进行呢,知乎上已经有过这样的提问。
逃逸分析,是Java虚拟机中的一种优化技术,但它并不是直接优化代码,而是为其他优化手段提供优化依据的分析技术。逃逸分析的基本行为就是分析对象的动态作用域:当一个对象在方法中被定义后,它可能被外部方法引用,例如作为参数传递到了其他方法中,这称之为方法逃逸;如果被外部线程访问到,比如赋值给类变量或者可以在其他线程中访问的实例变量,这称之为线程逃逸。
栈上分配
说明
在我们的常识中,几乎所有的对象都是在Java堆上分配和创建的,java堆中的对象是线程共享的,只要持有这个对象的引用,就可以访问堆中存储的这个对象的数据。虚拟机的垃圾收集,可以回收堆中不再使用的对象,但是,不管是回收还是筛选可回收对象,或者是回收和整理内存,都是需要消耗时间的。如果可以确定一个对象不会发生方法逃逸,那么可以让这个对象直接在栈上分配,这样对象占用的内存就可以随着栈帧出栈而销毁。在很多应用中,不会逃逸的局部变量所占的比例很大,如果可以使用栈上分配,那么大量的对象就会随着方法的结束而自动销毁了,这样垃圾收集系统的压力会小很多。
同步消除
如果逃逸分析能够确定一个变量不会发生线程逃逸,那么这个变量的读写就不会发生线程竞争,对这个变量实施的同步措施就可以消除了。
标量替换
说明
标量:一个数据无法再分解为更小的数据来表示了,Java虚拟机中的原始数据类型byte、short、int、long、boolean、char、float、double 以及reference类型等,都不能再进一步分解了,这些就可以称为标量。聚合量:如果一个数据可以继续分解,就称为聚合量。对象就是最典型的聚合量。如果把一个java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复到原始类型来访问,就叫标量替换。如果逃逸分析可以证明一个对象不会被外部访问,并且这个对象可以拆散的话,那程序真正执行时将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来替代。将对象拆分后,除了可以让对象的成员变量在栈上分配和读写外(栈上存储的数据,有很大概率会被虚拟机分配至物理机器的高速寄存器中存储),还可以为后续的进一步优化手段创造条件。
JVM 线程
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收操作系统负责将线程安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法如果一个线程抛异常,并且该线程时进程中最后一个守护线程,那么进程将停止
JVM 系统线程
1.如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。2.这些后台线程不包括调用public static void main(String [])的main线程以及所有这个main线程自己创建的线程。
这些主要的后台系统线程在Hotspot JVM里主要是以下几个:虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持编译线程:这种线程在运行时会将字节码编译成到本地代码信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理
类加载子系统 Class Loader SubSystem
描述
类加载器子系统作用
1.类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。2.ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。3.加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器
描述
类加载器是JVM执行类加载机制的前提。ClassLoader的作用:ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。
类加载器最早出现在Java1.0版本中,那个时候只是单纯地为了满足Java Applet应用而被研发出来。但如今类加载器却在OSGi、字节码加解密领域大放异彩。这主要归功于Java虚拟机的设计者们当初在设计类加载器的时候,并没有考虑将它绑定在JVM内部,这样做的好处就是能够更加灵活和动态地执行类加载操作。
图示
命名空间
描述
每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
类唯一性
何为类的唯一性?对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。
class --> Java.lang.Class
1.class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。2.class file加载到JVM中,被称为DNA元数据模板,放在方法区。3.在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
类与对象
对象
对象实例化方式
对象实例化过程
对象的内存布局
header头
运行时数据(Mark word)
哈希值(hash code)
GC年龄代
锁状态标识
线程持有锁
偏向锁线程ID
偏向时间戳
类型指针
说明:指向类元素数据,确定该对所属的类
实例数据
填充数据
图示
子主题
对象定位
定义:jvm 如何通过栈帧的局部表对象引用定位到堆中的对象实例?以及类中的类型指针怎么找到方法区中的类型数据?
句柄访问
图示
好处:reference中存储稳定句柄地址,对象移动时(垃圾收集)时只会改变变句柄的数据指针即可,reference中不需要改变
直接指针(hotspot采用)
图示
垃圾收集
垃圾回收相关算法
垃圾标记阶段:对象存活判断
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段。那么在JVM中究竟如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
引用计数算法(Reference Counting)
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。缺点:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
小结:引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。Java 并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。Python如何解决循环引用?手动解除:在合适的时机,手动解除引用关系。使用弱引用weakref,weakref 是Python提供的标准库,旨在解决循环引用。
可达性分析算法
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄露的发生。相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫做追踪性垃圾收集(Tracing Garbage Collection)。所谓 ”GC Roots“ 根集合就是一组必须活跃的引用。
基本思路:可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots
在Java语言中,GC Roots 包括以下几类元素:1)虚拟机栈中引用的对象比如:各个线程被调用的方法中使用到的参数、局部变量等。2)本地方法栈内JNI(通常说的本地方法)引用的对象3)方法区中静态属性引用的对象比如:Java类的引用类型静态变量4)方法区中常量引用的对象比如:字符串常量池(String Table)里的引用5)所有被同步锁 synchronized 持有的对象6)Java虚拟机内部的引用7)基本数据类型对应的Class对象,一些常驻的异常对象(如 NullPointerException、OutOfMemoryError),系统类加载器。反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其它对象”临时性“地加入,共同构成完整 GC Roots 集合。比如:分代收集和局部回收(Partial GC)。如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其它区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots 集合中去考虑,才能保证可达性分析的准确性。
注意: 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致GC进行时必须”Stop The World“的一个重要原因。 即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
对象的 finalization 机制
介绍
描述:Java语言提供了对象终止(finalization)机制来允许开发人员提供对像被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的 finalize() 方法。finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用。理由包括下面三点:在 finalize() 时可能会导致对象复活。finalize() 方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则 finalize() 方法将没有执行机会。一个糟糕的 finalize() 会严重影响 GC 的性能。
对象可能的三种状态
如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是”非死不可“的。这时候它们暂时处于”缓刑“阶段。一个无法触及的对象有可能在某一个条件下”复活“自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态
可触及的:从根节点开始,可以到达这个对象。可复活的:对象的所有引用都被释放,但是对象有可能在 finalize() 中复活。不可触及的:对象的 finalize() 被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize() 只会被调用一次。
具体过程
判定一个对象 objA 是否可回收,至少经历两次标记过程:如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。进行筛选,判断此对象是否有必要执行 finalize() 方法如果对象 objA 没有重写 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,则虚拟机视为”没有必要执行“,objA 被判定为不可触及的。如果对象 objA 重写了 finalize() 方法,且还未执行过,那么 objA 会被插入到 F-Queue 队列中,由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其 finalize() 方法执行。finalize() 方法是对象逃脱死亡的最后机会,稍后GC会对 F-Queue 队列中的对象进行第二次标记,如果 objA 在 finalize() 方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA 会被移出”即将回收“集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize 方法只会被调用一次。
代码演示
public class CanReliveObj { public static CanReliveObj obj; //类变量,属于 GC Roots// @Override// protected void finalize() throws Throwable {// super.finalize();// System.out.println("调用当前类重写的finalize()方法");// obj = this;// } public static void main(String[] agrs) { try { obj = new CanReliveObj(); // 对象第一次成功拯救自己 obj = null; System.gc(); //调用垃圾回收器 System.out.println("第1次 gc"); // 因为Finalizer线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } System.out.println("第2次 gc"); // 下面这段代码与上面的完全相同,但是这次自救却失败了 obj = null; System.gc(); // 因为Finalizer线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } } catch (InterruptedException e) { e.printStackTrace(); } }}
垃圾清除阶段算法
当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用 内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是标记-清除算法(Mark-Sweep)、复制算法(Copying)、标记-压缩算法(Mark-Compact)。
注意:何为清除?这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位址空间是否够,如果够,就存放。
标记-清除算法(Mark-Sweep)
背景:标记-清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被 J.McCarthy 等人在1960年提出并应用于 Lisp 语言。
执行过程:当堆中的有效内存空间(available memory)被耗尽的时候,,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记:Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
优缺点
缺点:效率不算高在进行GC的时候,需要停止整个应用程序,导致用户体验差这个方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
复制算法(Copying)
背景
为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky 于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器 CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky 在该论文中描述的算法被人们称为复制(Copying)算法,它也被 M.L.Minsky 本人成功地引入到了Lisp语言的一个实现版本中。
核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存后对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优缺点
优点:没有标记和清除过程,实现简单,运行高效。复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:此算法的缺点也是很明显的,就是需要两倍的内存空间。对于 G1 这种分拆成为大量 region 的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。特别的:如果系统中的存活的对象很多,复制算法不会很理想。因为复制算法需要的是复制的存活对象数量并不会太大,或者说非常小,复制算法才是理想的。
应用场景:
在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代
标记-压缩(整理)算法(Mark-Compact)
背景:
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其它的算法。 标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。 1970年前后,G.L.Steele、C.J.Chene 和 D.S.Wise 等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。
执行过程
第一阶段和标记-清除算法一样,从根根节点开始标记所有被引用对象。第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
与标记清除算法对比
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。可以看到,标记的存活对象将会被整理,按照内存地址依次排序,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
优缺点
优点:消除了标记-清除算法当中,内存区域分散的缺点,需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。消除了复制算法当中,内存减半的高额代价。缺点:从效率上来说,标记-整理算法要低于复制算法。移动对象的同时,如果对象被其它对象引用,则还需要调整引用的地址。移动过程中,需要全程暂停用于应用程序,即:STW。
分代收集算法
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收效率。
目前几乎所有的GC都是采用分代收集(Generational Collection)算法执行垃圾回收的。
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。年轻代(Young Gen):年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过HotSpot中的两个 survivor 的设计得到缓解。老年代(Tenured Gen):老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。Mark阶段的开销与存活对象的数量成正比。Sweep阶段的开销与所管理区域的大小成正相关。Compact阶段的开销与存活对象的数据成正比。
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的 Concurrent Mode Failure时),将采用 Serial Old 执行 Full GC 以达到对老年代内存的整理。分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
写在最后:
注意,这些只是基本的算法思路,实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是复合算法,并且并行和并发兼备。
垃圾回收相关思想
增量收集算法
基本思想
如果一次性将所有垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间region。每个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
垃圾回收相关概念
System.gc()
介绍
1.在默认情况下,通过System.gc()者Runtime.getRuntime().gc() 的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。2.然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)3.JVM实现者可以通过System.gc() 调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。4.在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()
System.gc()会触发Full GC,可以通过-XX:+DisableExplicitGC参数屏蔽System.gc(),在使用CMS GC的前提下,也可以使用-XX:+ExplicitGCInvokesConcurrent参数来进行并发Full GC,提升性能。不过,一般不推荐使用System.gc(),因为Full GC 耗时比较长,对应用影响较大,如前段时间的一个案例:依赖包滥用System.gc()导致的频繁Full GC。并且也不建议设置-XX:+DisableExplicitGC,特别是在有使用堆外内存的情况下,如果堆外内存申请不到足够的空间,jdk会触发一次System.gc(),来进行回收,如果屏蔽了,最后一根救命稻草也就失效了,自然就OOM了。
代码演示
/** * @author shkstart shkstart@126.com * @create 2020 14:57 */public class LocalVarGC { public void localvarGC1() { byte[] buffer = new byte[10 * 1024 * 1024];//10MB System.gc(); } public void localvarGC2() { byte[] buffer = new byte[10 * 1024 * 1024]; buffer = null; System.gc(); } public void localvarGC3() { { byte[] buffer = new byte[10 * 1024 * 1024]; } System.gc(); } public void localvarGC4() { { byte[] buffer = new byte[10 * 1024 * 1024]; } int value = 10; System.gc(); } public void localvarGC5() { localvarGC1(); System.gc(); } public static void main(String[] args) { LocalVarGC local = new LocalVarGC(); // TODO local.localvarGC1(); }}
分析:1.调用 localvarGC1() 方法执行 System.gc() 仅仅是将年轻代的 buffer 数组对象放到了老年代(Why?buffer 数组年龄应该还没达到阈值吧~~~)2.调用 localvarGC2() 方法由于 buffer 数组对象没有引用指向它,执行 System.gc() 将被回收3.调用 localvarGC3() 方法虽然出了代码块的作用域,但是 buffer 数组对象并没有被回收,来看看字节码:实例方法局部变量表第一个变量肯定是 this,你有没有看到,局部变量表的大小是 2 ,也就是说执行 System.gc() 时,栈中还有 buffer 变量指向堆中的字节数组4.调用 localvarGC4() 方法就多定义了一个局部变量 value ,怎么就能把字节数组回收了呢?看,value 位于局部变量表中索引为 1 的位置,局部变量表长度为 2 ,这说明了出了代码块时,buffer 就出了其作用域范围,此时没有为 value 开启新的槽,所以 value 变量占据了 buffer 变量的槽(Slot),导致堆中的字节数组没有引用再指向它,执行 System.gc() 时被回收5.调用 localvarGC5() 方法这有啥好说的。。。局部变量除了方法范围就是失效了,堆中的字节数组铁定被回收呗~
内存溢出与内存泄露
内存溢出(OOM)
介绍
内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。Javadoc中对OutofMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
类型
JVM Heap(堆)溢出:java.lang.OutOfMemoryError: Java heap space
问题
代码中试图向JVM申请内存空间,但是没有足够的空间。注意:机器的物理空间足够,但是JVM的堆大小限制,也会导致出错。 JVM在启动的时候会自动设置JVM Heap的值, 可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap的大小是Young Generation 和Tenured Generaion 之和。在JVM中如果98%的时间是用于GC,且可用的Heap size 不足2%的时候将抛出此异常信息。解决方法:手动设置JVM Heap(堆)的大小。
原因分析
1.最直接的原因就是你配置的堆内存太小,或者你的机器内存不够。2.用户量请求量或者书籍处理量上来后,程序所需的资源,远远大于平常,导致JVM堆内存不够用。3.内存泄漏,不知情的情况下,内存被某些功能中的对象锁占用了。通常是编码过程中的错误方式导致对象使用后无法被回收。
Metaspace或者 Permgen spacee溢出: java.lang.OutOfMemoryError: PermGen space
PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。为什么会内存溢出,这是由于这块内存主要是被JVM存放Class和Meta信息的,Class在被Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同,sun的 GC不会在主程序运行期对PermGen space进行清理,所以如果你的APP会载入很多CLASS的话,就很可能出现PermGen space溢出。一般发生在程序的启动阶段。解决方法: 通过-XX:PermSize和-XX:MaxPermSize设置永久代大小即可。 方法区用于存放java类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。在类装载器加载class文件到内存的过程中,虚拟机会提取其中的类型信息,并将这些信息存储到方法区。当需要存储类信息而方法区的内存占用又已经达到-XX:MaxPermSize设置的最大值,将会抛出OutOfMemoryError异常。对于这种情况的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。这里需要借助CGLib直接操作字节码运行时,生成了大量的动态类。
栈溢出: java.lang.StackOverflowError : Thread Stack space
栈溢出了,JVM依然是采用栈式的虚拟机,这个和C和Pascal都是一样的。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的 “层”太多了,以致于把栈区溢出了。 通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K的空间(这个大约相当于在一个C函数内声明了256个int类型的变量),那么栈区也不过是需要1MB的空间。通常栈的大小是1-2MB的。通俗一点讲就是单线程的程序需要的内存太大了。 通常递归也不要递归的层次过多,很容易溢出。解决方法:1:修改程序。2:通过 -Xss: 来设置每个线程的Stack大小即可。在Java虚拟机规范中,对这个区域规定了两种异常状况:StackOverflowError和OutOfMemoryError异常。
数组大小溢出:Requested array size exceeds VM limit
java.lang.OutOfMemoryError : GC overhead limit exceeded
问题
当GC花费了程序运行总时间的98%以上,而回收不到2%的堆,则抛出该异常。
The parallel collector throws an OutOfMemoryError if too much time is being spent in garbage collection (GC): If more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, then an OutOfMemoryError is thrown. This feature is designed to prevent applications from running for an extended period of time while making little or no progress because the heap is too small. If necessary, this feature can be disabled by adding the option -XX:-UseGCOverheadLimit to the command line.
原因分析及解决思路
这个异常不是很容易重现。因为GC时也意味着堆内存不够,可能实际抛出的是更常见的java.lang.OutOfMemoryError: Java heap space,比如你把上面这个例子堆内存设置为16m或者更大的时候。也不要尝试通过 -XX:-UseGCOverheadLimit 参数关闭这个功能,否则导致无法看到java.lang.OutOfMemoryError完整的信息。
原因分析
首先说没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有二:1.Java虚拟机的堆内存设置不够。 比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。 我们可以通过参数-Xms 、-Xmx来调整。2.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用) 对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见 尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。 对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:PermGen space"。 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息则变成了:“java.lang.OutofMemoryError:Metaspace"。直接内存不足,也会导致OOM。
一)是否应用中的类中和引用变量过多使用了Static修饰 如public staitc Student s;在类中的属性中使用 static修饰的最好只用基本类型或字符串。如public static int i = 0; //public static String str;二)是否 应用 中使用了大量的递归或无限递归(递归中用到了大量的建新的对象)三)是否App中使用了大量循环或死循环(循环中用到了大量的新建的对象)四)检查 应用 中是否使用了向数据库查询所有记录的方法。即一次性全部查询的方法,如果数据量超过10万多条了,就可能会造成内存溢出。所以在查询时应采用“分页查询”。五)检查是否有数组,List,Map中存放的是对象的引用而不是对象,因为这些引用会让对应的对象不能被释放。会大量存储在内存中。六)检查是否使用了“非字面量字符串进行+”的操作。因为String类的内容是不可变的,每次运行"+"就会产生新的对象,如果过多会造成新String对象过多,从而导致JVM没有及时回收而出现内存溢出。
说明
这里面隐含着一层意思是,在抛出OutofMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。在java.nio.Bits.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。当然,也不是在任何情况下垃圾收集器都会被触发的比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutofMemoryError。
内存泄漏(Memory Leak)
两种定义
严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。
说明
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutofMemory异常,导致程序崩溃。注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
例子
单例模式 单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。一些提供close()的资源未关闭导致内存泄漏 数据库连接 dataSourse.getConnection(),网络连接socket和io连接必须手动close,否则是不能被回收的。
关于内存使用的建议
1、尽早释放无用对象的引用,让引用变量在退出活动域后自动设置为null。2、程序进行字符串处理时,尽量避免使用String,而使用StringBuffer。3、静态变量是全局的,GC不会回收。4、避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作。
参数
-Xms:java Heap初始大小, 默认是物理内存的1/64。-Xmx:java Heap最大值,不可超过物理内存。-Xmn:young generation的heap大小,一般设置为Xmx的3、4分之一 。增大年轻代后,将会减小年老代大小,可以根据监控合理设置。-Xss:每个线程的Stack大小,而最佳值应该是128K,默认值好像是512k。-XX:PermSize:设定内存的永久保存区初始大小,缺省值为64M。-XX:MaxPermSize:设定内存的永久保存区最大大小,缺省值为64M。-XX:SurvivorRatio:Eden区与Survivor区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。-XX:+UseParallelGC:F年轻代使用并发收集,而年老代仍旧使用串行收集。-XX:+UseParNewGC:设置年轻代为并行收集,JDK5.0以上,JVM会根据系统配置自行设置,所无需再设置此值。-XX:ParallelGCThreads:并行收集器的线程数,值最好配置与处理器数目相等 同样适用于CMS。-XX:+UseParallelOldGC:年老代垃圾收集方式为并行收集(Parallel Compacting)。-XX:MaxGCPauseMillis:每次年轻代垃圾回收的最长时间(最大暂停时间),如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。-XX:+ScavengeBeforeFullGC:Full GC前调用YGC,默认是true。
Stop the World
在垃圾回收过程中,应用软件将处于一种 Stop the World 的状态。在 Stop the World 状态下,应用程序所有的线程都会挂起,暂停一切正常工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。
Stop the World 的注意事项
1.STW事件和采用哪款GC无关,所有的GC都有这个事件。2.哪怕是G1也不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。3.STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。4.开发中不要用System.gc() ,这会导致Stop-the-World的发生。
安全点与安全区域
安全点(Safepoint)
介绍
1.程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)”。2.Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。3.大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。4.比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
安全点的中断实现方式
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?1.抢先式中断:(目前没有虚拟机采用了)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。2.主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)
安全区域(Safe Region)
介绍
Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?例如线程处于Sleep状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
安全区域的执行流程
当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止;
垃圾回收的并行与并发
并发与并行概念介绍
并发的概念
1.在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行2.并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换3.由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行
并行的概念
1.当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)2.其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行3.适合科学计算,后台处理等弱交互场景
并发与并行的对比
并发,指的是多个事情,在同一时间段内同时发生了。并行,指的是多个事情,在同一时间点上同时发生了。并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的。只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
垃圾回收的并行与串行
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。如ParNew、Parallel Scavenge、Parallel Old串行(Serial)相较于并行的概念,单线程执行。如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收(单线程)
垃圾回收的并发
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。比如用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;典型垃圾回收器:CMS、G1
垃圾收集器
垃圾回收器概述
1.垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。2.由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。3.从不同角度分析垃圾收集器,可以将GC分为不同的类型。
垃圾回收器分类
按垃圾回收线程数分
串行垃圾回收器
串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
并行垃圾回收器
和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“Stop-the-World”机制。
使用场景
在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器对于新生代,回收次数频繁,使用并行方式高效。对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
按照工作模式分
并发式垃圾回收器
并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
独占式垃圾回收器
独占式垃圾回收器(Stop the World)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
按碎片处理方式分
压缩式垃圾回收器
压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片,分配对象空间使用指针碰撞
非压缩式垃圾回收器
非压缩式的垃圾回收器不进行这步操作,分配对象空间使用空闲列表
按工作的内存区间分
年轻代垃圾回收器
老年代垃圾回收器
评估 GC 的性能指标
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。收集频率:相对于应用程序的执行,收集操作发生的频率。内存占用:Java堆区所占的内存大小。快速:一个对象从诞生到被回收所经历的时间。
1.吞吐量、暂停时间、内存占用这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。2.这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。3.简单来说,因为内存不值钱了,主要抓住两点:吞吐量暂停时间
7 种经典的垃圾收集器
简介
串行回收器:Serial、Serial old并行回收器:ParNew、Parallel Scavenge、Parallel old并发回收器:CMS、G1
查看默认垃圾收集器:设置 -XX:+PrintCommandLineFlags 查看
7款经典回收器与垃圾分代之间的关系
新生代收集器:Serial、ParNew、Parallel Scavenge;老年代收集器:Serial old、Parallel old、CMS;整堆收集器:G1;
Serial 回收器
描述/特点
Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。Serial收集器采用复制算法、串行回收和"Stop-the-World"机制的方式执行内存回收。除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial old收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。Serial Old是运行在Client模式下默认的老年代的垃圾回收器,Serial Old在Server模式下主要有两个用途:与新生代的Parallel Scavenge配合使用作为老年代CMS收集器的后备垃圾收集方案
串行回收器
使用-XX:+UseSerialGC参数 显式指定serial回收器
工作流程
它只会使用一个CPU或一条收集线程去完成垃圾收集工作更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)
优劣势
1.优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。2.运行在Client模式下的虚拟机是个不错的选择。3.在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。
PaeNew 回收器
描述/特点
1.如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。2.Par是Parallel的缩写,New:只能处理新生代3.ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。4.ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。
并行回收
使用-XX:+UseParNewGC参数 显式指定serial回收器
工作流程
ParNew 与 Serial 比较
由于ParNew收集器基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?并不能ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。但是在单个CPU的环境下,ParNew收集器不比Serial收集器更高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。
Parallel 回收器
描述/特点
1.HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制。2.那么Parallel收集器的出现是否多此一举? 和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。3.高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。4.Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器。5.Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-World"机制。
在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在server模式下的内存回收性能很不错。在Java8中,默认是此垃圾收集器。
Parallel Scavenge 回收器参数设置
-XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。-XX:+UseParallelOldGC:手动指定老年代都是使用并行回收收集器。上面两个参数分别适用于新生代和老年代。默认jdk8是开启的。默认开启一个,另一个也会被开启。(互相激活)
-XX:ParallelGCThreads:设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量。当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU_Count]/8]
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。为了尽可能地把停顿时间控制在XX:MaxGCPauseMillis 以内,收集器在工作时会调整Java堆大小或者其他一些参数。对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。该参数使用需谨慎。
-XX:GCTimeRatio垃圾收集时间占总时间的比例,即等于 1 / (N+1) ,用于衡量吞吐量的大小。取值范围(0, 100)。默认值99,也就是垃圾回收时间占比不超过1。与前一个-XX:MaxGCPauseMillis参数有一定矛盾性STW暂停时间越长,Radio参数就容易超过设定的比例。
-XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。
工作流程
CMS 回收器
描述/特点
1.在JDK1.5时期,Hotspot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。2.CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。3.目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。4.CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-World"5.不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。
低延迟
工作原理
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW的阶段主要是:初始标记 和 重新标记)1.初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。2.并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。3.重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,并且也会导致“Stop-the-World”的发生,但也远比并发标记阶段的时间短。4.并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
CMS 特点与弊端分析
1.尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop-the-World”,只是尽可能地缩短暂停时间。2.由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。3.因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。4.要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。4.CMS收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。
为什么 CMS 不采用标记-压缩算法呢?答案其实很简答,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“stop the world”这种场景下使用
优缺点
优点
并发收集低延迟
缺点
1.会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。2.CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。3.CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
CMS 参数配置
-XX:+UseConcMarkSweepGC:手动指定使用CMS收集器执行内存回收任务。开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+CMS(Old区)+Serial Old(Old区备选方案)的组合。-XX:CMSInitiatingOccupanyFraction:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。-XX:+UseCMSCompactAtFullCollection:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。-XX:CMSFullGCsBeforeCompaction:设置在执行多少次Full GC后对内存空间进行压缩整理。-XX:ParallelCMSThreads:设置CMS的线程数量。CMS默认启动的线程数是 (ParallelGCThreads + 3) / 4,ParallelGCThreads是年轻代并行收集器的线程数,可以当做是 CPU 最大支持的线程数当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
G1 回收器
描述/特点
区域化分代式
既然我们已经有了前面几个强大的 GC ,为什么还要发布 Garbage First(G1)GC?1.原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。2.G1(Garbage-First)垃圾回收器是在Java7 update4之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。3.与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。4.官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
为什么名字叫 Garbage First(G1) 呢?1.因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。2.G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。3.由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。4.G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。5.在JDK1.7版本正式启用,移除了Experimental的标识,是JDK9以后的默认垃圾回收器,取代了CMS回收器以及Parallel+Parallel Old组合。被Oracle官方称为“全功能的垃圾收集器”。6.与此同时,CMS已经在JDK9中被标记为废弃(deprecated)。G1 在JDK8中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC来启用。
G1的适用场景
1.面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)2.最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;3.如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。4.用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:超过50%的Java堆被活动数据占用;对象分配频率或年代提升频率变化很大;GC停顿时间过长(长于0.5至1秒)5.HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器均使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
分区 Region
1.使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过2.XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。3.一个Region有可能属于Eden,Survivor或者Old/Tenured内存区域。但是一个Region只可能属于一个角色。图中的E表示该Region属于Eden内存区域,S表示属于Survivor内存区域,O表示属于Old内存区域。图中空白的表示未使用的内存空间。4.G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1.5个Region,就放到H。
设置 H 的原因
1.对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。2.为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。3.如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。
Regio的内部结构
每个Region都是通过指针碰撞来分配空间每个Region都有TLAB,提高对象分配的效率
优缺点
优点
并行与并发
与其他GC收集器相比,G1使用了全新的分区算法,其特点如下所示:并行与并发兼具并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
分代收集
从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
空间整合
CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
可预测的停顿时间模型
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。1.由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。2.G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。3.相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
缺点
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
G1 参数配置
-XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务-XX:G1HeapRegionSize:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。-XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间指标JVM会尽力实现,但不保证达到。默认值是200ms-XX:+ParallelGCThread:设置STW工作线程数的值。最多设置为8-XX:ConcGCThreads:设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGcThreads)的1/4左右。-XX:InitiatingHeapOccupancyPercent:设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
G1 垃圾回收流程
主要环节
G1 GC的垃圾回收过程主要包括如下三个环节:年轻代GC(Young GC)老年代并发标记过程(Concurrent Marking)混合回收(Mixed GC)极端情况下会出现full GC
大致的回收流程
1.应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。2.在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。3.当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。标记完成马上开始混合回收过程。4.对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。5.和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。6.举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
G1 年轻代 GC
1.JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。2.YGC时,首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
回收过程
1.第一阶段,扫描根根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口。2.第二阶段,更新RSet处理dirty card queue(见备注)中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用。3.第三阶段,处理RSet识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。4.第四阶段,复制对象。此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。5.第五阶段,处理引用处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
备注:1.对于应用程序的引用赋值语句 oldObject.field=new Object(),JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。2.在年轻代回收的时候,G1会对Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。3.那为什么不在引用赋值语句处直接更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多。
G1 并发标记过程
1.初始标记阶段: 标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。 正是由于该阶段时STW的,所以我们只扫描根节点可达的对象,以节省时间。2.根区域扫描(Root Region Scanning): G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。 这一过程必须在Young GC之前完成,因为Young GC会使用复制算法对Survivor区进行GC。3.并发标记(Concurrent Marking): 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被Young GC中断。 在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。 同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。4.再次标记(Remark): 由于应用程序持续进行,需要修正上一次的标记结果。是STW的。 G1中采用了比CMS更快的初始快照算法:Snapshot-At-The-Beginning(SATB)。5.独占清理(cleanup,STW): 计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。 为下阶段做铺垫。是STW的。这个阶段并不会实际上去做垃圾的收集6.并发清理阶段: 识别并清理完全空闲的区域。
G1 混合回收过程
1.当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。2.这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。
细节
1.并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。2.默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收3.混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。4.由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收。5.XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。6.混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。
G1 回收可选的过程四:Full GC
1.G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。2.要避免Full GC的发生,一旦发生Full GC,需要对JVM参数进行调整。什么时候会发生Ful1GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到Full GC,这种情况可以通过增大内存解决。导致G1 Full GC的原因可能有两个:Evacuation的时候没有足够的to-space来存放晋升的对象;并发处理过程完成之前空间耗尽。
存在的问题
1.一个对象被不同区域引用的问题2.一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?3.在其他的分代收集器,也存在这样的问题(而G1更突出,因为G1主要针对大堆)4.回收新生代也不得不同时扫描老年代?这样的话会降低Minor GC的效率
解决方法:
记忆集(Remembered Set)
1.无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描;2.每个Region都有一个对应的Remembered Set3.每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;4.然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);5.如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;6.当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。
总结:1.在回收 Region 时,为了不进行全堆的扫描,引入了 Remembered Set2.Remembered Set 记录了当前 Region 中的对象被哪个对象引用了3.这样在进行 Region 复制时,就不要扫描整个堆,只需要去 Remembered Set 里面找到引用了当前 Region 的对象4.Region 复制完毕后,修改 Remembered Set 中对象的引用即可
G1 回收器的优化建议
1.年轻代大小 避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小 固定年轻代的大小会覆盖暂停时间目标2.暂停时间目标不要太过严苛 G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受 更多的垃圾回收开销,而这些会直接影响到吞吐量。
垃圾回收器总结
7 种垃圾回收器的比较
怎么选择垃圾回收器
Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。怎么选择垃圾收集器?优先调整堆的大小让JVM自适应完成。如果内存小于100M,使用串行收集器如果是单核、单机程序,并且没有停顿时间的要求,串行收集器如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。
革命性的 ZGC
子主题
GC 日志分析
内存分配与垃圾回收的参数列表
-XX:+PrintGC :输出GC日志。类似:-verbose:gc-XX:+PrintGCDetails :输出GC的详细日志-XX:+PrintGCTimestamps :输出GC的时间戳(以基准时间的形式)-XX:+PrintGCDatestamps :输出GC的时间戳(以日期的形式,如2013-05-04T21: 53: 59.234 +0800)-XX:+PrintHeapAtGC :在进行GC的前后打印出堆的信息-Xloggc:…/logs/gc.log :日志文件的输出路径
Young GC 图例
Full GC 图例
直接内存
不是JVM运行时数据区中的一部分,也不是java虚拟机规范定义的内存直接内容是java堆外的,直接向操作系统申请的来源于NIO,通过在堆中的DirectByteBuffer类操作Native内存直接存储内存速度优于java内存,读写性能会更高
直接内存也可以发生OOM:Direct buffer memoryJAVA堆与直接内存受到操作系统内给出来的最大内存缺点:分配与回收成本较高,不受JVM内存回收管理最大值可以通过DirectMemorySize设置,默认与最大堆空间相同
Native内存操作
直接内存如何回收
Unsafe
IO/NIO简介
图示
执行引擎Execution Engine