1、程序计数器
1.1、定义
Program Counter Register 程序计数器 ,通过计算机CPU的寄存器(速度极快)实现,也是对计算机物理硬件的屏蔽与抽象
1.2、作用
是记住下一条JVM指令的执行地址
二进制字节码(前方的数字可看作指令的地址) Java源代码
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
- 上面右侧的Java源代码编译后会编程左边的二进制字节码,它们都是JVM中的一系列指令,而Java跨平台的基础就是这一系列的二进制指令,所有平台都长这样
- 这些指令还不能直接交给CPU来执行,还必须经过解释器(JVM执行引擎之一,负责把每一个JVM指令解释为机器码,机器码便可以交给CPU直接执行)
- 如第一行,当getstatic指令放入解释器执行后,程序计数器便会将astore_1指令的地址3放入,这样重复的执行操作
1.3、特点
- 是线程私有的,每一个线程都会有自己的程序计数器
- 当一个线程时间片用完后,当前执行到哪一行指定,会在程序计数器中保存下来,然后切换到另一个线程执行,当这个线程再次执行的时候,就还能根据当前程序计数器中保存的指令行,来决定从哪里开始执行
- 是JVM规范中唯一一个不会存在内存溢出的区域
2、虚拟机栈
2.1、定义
Java Virtual Machine Stacks (Java 虚拟机栈) ,即线程运行所需要的内存空间
- 栈帧(Frame):线程执行代码的时候是从上而下的,而代码都是由一个个方法组成,所以我们将一个个方法所需要的内存空间称为一个个栈帧,而Java虚拟机栈中存放的单位便是栈帧,栈帧中包含参数,局部变量,返回地址,当一个方法被调用后,就会将其压入Java虚拟机栈,当这个方法执行完毕,就会释放这个方法所占用的内存,让其出栈,如果一个方法A压栈后,其中又调用了另一个方法B,然后又会将方法B压栈,如果方法B中又调用了另一个方法C,同样还会将方法C压栈…,最后从栈顶开始调用方法链执行,…方法C执行完毕出栈,执行方法B,执行完毕出栈,执行方法A,执行完毕再出栈。每个线程都只能有一个活动栈帧,对应当前正在执行的那个方法
2.2、演示栈帧
使用下面示例代码,将断点打到main方法里
/**
* @author PengHuanZhi
* @date 2021年12月08日 18:41
*/
public class Frames {
public static void main(String[] args) throws InterruptedException {
method1();
}
private static void method1() {
int i = method2(1, 2);
System.out.println(i);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
观察调试控制台
图中的Frames便是对应于咱们的Java虚拟机栈帧,当前代码执行到Main方法,还未进入method1方法,所以栈帧集合只有一个main方法,现在进入method1,再次观察
同理,再次进入method2
从method2方法中出来,再观察,可以发现,method2被弹出去啦
2.3、几个问题
垃圾回收是否设计栈内存?
- 栈内存即一次次的方法调用,所产生的栈帧内存,而栈帧内存在每一次方法调用完毕都会弹出栈,会被自动回收掉,所以根部不需要垃圾回收去管理栈内存
栈内存分配越大越好么?
- 对于Java程序,我们可以手动指定Java的栈内存大小:
-Xss1m
-Xss1024k
-Xss1048576
- 强调,栈,划分的越多,反而会让线程数逐渐减少,因为物理内存的大小是一定的,如果一个线程给它分配的内存越多,那么整个物理内存所能承载的线程数就会变少,划分的多了,只能提升方法递归调用的能力,不会提高运行效率,一般使用默认的栈内存大小就可以了
方法内的局部变量是否线程安全?
- 要考虑这个问题,可以参考这个变量是否是多个线程所共享的即可,对于每一个线程执行同一个方法时,都会在自己的栈中新建一个对应的栈帧, 而各个栈帧所用内存是独立唯一的,其中都会存在一个独立的局部变量,所以方法内部的局部变量是线程安全的。如果变量是static的,则每一个栈帧都共享此变量,则会出现线程安全的问题,下面我们看一段代码,再思考一下
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
- m1根据上面的结论,很容易判断,是线程安全的
- m2,由于StringBuilder对象是一个方法参数,它是通过其他地方传递过来的,所以它可能会被多个线程使用到,故而不是线程安全的
- m3中是StringBuilder被当作返回出返回了,则它可能会被其他线程接收访问,故而也不是线程安全的
结论那是:如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
2.4、栈内存溢出
在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
2.5、线程运行诊断
案例1、CPU占用过多
/**
* @author PengHuanZhi
* @date 2021年12月09日 9:37
*/
public class WhileTrueDemo {
public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("1...");
while (true) {
}
}, "thread1").start();
new Thread(null, () -> {
System.out.println("2...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();
new Thread(null, () -> {
System.out.println("3...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread3").start();
}
}
- 将上面代码放在一个Linux操作系统环境下,然后编译运行:
javac WhileTrueDemo.java
java WhileTrueDemo
- top命令观察系统Cpu占用情况:
- 可以看到是java的进程占用CPU近百分百,使用ps命令查看哪一个线程占用这么高
ps H -eo pid,tid,%cpu
- 可以看到是7048进程的7063线程导致CPU占用近百分百,使用jstack+进程id找到问题的进程,进一步找到问题代码的源码行号
jstack 7048
- 由于jstack显示的线程号是16进制,所以这里将7063转换为16进制:1B97
案例2、程序运行很长时间没有结果
/**
* @author PengHuanZhi
* @date 2021年12月09日 12:52
*/
public class DeadLockDemo {
static A a = new A();
static B b = new B();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (a) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (b) {
synchronized (a) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
}
}
class A {
};
class B {
};
- 同样将上述代码放在Linux操作系统中,然后先编译(javac)再运行(使用nohub java可以直接返回进程号,然后后台执行)
javac DeadLockDemo.java
nohub java DeadLockDemo &
- 再次使用jstack观察:
jstack 7756
3、本地方法栈
Java虚拟机调用本地方法时,需要给本地方法提供的一块内存空间,本地方法(Native method),不是由Java代码编写的,Java是不能直接和操作系统底层API打交道,需要依赖使用C/C++等语言编写的方法来与操作系统API交互
- 本地方法都是使用native关键字修饰的,比如Object类中的clone方法
4、堆
4.1、定义
堆(Heap):通过 new 关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 当对象不再被人使用,就会被垃圾回收机制回收
4.2、堆内存溢出
/**
* @author PengHuanZhi
* @date 2021年12月09日 13:10
*/
public class OutOfMemoryDemo {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
执行后观察控制台:
-
可以看见由于堆空间内存不足导致了内存溢出
-
也可以通过调整系统参数,动态指定堆内存大小
-Xmx2048m
对于线上服务器的内存一般会很大,在项目初期可能不太会出现内存溢出的问题,如果想要尽早暴露出这个问题,可以先尝试将堆内存设置的小一些
4.3、堆内存诊断
堆内存诊断命令大致有如下几个
-
jps
- jps可以查看当前系统中有那些Java进程
-
jmap
- 可以查看堆内存占用情况
-
jconsole
- 带有图形化界面,是一个多功能的检测工具,可以连续检测
使用下面的Demo我们来实战一下
/**
* @author PengHuanZhi
* @date 2021年12月09日 13:21
*/
public class MonitorDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
// 10 Mb
byte[] array = new byte[1024 * 1024 * 10];
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}
4.3.1、jmap
- 第一个sleep睡眠30S之前,我们将当前进程占用Id记下来,并用jmap对这个进程的堆内存情况记录一下
PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18808
Attaching to process ID 18808, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12
using thread-local object allocation.
Parallel GC with 10 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 8568963072 (8172.0MB)
NewSize = 178782208 (170.5MB)
MaxNewSize = 2856321024 (2724.0MB)
OldSize = 358088704 (341.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 134742016 (128.5MB)
used = 10779448 (10.280082702636719MB)
free = 123962568 (118.21991729736328MB)
8.000064360028574% used
From Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
To Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
PS Old Generation
capacity = 358088704 (341.5MB)
used = 0 (0.0MB)
free = 358088704 (341.5MB)
0.0% used
3174 interned Strings occupying 260400 bytes.
- 等待第一次睡眠结束第二次睡眠开始,再次用jmap查看一下堆内存使用情况
PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18808
Attaching to process ID 18808, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12
using thread-local object allocation.
Parallel GC with 10 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 8568963072 (8172.0MB)
NewSize = 178782208 (170.5MB)
MaxNewSize = 2856321024 (2724.0MB)
OldSize = 358088704 (341.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 134742016 (128.5MB)
used = 21265224 (20.28009796142578MB)
free = 113476792 (108.21990203857422MB)
15.782177401887768% used
From Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
To Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
PS Old Generation
capacity = 358088704 (341.5MB)
used = 0 (0.0MB)
free = 358088704 (341.5MB)
0.0% used
3175 interned Strings occupying 260448 bytes.
- 最后一次sleep时,再次用jmap查看一下
PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18808
Attaching to process ID 18808, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12
using thread-local object allocation.
Parallel GC with 10 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 8568963072 (8172.0MB)
NewSize = 178782208 (170.5MB)
MaxNewSize = 2856321024 (2724.0MB)
OldSize = 358088704 (341.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 134742016 (128.5MB)
used = 2694856 (2.5700149536132812MB)
free = 132047160 (125.92998504638672MB)
2.000011637053137% used
From Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
To Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
PS Old Generation
capacity = 358088704 (341.5MB)
used = 973384 (0.9282913208007812MB)
free = 357115320 (340.5717086791992MB)
0.2718276195609901% used
3161 interned Strings occupying 259456 bytes.
4.3.2、jconsole
- 还是这个Demo,重新运行,然后在控制台输入jconsole,在弹出的页面选择本地进程Demo进程,然后直接点击连接
- 然后再选择不安全的连接
- 就可以动态的观察当前堆内存的使用情况了
4.3.3、jvisualvm
有些情况下,在垃圾回收后,内存占用仍然很高,接下来,我们运行下面的Demo,然后使用jps+jmap查看一下当前堆内存使用情况,我们需要关注的是最后一行PS Old Generation(后面会解释,这里先略过)
PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18848
Attaching to process ID 18848, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12
using thread-local object allocation.
Parallel GC with 10 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 8568963072 (8172.0MB)
NewSize = 178782208 (170.5MB)
MaxNewSize = 2856321024 (2724.0MB)
OldSize = 358088704 (341.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 134742016 (128.5MB)
used = 108979624 (103.93106842041016MB)
free = 25762392 (24.568931579589844MB)
80.88020888747872% used
From Space:
capacity = 22020096 (21.0MB)
used = 21815664 (20.805038452148438MB)
free = 204432 (0.1949615478515625MB)
99.07161167689732% used
To Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
PS Old Generation
capacity = 358088704 (341.5MB)
used = 105915984 (101.00935363769531MB)
free = 252172720 (240.4906463623047MB)
29.578141621579885% used
5442 interned Strings occupying 449456 bytes.
- 再使用jconsole工具,我们选择内存页面,点击执行GC垃圾回收
- 再次观察jmap日志的PS Old Generation使用情况,发现还是占用非常高
PS D:\IDEA WorkSpace\JVMProject> jmap -heap 18848
Attaching to process ID 18848, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12
using thread-local object allocation.
Parallel GC with 10 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 8568963072 (8172.0MB)
NewSize = 178782208 (170.5MB)
MaxNewSize = 2856321024 (2724.0MB)
OldSize = 358088704 (341.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 269484032 (257.0MB)
used = 9529640 (9.088172912597656MB)
free = 259954392 (247.91182708740234MB)
3.536254051594419% used
From Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
To Space:
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
PS Old Generation
capacity = 358088704 (341.5MB)
used = 212021304 (202.19927215576172MB)
free = 146067400 (139.30072784423828MB)
59.20915729304882% used
5502 interned Strings occupying 454520 bytes.
- 可以看到垃圾回收后,堆内存还是没有得到有效释放,这时候借助另一个工具jvisualvm,在控制台输入该命令,然后在弹出的页面选择Demo进程
- 可以看到这个工具也可以以图像形式动态展示堆内存使用情况,点击右边的堆Dump,可以对当前堆内存使用情况做一个快照,然后在快照中检查堆中对象的使用情况
- 可以看到其中的ArrayList占用内存很高
- 点击查看详情
- 可以看到一个元素就有1M,总计是200M左右
5、方法区
5.1、定义
网上对方法区的定义不太清晰,这里给出官方权威的定义:点击跳转
将其大致理解归纳如下
- 所有线程所共享
- 存储了类的结构的相关信息,包含,类的成员变量,方法数据,成员方法,构造方法的代码,以及运行时常量池(后面会单独说明)
- 方法区会在JVM启动的时候创建
- 逻辑上面其实是堆的组成部分(但是并不强制定义方法区的位置,所以不同的JVM实现厂商的实现方式也不一样,所以只能说是逻辑上)
- 如果创建方法区的时候,内存不够了,也会抛出OutOfMemory堆栈溢出错误
5.2、组成
这里以Oracle的Hotspot JVM为例
- 1.6方法区采用永久代实现,里面会保存一个类中的若干信息,其中的StringTable常量表(后面会讲)保存在运行时常量池中
- 1.8后方法区采用了元空间实现,它保存在了操作系统的本地内存中,其中类的若干信息也随之保存在本地内存中,但是StringTable常量表确保存在了堆内存中
5.3、方法区内存溢出
- 1.8以前会导致永久代内存溢出
- 1.8以后会导致元空间内存溢出
我们使用下面的代码来演示
/**
* @author PengHuanZhi
* @date 2021年12月09日 20:33
*/
public class MethodOutOfMemory extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
MethodOutOfMemory test = new MethodOutOfMemory();
for (int i = 0; i < 100000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
- 继承ClassLoader可以使我们的类可以用代码来加载类的字节码
- ClassWriter可以用来生成类的字节码
参数意义分别是
- Opcodes.V1.8标识Java版本号
- Opcodes.ACC_PUBLIC标识访问修饰符为public
- “Class”+i为类的名称
- null包名
- **“java/lang/Object”**类的父类
- null要实现的接口名称
由于当下我运行的环境为JDK1.8,方法区使用的是操作系统的元空间,内存会直接使用我们操作系统的物理内存,很难演示内存溢出的问题,我们可以配置一个系统参数来限制最大元空间大小:
-XX:MaxPermSize=16m
- 如果是1.8之前采用的永久代实现,则配置参数会有变化:
-XX:MaxPermSize=16m
5.4、运行时常量池
给出如下一个简单打印表达式
public static void main(String[] args) {
System.out.println("HelloWorld!");
}
- 这段代码运行之前,肯定需要编译为一个二进制字节码,其中大致有三部分组成,如类基本信息,类方法定义,常量池
讲编译后的class文件使用javap反编译:
javap -v HelloWorld
结果如下
Classfile /root/HelloWorld.class
Last modified 2021-12-9; size 425 bytes
MD5 checksum a3776b6ec5315d41074a229033ec3c42
Compiled from "HelloWorld.java"
//类方法定义
public class 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 // HelloWorld!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // 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 HelloWorld!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 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 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 1: 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 HelloWorld!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "HelloWorld.java"
定义:
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息
- 运行时常量池,常量池是 .class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址(如*#1,#2**)变为真实地址
5.5、StringTable
俗称串池
5.5.1、常量池与串池的关系
package com.phz;
/**
* @author PengHuanZhi
* @date 2021年12月09日 21:19
*/
public class Demo {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
讲上述代码编译然后再反编译:
- 常量池:
- 程序指令
- 常量池最初存在于字节码文件中,当其运行时,就会被加载进入运行时常量池中,但只是加载进入运行时常量池,这些信息还没有成为一个对象,仅仅为运行时常量池中的符号,当程序去引用它时,如String s1 = “a”,才会把a符号变为一个字符串对象,然后再准备一个空间StringTable,讲刚才的符号存入StringTable中(用到了才会创建,类似于懒加载),最后就会是:[“a”,”b”,”ab”]
5.5.2、字符串变量拼接
将上一个Demo中添加一行
package com.phz;
/**
* @author PengHuanZhi
* @date 2021年12月09日 21:19
*/
public class Demo {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
}
}
同样进行编译反编译观察
我们进入StringBuilder类的toString方法,可以看到,StringBuilder最终是new出来的一个新的String对象
也就是说,之前的s3,是存在于我们的串池中,而新的s4,因为是new出来的,所以是存在于堆中,两者虽然都是ab,但是如果用s3 == s4 应该返回false
5.5.3、编译期优化
现在再将上面加的那行代码改为
String s4 = “a” + “b”;
String s4 = “a” + “b”;
编译后再次反编译查看
可以发现,现在就直接就去常量池中找已经存在的ab,这时候调用s3 == s4 返回的则是true,这其实是Javac在编译期间的优化,因为结果已经在编译期间被确定了,都是常量,而上一步的s1+s2都是变量,所以需要重新new
5.5.4、字符串延迟加载
执行下面这段代码
public static void main(String[] args) {
System.out.println();
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("1");
System.out.println("2");
System.out.println("3");
}
使用IDEA自带的断点调试工具,开启调试,程序执行打印1前,观察内存中的String个数为2131
然后走完前三行代码
再将代码走完
也就是说前面1~3已经在串池中创建好了,后面再使用就不会创建了
5.5.5、intern
可以主动将串池中还没有的字符串对象主动放入串池
public static void main(String[] args) {
//new 出来的在堆中
String s1 = new String("a");
String s2 = new String("b");
//调用了StringBuilder的append两次,然后调用toString来创建一个String赋予s3
String s3 = s1 + s2;
//串池中ab不等于堆中的s3
//System.out.println(s3 == "ab");
//尝试将ab放入串池,如果有则不会放入,如果没有则会放入,不管有无都会将串池中的对象返回出来
String intern = s3.intern();
//相等
System.out.println(intern == "ab");
}
如果有则不会放入,如果没有则会放入,不管有无都会将串池中的对象返回出来
区别如下:
- JDK1.8之后:如果没有,放入串池,那么**s3 == “ab”**返回真,如果已经有了,就不会再放,**s3 == “ab”**则会返回假
- JDK1.8之前:如果没有,先new一个新的,将新的放入串池,那么**s3 == “ab”**返回真,如果已经有了,就不会再放,**s3 == “ab”**则会返回假
5.6、StringTable面试题
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab 串池中
String s4 = s1 + s2; // StringBuilder =》 new String("ab") 堆中
String s5 = "ab"; //串池
String s6 = s4.intern();//ab已存在串池,s4仍然在堆,s6为串池中的ab
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();
String x1 = "cd";
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
}
5.7、StringTable位置
- 永久代的垃圾回收效率很低,只有在Full GC(老年代空间不足)的时候才会触发,由于一个应用程序中常量会有很多,都放在了串池中,如果回收效率不高,很容易造成内存不足的情况
- 而1.8将串池放在了堆中,堆的垃圾回收在Minor GC的时候就会触发,效率会高很多
5.8、StringTable垃圾回收
运行如下Demo
/**
* @author PengHuanZhi
* @date 2021年12月10日 15:48
*/
public class StringTableGCDemo {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
运行之前,我们添加几个运行参数:
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
其中:
- -Xmx10m:设置虚拟机堆内存最大值
- -XX:+PrintStringTableStatistics:打印串池的统计信息
- -XX:+PrintGCDetails和 -verbose:gc:可以显示垃圾回收的详细信息
观察一下输出结果
初始的一些字符串常量会有方法名,类名等信息
现在我们在Try-Catch语句中加入下面代码
for (int j = 0; j < 100; j++) {
String.valueOf(j).intern();
i++;
}
再次观察控制台
此时还没有触发垃圾回收,尝试将循环次数增加至10000,按道理这里的Entry应该变为一万多,再次观察
可以发现,当分配失败后,会触发垃圾回收
5.9、StringTable性能调优
因为StringTable底层采用Hash表存储的,所以如果想要StringTable的性能更高,可以尝试将Hash桶的个数变多,这样Hash碰撞的几率就会更小,链表长度也会更短,调整StringTable的Hash桶个数也是需要配置虚拟机参数的:
-XX:StringTableSize=桶个数
一般如果程序中常量非常多,那么调整StringTable的桶个数是很有必要的
还有一种情况就是,程序中出现了大量的字符串,而这些字符串可能会出现重复的问题,那么我们可以使用intern将其入池
6、直接内存
6.1、定义
Direct Memory,常见于NIO操作,用于数据缓冲区,分配回收成本较高,但是读写性能高,不受JVM内存回收管理,所以需要合理使用,否则容易造成内存泄漏
6.2、直接内存与GC
使用下面的Demo我们来演示一下如何使用直接内存
/**
* @author PengHuanZhi
* @date 2021年12月10日 20:32
*/
public class DirectGCDemo {
//1G
static int SIZE = 1024 * 1024 * 1024;
/**
* -XX:+DisableExplicitGC 显式的
*/
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(SIZE);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
// 显式的垃圾回收,Full GC
System.gc();
System.in.read();
}
}
将程序运行起来,观察系统任务管理器:
控制台回车,开始执行垃圾回收,观察该直接内存会不会被释放
发现被释放了,不是直接内存不会被GC管理吗,怎么还被释放了
为了解释这个问题,这里先介绍一下JDK中一个很底层的类UnSafe,它参与我们JDK分配和释放直接内存的工作,但是它不能直接拿来使用,毕竟只是JDK内部自己使用的东西,我们做程序一般情况不要用它,它内部边保存了自身的一个静态的私有对象,我们用反射把它拿出来:
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(SIZE);
unsafe.setMemory(base, SIZE, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
private static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
throw new RuntimeException();
}
}
同样的方式,运行
回车释放
我们来扒一扒ByteBuffer是如何给我们释放直接内存的,进入allocateDirect方法
进入这个构造方法,便可以直接看到调用了unsafe对象去分配了一块直接内存
那什么时候调用直接内存的释放呢,要知道需要直接调用unsafe的freeMemory才能释放,我们程序并没有手动去释放,我们观察最后面的cleaner
其中第二个参数Deallocator 我们进去看看是个什么
发现这是一个Runnable的实现类,run方法中便是释放直接内存,那么谁执行这个Runnable呢,回到前面,Cleaner这个类有点特殊,是Java中的一个虚引用类型,它有一个特点,当它关联的对象被垃圾回收时,就会执行它里面的runnable事件
这个Cleaner运行位置不在Main,而在Java中的ReferenceHandler的一个守护线程中,专门去检测虚引用对象
6.3、关闭显式GC
在程序中,我们可以手动调用System.GC执行垃圾回收,但是这一操作很耗费性能,为了避免程序员自己经常在代码中显式执行垃圾回收,我们可以配置下面这个参数来屏蔽显式GC
-XX:+DisableExplicitGC
但是这样对于我们使用的直接内存就很不友好,我们只能等待自身触发垃圾回收的时候才能正确释放,但是这样会导致我们有一段时间内,直接内存使用率很高,所以一个比较合理的解决方式便是,直接使用Unsafe对象,手动来管理直接内存