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(); }
}
```javajava.lang.StackOverflowErrorat 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 是 pidps -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 {
}
```javajava.lang.OutOfMemoryError: Java heap spaceat 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: Metaspaceat 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.classLast modified 2022-4-30; size 623 bytesMD5 checksum ea668646eeb8801b83fe3f2f743c150eCompiled from "HelloWorld.java"public class org.masteryourself.tutorial.jvm.constantpool.HelloWorldminor version: 0major version: 52flags: 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: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 12: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lorg/masteryourself/tutorial/jvm/constantpool/HelloWorld;// 静态 main 方法public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 15: 0line 16: 8LocalVariableTable:Start Length Slot Name Signature0 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 world5: invokevirtual #4 => #24.#25 => #31.#32:#33 => java/io/PrintStream.println:(Ljava/lang/String;)V8: 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";// falseSystem.out.println(c == d);// trueSystem.out.println(c == e);}}
Code:stack=2, locals=6, args_size=10: ldc #2 // String a2: astore_13: ldc #3 // String b5: astore_26: ldc #4 // String ab8: astore_39: new #5 // class java/lang/StringBuilder12: dup13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V16: aload_117: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;20: aload_221: 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 429: ldc #4 // String ab31: astore 533: 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]HeapPSYoungGen 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 1056768Kclass space used 355K, capacity 388K, committed 512K, reserved 1048576KSymbolTable statistics:Number of buckets : 20011 = 160088 bytes, avg 8.000Number of entries : 12438 = 298512 bytes, avg 24.000Number of literals : 12438 = 478280 bytes, avg 38.453Total footprint : = 936880 bytesAverage bucket size : 0.622Variance of bucket size : 0.625Std. dev. of bucket size: 0.791Maximum bucket size : 6StringTable statistics:Number of buckets : 60013 = 480104 bytes, avg 8.000 # 默认是 60013 个 HashTable 桶Number of entries : 1442 = 34608 bytes, avg 24.000Number of literals : 1442 = 86920 bytes, avg 60.277 # 常量池中的字面量总个数,如果不触发 GC,理论上应该有 10000+Total footprint : = 601632 bytes # 常量池中的总大小 4M 左右Average bucket size : 0.024Variance of bucket size : 0.024Std. dev. of bucket size: 0.155Maximum 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 memoryat 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;}}
