六、GC 日志分析


一、GC 日志

  • 可以通过配置把 Java 程序运行过程中的 GC 日志全部打印出来。
  • 分析 GC 日志得到关键性指标,GC 原因,实现 JVM 参数调优。
# 打印`GC`日志参数,在JVM参数里增加参数
‐XX:+PrintGCDetails \
‐XX:+PrintGCTimeStamps \
‐XX:+PrintGCDateStamps \
‐Xloggc:./gc.log

1. 日志分析

  • gc-2022-05-17_09-51-32.log
    在这里插入图片描述
  1. 项目配置的参数(这里不仅配置了打印 GC 日志,还有 JVM 默认的相关内存参数)。
  2. 发生一次 FullGC 的相关情况。
# 指`GC`的发生时间。
2022-05-17T09:51:42.702+0800: 
# 指`GC`的发生时间点(这个时间戳是从`JVM`启动开始计算的)。
9.920: 
# 指发生一次`FullGC`(括号里是`GC`的原因)。
[Full GC (Metadata GC Threshold) 
	# 指年轻代的`GC`。
	# `GC`前年轻代占用`528k`,`GC`后年轻代占用`0k`,整个年轻代大小`14336k`。
	[PSYoungGen: 528K->0K(14336K)] 
	# 指老年代的`GC`。
	[ParOldGen: 12856K->5857K(40960K)] 
	# `GC`前堆占用`13385K`,`GC`后堆占用`5857K`,整个堆大小`55296K`。
	13385K->5857K(55296K), 
	# 指元空间的`GC`。
	[Metaspace: 20193K->20193K(1067008K)], 
# 指本次`GC`总耗费时间。
0.0539936 secs] 
[Times: user=0.06 sys=0.02, real=0.05 secs] 

2. 可视化日志分析

  1. 下图可以看到 年轻代、老年代,以及 永久代 的内存分配,和最大使用情况。
    在这里插入图片描述
    在这里插入图片描述

  1. 下图可以看到堆内存在 GC 前和后的变化,以及其他信息。
    在这里插入图片描述

  1. 还提供基于机器学习的 JVM 智能优化建议(现在这个功能需要付费)。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

