一、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
- 项目配置的参数(这里不仅配置了打印
GC
日志,还有JVM
默认的相关内存参数)。- 发生一次
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. 可视化日志分析。
- 下图可以看到 年轻代、老年代,以及 永久代 的内存分配,和最大使用情况。
- 下图可以看到堆内存在
GC
前和后的变化,以及其他信息。
- 还提供基于机器学习的
JVM
智能优化建议(现在这个功能需要付费)。
二、Class 常量池
Class
常量池可以理解为Class
文件中的资源仓库。Class
文件中除了包含类的 版本、字段、方法、接口 等描述信息外,还有一项信息就是常量池(Constant pool table
)。- 常量池用于存放编译器生成的各种 字面量(
Literal
)和 符号引用(Symbolic References
)。
- 下图是一个
Class
文件的十六进制文件。
# 魔数 次版本 主版本(转换十进制52,指JDK-1.8) 常量池计数 常量池数据区
cafe babe 0000 0034 0067 0a00 1900 4803
1. javap
命令
javap -v User.class
javap
命令生成更可读的JVM
字节码指令文件。
- 红框标出的就是
Class
常量池信息。- 常量池中主要存放两大类常量:字面量 和 符号引用。
2. 字面量
- 字面量指由 字母、数字 等构成的字符串或者数值常量。
- 字面量只可以右值出现,所谓右值是指等号右边的值。
如:int a=1 这里的 a 为左值,1为右值,在这个例子中 1 就是字面量。
int a = 1;
int b = 2;
int c = "abcd";
3. 符号引用
- 符号引用指编译原理中的概念,是相对于 直接引用 来说的。
- 主要包括了以下三类常量。
- 类和接口的全限定名。
- 字段的名称和描述符。
- 方法的名称和描述符。
int a = 1;
int b = 2;
int c = "abcd";
- 上面的 a,b 是字段名称,就是一种 符号引用。
- 比如:Math 类常量池中,下面这些都是 符号引用。
- com.qs.javajvm.jvm_command.Math 是类的全限定名。
- main 和 compute 是方法名称。
- () 是一种 UTF8 格式的描述符。
- 这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息。
- 这些常量池一旦被装入内存就变成 运行时常量池,对应的符号引用在程序加载或运行时,会被转变为被加载到内存区域的代码的直接引用,也就是我们说的 动态链接。
- 例如:compute() 这个符号引用,在运行时就会被转变为 compute() 方法具体代码在内存中的地址,主要通过 对象头 里的 类型指针 去转换 直接引用。
- Jdk-1.6 及之前:有永久代, 常量池在方法区。
- Jdk-1.7:有永久代,但已经逐步去永久代,常量池在堆。
- Jdk-1.8 及之后:无永久代,常量池在元空间。
三、字符串常量池
字符串常量池的设计思想。
- 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。
- JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。
- 为字符串开辟一个字符串常量池,类似于缓存区。
- 创建字符串常量时,首先坚持字符串常量池是否存在该字符串。
- 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。
- 一些字符串局部变量操作。
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”) 创建多少个对象?
- 在常量池中查找是否有 “abc” 对象。
- 有则返回对应的引用实例。
- 没有则在常量池中创建对应的实例对象。
- 在堆中 new 一个 String(“abc”) 对象。
- 将对象地址赋值给 str,创建一个引用。
- 所以,常量池中没有 “abc” 字面量则创建两个对象。
否则创建一个对象,以及创建一个引用。
- 面试题:String str1 = new String(“A” + “B”) 会创建多少个对象?
- 字符串常量池:3个,“A”、“B”、“AB”。
- 堆:1个,new String(“AB”)。
- 引用:1个,str1。
- 总共:5个。
- 面试题:String str2 = new String(“ABC”) + “ABC” 会创建多少个对象?
- 字符串常量池:1个,“ABC”。
- 堆:1个,new String(“ABC”)。
- 引用:1个,str2。
- 总共:3个。
2. 操作字符串常量池的方式
- JVM 实例化字符串常量池时。
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);
// true
String.intern()
。
- 通过 new 操作符创建的字符串对象不指向字符串常量池中的任何对象,但是可以通过使用字符串的
intern()
方法来指向其中的某一个。java.lang.String.intern()
返回一个常量池里面的字符串,就是一个在字符串常量池中有了一个入口。- 如果以前没有在字符串常量池中,那么它就会被添加到里面。
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 中基本类型的包装类的大部分都实现了常量池技术,这些类是
Byte
、Short
、Integer
、Long
、Character
、Boolean
。- 另外两种 浮点数类型 的包装类则没有实现。
- 另外
Byte
、Short
、Integer
、Long
、Character
这 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 不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。- 这些特定的安全点位置主要有以下几种。
- 方法返回之前。
- 调用某个方法之后。
- 抛出异常的位置。
- 循环的末尾。
- 安全区域又是什么?
- Safe Point 是对正在执行的线程设定的。
- 如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。
- 因此 JVM 引入了 Safe Region。
- Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
- 线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。