4.垃圾回收调优
4.1.调优简介
1、查看虚拟机运行参数
java -XX:PrintFlagsFinal -version | findstr "GC"
2、回收器选择问题
【低延迟】还是【高吞吐量】,选择合适的回收器
- CMS、G1、ZGC
- ParallelGC
3、垃圾回收频繁的问题分析?
查看Full GC前后的内存占用,考虑下面几个问题
- 数据太多
- 数据表示太臃肿
- 是否存在内存泄露
4.2.新生代调优
1、新生代的特点
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- TLAB 是每个线程都会在伊甸园中分配一块私有的区域,当new一个对象的时候,先去TLAB中查看有没有足够的内存空间,如果有就占用这块内存。
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
2、调优参数
- 晋升阈值配置得当,让长时间存活对象尽快晋升
- -XX:MaxTenuringThreshold=threshold :最大晋升值
- -XX:+PrintTenuringDistribution :晋升的日志
4.3.老年代调优
- 以CMS为例
- CMS的老年代内存越大越好
- 先尝试不做调优,如果没有Full GC那么就无需调优,否则现场时调优新生代
- 观察发生FullGC时老年代的占比,将老年代内存预设调大1/4 ~ 1/3
- -XX:CMSInitatingOccupancyFraction=percent
- 参数为0,只要老年代有垃圾就回收
- 一般设置在百分之75到80,也就是说,预留20的空间来进行回收
4.4.GC调优案例
1、Full GC和Minor GC频繁
如果GC发生频繁,说明空间紧张,如果是新生代的空间紧张,当业务高峰期来了,大量对象被创建,很快就会把新生代的空间塞满,塞满之后还会造成一个问题,幸存区空间紧张,导致对象的晋升阈值减小,导致一些周期很短的对象也会被晋生到老年代,老年代存了大量的生命周期少的对象,发生Full GC。
增大新生代内存,新生代内存充裕后,就不会频繁发生GC,这样老年代也不会存储大量周期短的对象。
2、请求高峰期发生Full GC,单次暂停时间特别长(CMS)
CMS,查看GC日志,CMS初始标记和并发标记都是比较快的,耗时主要发生在重新标记上,重新标记不但会扫描老年代的和新生代的垃圾,可以在重新标记之前进行新生代的内存。使用参数-
XX:+CMSScavengeBeforeRemark设置重新标记之前进行新生代的一次GC回收。
3、老年代充裕的情况下,发生Full GC(CMS jdk1.7)
在1.8版本之前永久代的空间不足,也会产生Full GC。
5.字节码技术
5.1.类文件结构
1、JVM规范下类文件结构
2、二进制字节码文件
//Hello World 示例 public class Demo16 { public static void main(String[] args) { System.out.println("hello world"); } }
od -t -xC Demo16.class
0000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09 0000020 00 15 00 16 08 00 17 0a 00 18 00 19 07 00 1a 07 0000040 00 1b 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 0000140 00 04 74 68 69 73 01 00 14 4c 63 6f 6d 2f 6c 69 0000160 78 69 61 6e 67 2f 44 65 6d 6f 31 36 3b 01 00 04 0000200 6d 61 69 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 0000220 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 01 00 04 0000240 61 72 67 73 01 00 13 5b 4c 6a 61 76 61 2f 6c 61 0000260 6e 67 2f 53 74 72 69 6e 67 3b 01 00 0a 53 6f 75 0000300 72 63 65 46 69 6c 65 01 00 0b 44 65 6d 6f 31 36 0000320 2e 6a 61 76 61 0c 00 07 00 08 07 00 1c 0c 00 1d 0000340 00 1e 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 0000360 07 00 1f 0c 00 20 00 21 01 00 12 63 6f 6d 2f 6c 0000400 69 78 69 61 6e 67 2f 44 65 6d 6f 31 36 01 00 10 0000420 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 0000440 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 0000460 74 65 6d 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 0000500 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 0000520 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 0000540 74 53 74 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 0000560 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 0000600 53 74 72 69 6e 67 3b 29 56 00 21 00 05 00 06 00 0000620 00 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00 0000640 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1 0000660 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 04 0000700 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c 00 0d 0000720 00 00 00 09 00 0e 00 0f 00 01 00 09 00 00 00 37 0000740 00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00 04 0000760 b1 00 00 00 02 00 0a 00 00 00 0a 00 02 00 00 00 0001000 06 00 08 00 07 00 0b 00 00 00 0c 00 01 00 00 00 0001020 09 00 10 00 11 00 00 00 01 00 12 00 00 00 02 00 0001040 13 0001041
5.2.字节码指令
1、字节码指令入门
研究两组字节码指令
(1)public cn.itcast.jvm.t5.HelloWord();构造方法的字节码指令
2a b7 00 01 b1
- 2a:aload_0加载slot 0的局部变量,即this作为下main的invokespecial构造方法调用的参数
- b7:invokespecial预备调用构造方法
- 00 01:引用常量池中#1项,即【Method java/lang/Object.“<init>”😦)V】
- b1:表示返回
(2)public static void main(java.lang.String[]);之方法的字节码指令
b2 00 02 12 03 b6 00 04 b1
- b2:getstatic用来加载静态变量
- 00 02:引用常量池中#2项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
- 12:ldc加载参数
- 03:引用常量池中#3项,即【String hello world】
- b6:invokevirtuaf预备调用成员方法。
- 00 04:引用常量池中#4项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
- b1:表示返回
5.3.图解方法执行流程
1、原始Java代码
public class Demo17 { /** * 演示字节码指令和操作数栈、常量池的关系 * @param args */ public static void main(String[] args) { int a = 10; int b = Short.MAX_VALUE+1; int c = a + b; System.out.println(c); } }
2、编译后的字节码文件
javap -v Demo17.class
Classfile /D:/ideaworkspace/jvm-demo/target/classes/com/lixiang/Demo17.class Last modified 2021-12-10; size 604 bytes MD5 checksum e36477501751b13feb4bebbef30bfa7b Compiled from "Demo17.java" public class com.lixiang.Demo17 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #7.#25 // java/lang/Object."<init>":()V #2 = Class #26 // java/lang/Short #3 = Integer 32768 #4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream; #5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V #6 = Class #31 // com/lixiang/Demo17 #7 = Class #32 // java/lang/Object #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 LocalVariableTable #13 = Utf8 this #14 = Utf8 Lcom/lixiang/Demo17; #15 = Utf8 main #16 = Utf8 ([Ljava/lang/String;)V #17 = Utf8 args #18 = Utf8 [Ljava/lang/String; #19 = Utf8 a #20 = Utf8 I #21 = Utf8 b #22 = Utf8 c #23 = Utf8 SourceFile #24 = Utf8 Demo17.java #25 = NameAndType #8:#9 // "<init>":()V #26 = Utf8 java/lang/Short #27 = Class #33 // java/lang/System #28 = NameAndType #34:#35 // out:Ljava/io/PrintStream; #29 = Class #36 // java/io/PrintStream #30 = NameAndType #37:#38 // println:(I)V #31 = Utf8 com/lixiang/Demo17 #32 = Utf8 java/lang/Object #33 = Utf8 java/lang/System #34 = Utf8 out #35 = Utf8 Ljava/io/PrintStream; #36 = Utf8 java/io/PrintStream #37 = Utf8 println #38 = Utf8 (I)V { public com.lixiang.Demo17(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1(栈的深度), locals=1(局部变量表的长度), args_size=1(参数的长度) 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lixiang/Demo17; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: bipush 10 2: istore_1 3: ldc #3 // int 32768 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 13: iload_3 14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 17: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 line 13: 10 line 14: 17 LocalVariableTable: Start Length Slot Name Signature 0 18 0 args [Ljava/lang/String; 3 15 1 a I 6 12 2 b I 10 8 3 c I } SourceFile: "Demo17.java"
3、图解方法执行流程
(1)常量池载入运行时常量池
Java虚拟机的类加载器,会把main方法所在的类进行类加载操作,
会把类的字节码相关信息放入内存中,同时把常量池中的信息放入运行时常量池中,后续查找都在运行时常量池中。一些比较小的值,不会直接放到常量池中,而是存放在字节码指令中。常量池也属于方法区,只不过这里单独提出来了。
(2)方法字节码载入方法区,main线程开始运行,分配栈帧内存
(stack=2,locals=4),两个操作数栈,局部变量表中有四个槽位。
(3)执行引擎开始执行字节码
- bipush:将一个byte压入操作数栈(其长度会补齐4个字节),类似的指令还有
- sipush:将一个short压入操作数栈(其余长度会补齐4个字节)
- ldc:将一个int压入操作数栈
- ldc2_w:将一个long压入操作数栈(分两次压入,因为long是8个字节)
- 这里小的数字都是和字节码指令存在一起,超过short范围的数字存入了常量池
bipush 10:将10这个数值,压入栈顶,执行引擎执行bipush 10
istore 1:将操作数栈中的元素弹出,放入局部变量表slot 1中,对应的代码a = 10
ldc #3:读取运行时常量池中#3位置,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
istore 2:将操作数栈中的元素弹出,放到局部变量表的2号位置
istore 3:将操作数栈中的元素弹出,放入局部变量表的3号位置
getstatic #4:在运行时常量池中找到#4,发现是一个对象,在堆内找到该对象,将其引用放入操作数栈中。
iload 3:将局部变量表中3号位置的元素压入操作数栈中
invokevirtual 5:找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法,生成新的栈帧(分配 locals、stack等)
传递参数,执行新栈帧中的字节码。
执行完毕,弹出栈帧
清除 main 操作数栈内容
return
完成 main 方法调用,弹出 main 栈帧,程序结束
5.4.字节码分析x=0
代码:
public class Demo2 { public static void main(String[] args) { int i=0; int x=0; while(i<10) { x = x++; i++; } System.out.println(x); //接过为0 } }
字节码解析:
Code: stack=2, locals=3, args_size=1 //操作数栈分为两个空间,局部变量表分为3个空间,参数的长度为1 0: iconst_0 //准备一个常数0 1: istore_1 //将常数0放在局部变量表第一个槽位上 2: iconst_0 //准备一个常数0 3: istore_2 //将常数0放在局部变量表第二个槽位上 4: iload_1 //将局部变量表中1号槽位的数据放入操作数栈中 5: bipush 10 //将10 放入操作数栈中,此时操作数栈中有10 和 0两个数 7: if_icmpge 21 //比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到21步 10: iload_2 //将局部变量表中2号槽位的数据放入操作数栈中,放入的数值为0 11: iinc 2, 1 //局部变量表中2号槽位的数加1,自增后,槽位中的数值为1 14: istore_2 //将操作数组栈中的数放入到局部变量表中的2号槽位,2号槽位有变成了0 15: iinc 1, 1 //1号槽位进行+1,1号槽位的数值为1 18: goto 4 //跳转到第四条指令 21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 24: iload_2 25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 28: return
5.5.构造方法
1、<cinit()>V
public class Demo3 { static int i = 10; static { i = 20; } static { i = 30; } public static void main(String[] args) { System.out.println(i); //结果为30 } }
编译器会按从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法cinit()V:
stack=1, locals=0, args_size=0 0: bipush 10 //将变量10从字节码中压入操作数栈 2: putstatic #3 // Field i:I 5: bipush 20 //将变量20从字节码中压入操作数栈 7: putstatic #3 // Field i:I 10: bipush 30 //将变量30从字节码中压入操作数栈 12: putstatic #3 // Field i:I 15: return //最后返回,以最后一次的取值为准
2、<init>()V
public class Demo4 { private String a = "s1"; { b = 20; } private int b = 10; { a = "s2"; } public Demo4(String a, int b) { this.a = a; this.b = b; } public static void main(String[] args) { Demo4 d = new Demo4("s3", 30); System.out.println(d.a); System.out.println(d.b); } }
编译器会按从上至下的顺序,收集所有{}(实例块)和成员变量复制的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。
Code: stack=2, locals=3, args_size=3 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String s1 7: putfield #3 // Field a:Ljava/lang/String; 10: aload_0 11: bipush 20 13: putfield #4 // Field b:I 16: aload_0 17: bipush 10 19: putfield #4 // Field b:I 22: aload_0 23: ldc #5 // String s2 25: putfield #3 // Field a:Ljava/lang/String; //原始构造方法在最后执行 28: aload_0 29: aload_1 30: putfield #3 // Field a:Ljava/lang/String; 33: aload_0 34: iload_2 35: putfield #4 // Field b:I 38: return
不同方法在调用时,对应的虚拟机指令有所区别。
私有、构造、被final修饰的方法,在调用时都使用invokespecial指令。
普通成员方法在调用的时候,使用invokevirtual指令。因为编译期间无法确定该方法的内容。只有在运行期间才能确定。
静态方法在调用时使用invokestatic指令。
Code: stack=2, locals=2, args_size=1 0: new #2 // class com/nyima/JVM/day5/Demo5 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokespecial #4 // Method test1:()V 12: aload_1 13: invokespecial #5 // Method test2:()V 16: aload_1 17: invokevirtual #6 // Method test3:()V 20: invokestatic #7 // Method test4:()V 23: return
new是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈。
dup是赋值操作数栈栈顶的内容,本例为【对象引用】,本例即为【对象引用】,为什么需要两份引用呢,一个是配合invokespecial调用该对象的构造方法“init:()V‘(会消耗掉栈顶一个引用),另一个要配合astore_1赋值给局部变量。
终方法(final),私有方法(private),构造方法都是由invokespecial指令来调用,属于静态绑定。
普通成员方法是由invokevirtual调用,属于动态绑定,即支持多态,成员方法与静态方法调用的另一个区别是,执行方法前是否需要对象。