JVM学习3 - GC调优及字节码

九. GC调优

//查看所有GC相关的参数
java -XX:+PrintFlagsFinal -version | findstr "GC"

确定目标: 低延迟还是高吞吐量, 选择合适的回收器.

  • 低延迟: CMS, G1, ZGC
  • 高吞吐量: ParallelGC

1. 最快的GC是不发生GC

查看 FullGC 前后的内存占用, 从代码方面考虑下面几个问题:

  • 数据是不是太多? (如一次性从数据库里查了过多的数到缓存中)
  • 数据表示是否太臃肿?
    • 对象图 (如从数据库里查数时只查需要的字段)
    • 对象大小 (如包装类型 Integer 需要占24字节, 而 int 只占用 4 字节)
  • 是否存在内存泄漏? (使用软/弱引用)
    • (如创建了一个长时间存活的对象, 还在不断往里添加数据: static Map map = xxxx )
    • 缓存尽量靠第三方缓存实现 (如 redis)

2. 新生代调优

(1) 新生代的特点:
  • 所有 new 操作的内存分配非常快速 (在伊甸园区)
  • 死亡对象的回收代价是零 (不需要复制到幸存区, 内存可立即被恢复)
  • 大部分对象用过即死
  • Minor GC的时间远远低于 Full GC
(2) 新生代内存越大越好吗? (-Xmn)

如果新生代内存过大, 老年代内存就相对较小, 如果老年代内存不足, 将触发 Full GC, 花费时间更长. 建议新生代内存设置为整个堆内存的 25% ~ 50%.

(3) 幸存区

需要足够大, 能保留当前活跃对象 + 需要晋升对象.

如果不够大, jdk 会动态降低晋升阈值, 将某些即将被回收的对象晋升到老年代, 这些对象只能靠之后的 Full GC 才能回收, 影响效率.

然而, 晋升阈值需要配置得当, 好让长时间存活的对象尽快晋升.

3. 老年代调优

以 CMS 为例,

  • CMS 的老年代内存越大越好 (因为CMS是并发型, 会产生浮动垃圾, 如果出现并发失败, 将退化为串行)
  • 先不尝试做调优, 如果没有 Full GC, 那么系统运行情况已经很好了, 否则先尝试新生代调优
  • 观察 Full GC 时老年代内存占用, 将老年代内存调大 1/4 ~ 1/3.
    (-XX: CMSInitiatingOccupancyFraction=percent, 设置当老年代内存占用达到多少时进行垃圾回收)

4. 调优案例

(1) Full GC 和 Minor GC 频繁

新生代内存过小 -> 尝试增大新生代内存

(2) 请求高峰期发生Full GC, 单次暂停时间特别长 (CMS)

重新标记时间占用较长, 可在重新标记之前先对新生代对象做一次垃圾回收, 减少需要重新标记的对象个数.
(-XX: +CMSScavengeBeforeRemark)

(3) 在老年代充裕情况下发生了Full GC (CMS jdk 1.7)

jdk 1.7 使用的是永久代而不是元空间, 永久代内存不足时也会发生 Full GC

十. 类加载与字节码

oracle 官网参考

1. 类文件结构

在这里插入图片描述
用一个简单的 HelloWorld.java 来看它的字节码:

javac HellowWorld.java
package cn.itcast.jvm.t5;
public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("Hello World");
	}
}

Linux命令查看class文件:

od -t xC HelloWorld.class
0000000 ca fe ba be 00 00 00 34 00 1d 0a 00 06 00 0f 09
0000020 00 10 00 11 08 00 12 0a 00 13 00 14 07 00 15 07
0000040 00 16 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 04 6d 61 69
0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 0a 53 6f 75
0000160 72 63 65 46 69 6c 65 01 00 0f 48 65 6c 6c 6f 57
0000200 6f 72 6c 64 2e 6a 61 76 61 0c 00 07 00 08 07 00
0000220 17 0c 00 18 00 19 01 00 0b 48 65 6c 6c 6f 20 57
0000240 6f 72 6c 64 07 00 1a 0c 00 1b 00 1c 01 00 1b 63
0000260 6e 2f 69 74 63 61 73 74 2f 6a 76 6d 2f 74 35 2f
0000300 48 65 6c 6c 6f 57 6f 72 6c 64 01 00 10 6a 61 76
0000320 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10
0000340 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d
0000360 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69
0000400 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00
0000420 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74
0000440 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00
0000460 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000500 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
0000520 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1d
0000540 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00
0000560 01 00 0a 00 00 00 06 00 01 00 00 00 02 00 09 00
0000600 0b 00 0c 00 01 00 09 00 00 00 25 00 02 00 01 00
0000620 00 00 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 01
0000640 00 0a 00 00 00 0a 00 02 00 00 00 04 00 08 00 05
0000660 00 01 00 0d 00 00 00 02 00 0e
0000672