二、Class 常量池

  • Class 常量池可以理解为 Class 文件中的资源仓库。
  • Class 文件中除了包含类的 版本、字段、方法、接口 等描述信息外,还有一项信息就是常量池(Constant pool table)。
  • 常量池用于存放编译器生成的各种 字面量(Literal)和 符号引用(Symbolic References

  1. 下图是一个 Class 文件的十六进制文件。
    在这里插入图片描述
# 魔数		次版本	主版本(转换十进制52,指JDK-1.8)	常量池计数	常量池数据区
cafe babe 	0000 	0034							0067 		0a00 1900 4803

1. javap 命令

javap -v User.class
  • javap 命令生成更可读的 JVM 字节码指令文件。
  1. 红框标出的就是 Class 常量池信息。
  2. 常量池中主要存放两大类常量:字面量 和 符号引用
    在这里插入图片描述

2. 字面量

  • 字面量指由 字母、数字 等构成的字符串或者数值常量。
  • 字面量只可以右值出现,所谓右值是指等号右边的值。
    如:int a=1 这里的 a 为左值,1为右值,在这个例子中 1 就是字面量。
int a = 1;
int b = 2;
int c = "abcd";

3. 符号引用

  • 符号引用指编译原理中的概念,是相对于 直接引用 来说的。
  • 主要包括了以下三类常量。
  1. 类和接口的全限定名。
  2. 字段的名称和描述符。
  3. 方法的名称和描述符。

int a = 1;
int b = 2;
int c = "abcd";
  • 上面的 a,b 是字段名称,就是一种 符号引用
  • 比如:Math 类常量池中,下面这些都是 符号引用
  1. com.qs.javajvm.jvm_command.Math 是类的全限定名。
  2. main 和 compute 是方法名称。
  3. () 是一种 UTF8 格式的描述符。

  • 这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息。
  • 这些常量池一旦被装入内存就变成 运行时常量池,对应的符号引用在程序加载或运行时,会被转变为被加载到内存区域的代码的直接引用,也就是我们说的 动态链接
  • 例如:compute() 这个符号引用,在运行时就会被转变为 compute() 方法具体代码在内存中的地址,主要通过 对象头 里的 类型指针 去转换 直接引用

  • Jdk-1.6 及之前:有永久代, 常量池在方法区。
  • Jdk-1.7:有永久代,但已经逐步去永久代,常量池在堆。
  • Jdk-1.8 及之后:无永久代,常量池在元空间。

三、字符串常量池

字符串常量池的设计思想。

  • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。
  • JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。
  1. 为字符串开辟一个字符串常量池,类似于缓存区。
  2. 创建字符串常量时,首先坚持字符串常量池是否存在该字符串。
  3. 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。

  • 一些字符串局部变量操作。
String str1 = "abc";
String str2 = "abc";
String str3 = "abc";
String str4 = new String("abc");
String str5 = new String("abc");
String str5 = new String("abc");

在这里插入图片描述


1. 字面量面试题

  • 面试题:String str = new String(“abc”) 创建多少个对象?
  1. 在常量池中查找是否有 “abc” 对象。
  1. 有则返回对应的引用实例。
  2. 没有则在常量池中创建对应的实例对象。
  1. 在堆中 new 一个 String(“abc”) 对象。
  2. 将对象地址赋值给 str,创建一个引用。
  • 所以,常量池中没有 “abc” 字面量则创建两个对象。
    否则创建一个对象,以及创建一个引用。

  • 面试题:String str1 = new String(“A” + “B”) 会创建多少个对象?
  1. 字符串常量池:3个,“A”、“B”、“AB”。
  2. 堆:1个,new String(“AB”)。
  3. 引用:1个,str1。
  • 总共:5个。

  • 面试题:String str2 = new String(“ABC”) + “ABC” 会创建多少个对象?
  1. 字符串常量池:1个,“ABC”。
  2. 堆:1个,new String(“ABC”)。
  3. 引用:1个,str2。
  • 总共:3个。

2. 操作字符串常量池的方式

  • JVM 实例化字符串常量池时。
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);
// true

  • String.intern()
  1. 通过 new 操作符创建的字符串对象不指向字符串常量池中的任何对象,但是可以通过使用字符串的 intern() 方法来指向其中的某一个。
  2. java.lang.String.intern() 返回一个常量池里面的字符串,就是一个在字符串常量池中有了一个入口。
  3. 如果以前没有在字符串常量池中,那么它就会被添加到里面。
String s1 = "Hello";
String s2 = new String("Hello");
String s3 = s2.intern();

System.out.println(s1 == s2); 
// false
System.out.println(s1 == s3); 
// true

四、八种基本类型的 包装类 和 对象池

  • Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 ByteShortIntegerLongCharacterBoolean
  • 另外两种 浮点数类型 的包装类则没有实现。
  • 另外 ByteShortIntegerLongCharacter 这 5 种整型的包装类也只是在对应值小于等于 127 时才可使用对象池,即对象不负责创建和管理大于 127 的这些类的对象。
/**
 * @author wy
 * describe 八种基本类型的 包装类 和 对象池
 */
public class ObjectPool {

    @Test
    public void test() {
        // `5`种整形的包装类 Byte、Short、Integer、Long、Character 的对象。
        // 在值小于`127`时可以使用常量池。
        Integer i1 = 127;
        Integer i2 = 127;
        System.out.println(i1 == i2);
        // true

        // 值大于`127`时,不会从常量池中取对象。
        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4);
        // false

        // `Boolean`类也实现了常量池技术。
        Boolean bool1 = true;
        Boolean bool2 = true;
        System.out.println(bool1 == bool2);
        // true

        // 浮点类型的包装类没有实现常量池技术。
        Double d1 = 1.0;
        Double d2 = 1.0;
        System.out.println(d1 == d2);
        // false
    }
}

五、安全点与安全区域

  • 安全点就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样 JVM 就可以安全的进行一些操作。
    比如:GC 等,所以 GC 不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。
  • 这些特定的安全点位置主要有以下几种。
  1. 方法返回之前。
  2. 调用某个方法之后。
  3. 抛出异常的位置。
  4. 循环的末尾。

  • 安全区域又是什么?
  1. Safe Point 是对正在执行的线程设定的。
  2. 如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。
  3. 因此 JVM 引入了 Safe Region。
  4. Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
  5. 线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑士梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值