1. JVM 内存模型
2. 程序计数器
Program Counter Register 程序计数器(寄存器)
作用
- 用于保存JVM中下一条所要执行的指令的地址
特点
- 程序计数器是每个线程私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
-
3. 虚拟机栈
3.1 定义
每个线程运行需要的内存空间,称为虚拟机栈
每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
垃圾回收是否涉及栈内存? 不涉及。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存
栈内存的分配越大越好吗?
- 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少
方法内的局部变量是否是线程安全的?
- 如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题
3.2 栈内存溢出
```java public class StackOverflowDemo {
/**
添加 VM 参数 -Xss256k */ public static void main(String[] args) { try {
method1();
} catch (Throwable e) {
e.printStackTrace();
} }
private static void method1() { method1(); }
}
```java
java.lang.StackOverflowError
at org.masteryourself.tutorial.jvm.stack.StackOverflow.method1(StackOverflow.java:26)
at org.masteryourself.tutorial.jvm.stack.StackOverflow.method1(StackOverflow.java:26)
at org.masteryourself.tutorial.jvm.stack.StackOverflow.method1(StackOverflow.java:26)
at org.masteryourself.tutorial.jvm.stack.StackOverflow.method1(StackOverflow.java:26)
3.3 CPU 占用高问题排查
public class ThreadCpuHigh {
public static void main(String[] args) {
new Thread(() -> {
while (true) {
}
}).start();
}
}
top
# 9558 是 pid
ps -mp 9558 -o THREAD,tid,time
# 将线程 id 从十进制转成 16进制
printf "%x\n" 9559
jstack 线程id
4. 本地方法栈
一些带有 native 关键字的方法就是需要 JAVA 去调用本地的 C 或者 C++ 方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法
5. 堆
5.1 定义
定义
- 通过
new
关键字创建的对象都会被放在堆内存
特点
- 所有线程共享,堆内存中的对象都需要考虑线程安全问题
-
5.2 堆内存溢出
```java public class JavaHeapSpaceDemo {
/**
- 添加 VM 参数 -Xmx8m
*/
public static void main(String[] args) {
try {
} catch (Throwable e) {List<String> list = new ArrayList<>();
String str = "hello";
while (true) {
list.add(str);
str = str + str;
}
} }e.printStackTrace();
- 添加 VM 参数 -Xmx8m
*/
public static void main(String[] args) {
try {
}
```java
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at org.masteryourself.tutorial.jvm.heap.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:26)
5.3 堆内存溢出问题排查
public class HeapMemoryTool {
public static void main(String[] args) throws Exception {
// 1: 初始内存信息
System.out.println(1);
Thread.sleep(30000);
// 2. 分配了 10m 内存之后的信息
byte[] bytes = new byte[10 * 1024 * 1024];
System.out.println(2);
Thread.sleep(30000);
// 3. 调用 gc 清理之后的信息
bytes = null;
System.gc();
System.out.println(3);
Thread.sleep(30000);
}
}
# jmap -heap 进程id
6. 方法区
6.1 定义
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容
上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(Hotspot 在 jdk1.7 中的 PermGen)和元空间(Hotspot 在 jdk1.8 中的 Metaspace)
6.2 方法区内存溢出
JDK1.8 以前会导致永久代内存溢出
java.lang.OutOfMemoryError: PermGen space
JDK1.8 之后会导致元空间内存溢出
public class MetaspaceDemo extends ClassLoader {
/**
* 添加 VM 参数 -XX:MaxMetaspaceSize=32m
*/
public static void main(String[] args) {
try {
MetaspaceDemo metaspace = new MetaspaceDemo();
for (int i = 0; i < 100000; i++) {
// 生成类的字节码文件
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] code = cw.toByteArray();
// 执行类的加载
metaspace.defineClass("Class" + i, code, 0, code.length);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at org.masteryourself.tutorial.jvm.methodarea.MetaspaceDemo.main(MetaspaceDemo.java:29)
6.3 常量池
二进制字节码主要由三部分信息构成:类基本信息、常量池、类方法定义
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
// 反编译生成字节码指令
ruanrenzhao@MacBook-Pro constantpool % javap -v HelloWorld.class
// 类的基本信息
Classfile /Users/ruanrenzhao/IdeaProjects/masteryourself/tutorial/tutorial-java/tutorial-jvm/target/classes/org/masteryourself/tutorial/jvm/constantpool/HelloWorld.class
Last modified 2022-4-30; size 623 bytes
MD5 checksum ea668646eeb8801b83fe3f2f743c150e
Compiled from "HelloWorld.java"
public class org.masteryourself.tutorial.jvm.constantpool.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // org/masteryourself/tutorial/jvm/constantpool/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/masteryourself/tutorial/jvm/constantpool/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 org/masteryourself/tutorial/jvm/constantpool/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
// 类的方法定义(包含虚拟机指令)
{
// 空构造函数
public org.masteryourself.tutorial.jvm.constantpool.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 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/masteryourself/tutorial/jvm/constantpool/HelloWorld;
// 静态 main 方法
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 15: 0
line 16: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
0: getstatic #2 => #21.#22 => #28.#29:#30 => java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 => #23 => hello world
5: invokevirtual #4 => #24.#25 => #31.#32:#33 => java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
6.4 运行时常量池
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池,常量池是存在 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
6.5 StringTable
6.5.1 StringTable 特性
public class StringTableTest {
/**
* stringTable["a", "b", "ab"]
*/
public static void main(String[] args) {
String a = "a";
String b = "b";
// 存在串池中
String c = "ab";
// 字符串变量拼接的原理是 StringBuilder
// new StringBuilder().append("a").append("b").toString()
// toString() 方法会使用 new 创建一个新的 String 对象,存在堆中
String d = a + b;
// 字符串常量拼接的原理是编译期优化,存在串池中
String e = "a" + "b";
// false
System.out.println(c == d);
// true
System.out.println(c == e);
}
}
Code:
stack=2, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: return
public class StringInternalTest1 {
/**
* new String("a") 调用的构造方法是 String(String original)
* 所以这里有两个步骤: 首先从串池中查找 "a", 查找不到再创建后放入串池中, 然后在堆上 new String("a")
* </br>
* new String("a") + new String("b") 底层用到 StringBuilder.toString() 方法构造 String 对象, 这里调用的构造方法是 String(value, 0, count)
* 注意这里传入的 value 是 char 数组, 不是字面量, 所以是直接分配到堆上
*/
public static void main(String[] args) {
// stringTable 串池: ["a", "b"]
// 堆: new String("a"), new String("b"), new String("ab")
String c = new String("a") + new String("b");
// 调用 c.intern() 方法会尝试把 "ab" 放到串池中(如果存在就不会放入, 无论如何这个方法的返回值都是串池中的对象)
String d = c.intern();
// true, 因为串池中没有 "ab", 这里放入成功了
System.out.println(c == "ab");
// true, 都是串池中的对象
System.out.println(d == "ab");
}
}
public class StringInternalTest2 {
public static void main(String[] args) {
String e = "ab";
// stringTable 串池: ["a", "b"]
// 堆: new String("a"), new String("b"), new String("ab")
String c = new String("a") + new String("b");
// 调用 c.intern() 方法会尝试把 "ab" 放到串池中(如果存在就不会放入, 无论如何这个方法的返回值都是串池中的对象)
String d = c.intern();
// false, 因为串池中已经存在了 "ab", 这里放入失败了
System.out.println(c == "ab");
// true, 都是串池中的对象
System.out.println(d == "ab");
}
}
常量池中的字符串仅是符号,第一次用到时才变为对象,会先从串池中查找对象,如果查找不到,会把生成的对象放入到串池中
利用串池的机制,来避免重复创建字符串对象(串池中的对象只会存在一份)
字符串变量拼接的原理是 StringBuilder,字符串常量拼接的原理是编译期优化
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
6.5.2 StringTable 位置
StringTable 在 JDK1.6 中是放入在 PermGen 中的,在 JDK1.8 之后放到了 Heap 堆中
因为 PermGen 只有触发 FGC 才会回收,而 StringTable 中的对象需要频繁回收,所以放到了 Heap 堆中,这样 YGC 也会触发垃圾回收
6.5.3 StringTable 垃圾回收
StringTable 在内存紧张时,会发生垃圾回收
public class StringTableGarbageCollection {
/**
* 添加 VM 参数 -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public static void main(String[] args) {
try {
for (int j = 0; j < 10000; j++) {
String.valueOf(j).intern();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
# 这里触发了一次 GC
[GC (Allocation Failure) [PSYoungGen: 2048K->448K(2560K)] 2048K->448K(9728K), 0.0040281 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 2560K, used 546K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 2048K, 4% used [0x00000007bfd00000,0x00000007bfd18840,0x00000007bff00000)
from space 512K, 87% used [0x00000007bff00000,0x00000007bff70000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 7168K, used 0K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
object space 7168K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfd00000)
Metaspace used 3204K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 355K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 12438 = 298512 bytes, avg 24.000
Number of literals : 12438 = 478280 bytes, avg 38.453
Total footprint : = 936880 bytes
Average bucket size : 0.622
Variance of bucket size : 0.625
Std. dev. of bucket size: 0.791
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000 # 默认是 60013 个 HashTable 桶
Number of entries : 1442 = 34608 bytes, avg 24.000
Number of literals : 1442 = 86920 bytes, avg 60.277 # 常量池中的字面量总个数,如果不触发 GC,理论上应该有 10000+
Total footprint : = 601632 bytes # 常量池中的总大小 4M 左右
Average bucket size : 0.024
Variance of bucket size : 0.024
Std. dev. of bucket size: 0.155
Maximum bucket size : 2
6.5.4 StringTable 性能调优
因为 StringTable 是由 HashTable 实现的,所以可以适当增加 HashTable 桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=20000
考虑是否需要将字符串对象入池,可以通过 intern() 方法减少重复入池
7. 直接内存
7.1 定义
常见于 NIO 操作,用于数据缓冲区
分配回收成本较高,但读写性能高
7.2 直接内存溢出
public class DirectBufferMemoryDemo {
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
try {
while (true) {
ByteBuffer memory = ByteBuffer.allocateDirect(100 * 1024 * 1024);
list.add(memory);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at org.masteryourself.tutorial.jvm.directmemory.DirectBufferMemoryDemo.main(DirectBufferMemoryDemo.java:22)
7.3 分配和回收原理
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
ByteBuffffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean()
方法调用 freeMemory()
来释放直接内存
public class DirectMemoryAllocate {
public static void main(String[] args) {
int ONE_G = 1 * 1024 * 1024 * 1024;
Unsafe unsafe = getUnsafe();
// 利用 unsafe 分配 1G 内存
// 这个方法返回的是刚刚分配的内存地址, 需要结合 setMemory() 方法使用
long memoryAddress = unsafe.allocateMemory(ONE_G);
unsafe.setMemory(memoryAddress, ONE_G, (byte) 0);
// 释放内存
unsafe.freeMemory(memoryAddress);
}
private static Unsafe getUnsafe() {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
// 静态变量不需要传入实例
return (Unsafe) theUnsafe.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}