(1) 魔数与版本

0-3:魔数,ca fe ba be 表示它是class类型的文件
4-7:表示类的版本 00 34(52)表示是Java 8

(2) 类文件常量池
在这里插入图片描述

8-9:表示常量池长度,00 1d (29)表示常量池有 #1-#28项(#0项不计入)

#1项 0a(10)表示一个Method信息
00 06 和 00 15(21)表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】

#6项 07 表示一个class 信息
00 1c (28) 表示它引用了常量池中 #28 项

#7项 01 表示一个 utf8 串,00 06 表示长度,取后面6个字节,发现是【】

#8项 01 表示一个 utf8 串,00 03 表示长度,取后面3个字节,是【()V】表示无参且没有返回值。

#21项 0c 表示一个【名称+类型】,00 07 00 08 表示引用了常量池中 #7 #8 两项

#28项 01 表示一个 utf8 串,00 10(16)表示长度,则取后面16个字节,发现是【java/lang/Object】

(3) 访问标识与继承信息

21 (20+1)表示该 class 是一个类(20),公共的(1)
在这里插入图片描述
05 表示根据常量池中 #5 找到本类全限定名
06 表示根据常量池中 #6 找到父类全限定名
00 表示接口数量,本类为0

(4) Field 信息

00 表示成员变量的数量,本类为 0
在这里插入图片描述
(5) Method 信息

00 02 表示方法数量,本类为 2(构造方法和main方法)
后面接下来则为方法的信息。一个方法由访问修饰符、名称、参数描述、方法属性数量、方法属性组成。

(6) 附加属性

00 01 表示附加属性数量

2. 字节码指令

(1) javap反编译工具

Oracle提供了javap 工具来反编译 class 文件,无需我们自己解析二进制文件

javap -v HelloWorld.class
Classfile /C:/Users/Desktop/HelloWorld.class
  Last modified 2021-4-11; size 442 bytes
  MD5 checksum 103606e24ec918e862312533fda15bbc
  Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // hello world
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // cn/itcast/jvm/t5/HelloWorld
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               HelloWorld.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               hello world
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               cn/itcast/jvm/t5/HelloWorld
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public cn.itcast.jvm.t5.HelloWorld();
    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 4: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
}
SourceFile: "HelloWorld.java"

(2) 图解方法运行流程

public class Demo() {
	public static void main(String[] args) {
		int a = 10; //小数和字节码文件存放在一起
		int b = Short.MAX_VALUE + 1; // 超过Short范围的数存放在常量池中
		int c = a + b;
		System.out.println(c);
	}
}

栈帧:局部变量表(locals) + 操作数栈(stack)
方法区:存放字节码

bipush 10 :将 10 压入操作数栈中
istore 1:将 10 弹出存入局部变量表的1号位置
ldc #3:从常量池中加载#3项到操作数栈中
……
在这里插入图片描述
(3) 从字节码角度分析a++

package cn.itcast.jvm.t5;
public class HelloWorld {
	public static void main(String[] args) {
		int a = 10;
		int b = a++ + ++a + a--;
		System.out.println(a); // 11
		System.out.println(b); // 34
	}
}

字节码部分文件:

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: bipush        10
         2: istore_1
         3: iload_1
         4: iinc          1, 1   // 1号槽位自增1
         7: iinc          1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc          1, -1
        16: iadd
        17: istore_2
        18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        21: iload_1
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_2
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 4: 0
        line 5: 3
        line 6: 18
        line 7: 25
        line 8: 32

这里注意两点:

  • iinc 指令是直接在局部变量表中进行运算,不加载到操作数栈里;
  • a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc;
    在这里插入图片描述
    (4)条件判断指令
    在这里插入图片描述
public class HelloWorld {
	public static void main(String[] args) {
		int a = 0;
		if(a == 0) {
			a = 10;
		} else {
			a = 20;
		}
	}
}

字节码:

stack=1, locals=2, args_size=1
         0: iconst_0				// 初始化一个0
         1: istore_1				// 将0放入1号槽位
         2: iload_1					// 将0加载到操作数栈里
         3: ifne          12		// 判断是否不等于0,转到12
         6: bipush        10		// 放入一个10
         8: istore_1				// 存入1号槽位
         9: goto          15		// 转到15
        12: bipush        20
        14: istore_1
        15: return

(5)循环控制指令

public class HelloWorld {
	public static void main(String[] args) {
		int a = 0;
		while(a < 10) {
			a++;
		}
	}
}
// 或
public class HelloWorld {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++){
		}
	}
}
 	0: iconst_0
	1: istore_1
	2: iload_1					// 0
	3: bipush        10			// 10
	5: if_icmpge     14			// 0 > 10 ?
	8: iinc          1, 1		// 1号槽位自增1
	11: goto          2			// 跳转到 2
	14: return

(6)分析判断 x=x++

public class HelloWorld {
	public static void main(String[] args) {
		int i = 0;
		int x = 0;
		while(i < 10) {
			x = x++;
			i++;
		}
		System.out.println(x); // 0
	}
}
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1					// 加载 0(i)到操作数栈
5: bipush        10			// 加载 10 到操作数栈
7: if_icmpge     21			// 0 > 10 ?
10: iload_2					// 加载 0 (x)
11: iinc          2, 1		// 2号槽位自增1
14: istore_2				// 将 0 (x) 放回2号槽位
15: iinc          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

(7)构造方法指令

// 最后初始化完成 i = 30,按照顺序进行构造
public class HelloWorld {
	static int i = 10;
	static {
		i = 20;
	}
	static {
		i = 30;
	}
}
0: bipush        10
2: putstatic     #2                  // Field i:I
5: bipush        20
7: putstatic     #2                  // Field i:I
10: bipush        30
12: putstatic     #2                  // Field i:I
15: return

(8)异常指令

多出一个 Exception table 结构,[from, to)为前开后闭的检测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示行号。如果异常没有匹配成功,还会继续抛出。

finally中的字节码会复制分别放在 try 和 catch 之后。

例1:

public class HelloWorld {
	public static void main(String[] args) {
		int result = test();
		System.out.println(result);
	}
	
	public static int test() {
		try {
			return 10;
		} finally {
			return 20; //将返回20
		}
	}
}
  • 字节码如下,返回结果以 finally 为准。
  • 另外,特别注意,本来应该有的 athrow 字节码(当异常没有匹配上时要抛出)丢失了。这告诉我们:如果在 finally 中出现了 return,会吞掉异常!!!
stack=1, locals=2, args_size=0
         0: bipush        10
         2: istore_0			// 10被存入0号槽位
         3: bipush        20
         5: ireturn				// 返回栈顶的20
         6: astore_1
         7: bipush        20
         9: ireturn
      Exception table:
         from    to  target type
             0     3     6   any

例2:

public class HelloWorld {
	public static void main(String[] args) {
		int result = test();
		System.out.println(result);
	}
	
	public static int test() {
		int i = 10;
		try {
			return i;
		} finally {
			i = 20;
		}
	}
}
  • finally 对返回值的修改不会影响实际的返回结果。
stack=1, locals=3, args_size=0
         0: bipush        10
         2: istore_0
         3: iload_0				// 将 10 加载到栈顶 
         4: istore_1			// 将 10 暂存入 1 号槽位,目的是为了固定返回值
         5: bipush        20
         7: istore_0
         8: iload_1
         9: ireturn
        10: astore_2
        11: bipush        20
        13: istore_0
        14: aload_2
        15: athrow
      Exception table:
         from    to  target type
             3     5    10   any

(9)synchronized 指令

问题:synchronized 是如何实现加锁的对象即使是在代码发生异常时也一定会被解锁呢?

public class HelloWorld {
	public static void main(String[] args) {
		Object lock = new Object();
		synchronized(lock) {
			System.out.println("ok");
		}
	}
}

通过异常表,在抛出异常时也确保执行解锁操作:

stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: aload_1
         9: dup						// 这里将 lock 引用又复制了一份
        10: astore_2				// 将复制的引用存入 2 号槽位(栈中的复制引用则被去除)
        11: monitorenter			// 对原本的lock引用(在栈顶)加锁
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2
        21: monitorexit				// 解锁
        22: goto          30
        25: astore_3				// 将 any 异常存入 3号槽位
        26: aload_2					// 加载复制的 lock 引用
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值