1. JVM 内存模型

image.png

2. 程序计数器

Program Counter Register 程序计数器(寄存器)

作用

  • 用于保存JVM中下一条所要执行的指令的地址

特点

  • 程序计数器是每个线程私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
  • 不会存在内存溢出

    3. 虚拟机栈

    3.1 定义

    每个线程运行需要的内存空间,称为虚拟机栈
    每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存
    每个线程只能有一个活动栈帧,对应着当前正在执行的方法
    image.png
    垃圾回收是否涉及栈内存?

  • 不涉及。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存

栈内存的分配越大越好吗?

  • 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少

方法内的局部变量是否是线程安全的?

  • 如果方法内局部变量没有逃离方法的作用范围,则是线程安全
  • 如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题

    3.2 栈内存溢出

    ```java public class StackOverflowDemo {

    /**

    • 添加 VM 参数 -Xss256k */ public static void main(String[] args) { try {

      1. method1();

      } catch (Throwable e) {

      1. e.printStackTrace();

      } }

      private static void method1() { method1(); }

}

  1. ```java
  2. java.lang.StackOverflowError
  3. at org.masteryourself.tutorial.jvm.stack.StackOverflow.method1(StackOverflow.java:26)
  4. at org.masteryourself.tutorial.jvm.stack.StackOverflow.method1(StackOverflow.java:26)
  5. at org.masteryourself.tutorial.jvm.stack.StackOverflow.method1(StackOverflow.java:26)
  6. at org.masteryourself.tutorial.jvm.stack.StackOverflow.method1(StackOverflow.java:26)

3.3 CPU 占用高问题排查

  1. public class ThreadCpuHigh {
  2. public static void main(String[] args) {
  3. new Thread(() -> {
  4. while (true) {
  5. }
  6. }).start();
  7. }
  8. }
  1. top
  1. # 9558 是 pid
  2. ps -mp 9558 -o THREAD,tid,time
  3. # 将线程 id 从十进制转成 16进制
  4. printf "%x\n" 9559
  1. 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 {
      1. List<String> list = new ArrayList<>();
      2. String str = "hello";
      3. while (true) {
      4. list.add(str);
      5. str = str + str;
      6. }
      } catch (Throwable e) {
      1. e.printStackTrace();
      } }

}

  1. ```java
  2. java.lang.OutOfMemoryError: Java heap space
  3. at java.util.Arrays.copyOf(Arrays.java:3332)
  4. at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
  5. at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
  6. at java.lang.StringBuilder.append(StringBuilder.java:136)
  7. at org.masteryourself.tutorial.jvm.heap.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:26)

5.3 堆内存溢出问题排查

  1. public class HeapMemoryTool {
  2. public static void main(String[] args) throws Exception {
  3. // 1: 初始内存信息
  4. System.out.println(1);
  5. Thread.sleep(30000);
  6. // 2. 分配了 10m 内存之后的信息
  7. byte[] bytes = new byte[10 * 1024 * 1024];
  8. System.out.println(2);
  9. Thread.sleep(30000);
  10. // 3. 调用 gc 清理之后的信息
  11. bytes = null;
  12. System.gc();
  13. System.out.println(3);
  14. Thread.sleep(30000);
  15. }
  16. }
  1. # jmap -heap 进程id

6. 方法区

6.1 定义

供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容

上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(Hotspot 在 jdk1.7 中的 PermGen)和元空间(Hotspot 在 jdk1.8 中的 Metaspace)
image.png
image.png

6.2 方法区内存溢出

JDK1.8 以前会导致永久代内存溢出

  1. java.lang.OutOfMemoryError: PermGen space

JDK1.8 之后会导致元空间内存溢出

  1. public class MetaspaceDemo extends ClassLoader {
  2. /**
  3. * 添加 VM 参数 -XX:MaxMetaspaceSize=32m
  4. */
  5. public static void main(String[] args) {
  6. try {
  7. MetaspaceDemo metaspace = new MetaspaceDemo();
  8. for (int i = 0; i < 100000; i++) {
  9. // 生成类的字节码文件
  10. ClassWriter cw = new ClassWriter(0);
  11. cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
  12. byte[] code = cw.toByteArray();
  13. // 执行类的加载
  14. metaspace.defineClass("Class" + i, code, 0, code.length);
  15. }
  16. } catch (Throwable e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
  1. java.lang.OutOfMemoryError: Metaspace
  2. at java.lang.ClassLoader.defineClass1(Native Method)
  3. at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
  4. at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
  5. at org.masteryourself.tutorial.jvm.methodarea.MetaspaceDemo.main(MetaspaceDemo.java:29)

6.3 常量池

二进制字节码主要由三部分信息构成:类基本信息、常量池、类方法定义

  1. public class HelloWorld {
  2. public static void main(String[] args) {
  3. System.out.println("hello world");
  4. }
  5. }
  1. // 反编译生成字节码指令
  2. ruanrenzhao@MacBook-Pro constantpool % javap -v HelloWorld.class
  3. // 类的基本信息
  4. Classfile /Users/ruanrenzhao/IdeaProjects/masteryourself/tutorial/tutorial-java/tutorial-jvm/target/classes/org/masteryourself/tutorial/jvm/constantpool/HelloWorld.class
  5. Last modified 2022-4-30; size 623 bytes
  6. MD5 checksum ea668646eeb8801b83fe3f2f743c150e
  7. Compiled from "HelloWorld.java"
  8. public class org.masteryourself.tutorial.jvm.constantpool.HelloWorld
  9. minor version: 0
  10. major version: 52
  11. flags: ACC_PUBLIC, ACC_SUPER
  12. // 常量池
  13. Constant pool:
  14. #1 = Methodref #6.#20 // java/lang/Object."<init>":()V
  15. #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
  16. #3 = String #23 // hello world
  17. #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
  18. #5 = Class #26 // org/masteryourself/tutorial/jvm/constantpool/HelloWorld
  19. #6 = Class #27 // java/lang/Object
  20. #7 = Utf8 <init>
  21. #8 = Utf8 ()V
  22. #9 = Utf8 Code
  23. #10 = Utf8 LineNumberTable
  24. #11 = Utf8 LocalVariableTable
  25. #12 = Utf8 this
  26. #13 = Utf8 Lorg/masteryourself/tutorial/jvm/constantpool/HelloWorld;
  27. #14 = Utf8 main
  28. #15 = Utf8 ([Ljava/lang/String;)V
  29. #16 = Utf8 args
  30. #17 = Utf8 [Ljava/lang/String;
  31. #18 = Utf8 SourceFile
  32. #19 = Utf8 HelloWorld.java
  33. #20 = NameAndType #7:#8 // "<init>":()V
  34. #21 = Class #28 // java/lang/System
  35. #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
  36. #23 = Utf8 hello world
  37. #24 = Class #31 // java/io/PrintStream
  38. #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
  39. #26 = Utf8 org/masteryourself/tutorial/jvm/constantpool/HelloWorld
  40. #27 = Utf8 java/lang/Object
  41. #28 = Utf8 java/lang/System
  42. #29 = Utf8 out
  43. #30 = Utf8 Ljava/io/PrintStream;
  44. #31 = Utf8 java/io/PrintStream
  45. #32 = Utf8 println
  46. #33 = Utf8 (Ljava/lang/String;)V
  47. // 类的方法定义(包含虚拟机指令)
  48. {
  49. // 空构造函数
  50. public org.masteryourself.tutorial.jvm.constantpool.HelloWorld();
  51. descriptor: ()V
  52. flags: ACC_PUBLIC
  53. Code:
  54. stack=1, locals=1, args_size=1
  55. 0: aload_0
  56. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  57. 4: return
  58. LineNumberTable:
  59. line 12: 0
  60. LocalVariableTable:
  61. Start Length Slot Name Signature
  62. 0 5 0 this Lorg/masteryourself/tutorial/jvm/constantpool/HelloWorld;
  63. // 静态 main 方法
  64. public static void main(java.lang.String[]);
  65. descriptor: ([Ljava/lang/String;)V
  66. flags: ACC_PUBLIC, ACC_STATIC
  67. Code:
  68. stack=2, locals=1, args_size=1
  69. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  70. 3: ldc #3 // String hello world
  71. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  72. 8: return
  73. LineNumberTable:
  74. line 15: 0
  75. line 16: 8
  76. LocalVariableTable:
  77. Start Length Slot Name Signature
  78. 0 9 0 args [Ljava/lang/String;
  79. }
  80. SourceFile: "HelloWorld.java"
  1. 0: getstatic #2 => #21.#22 => #28.#29:#30 => java/lang/System.out:Ljava/io/PrintStream;
  2. 3: ldc #3 => #23 => hello world
  3. 5: invokevirtual #4 => #24.#25 => #31.#32:#33 => java/io/PrintStream.println:(Ljava/lang/String;)V
  4. 8: return

6.4 运行时常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

运行时常量池,常量池是存在 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

6.5 StringTable

6.5.1 StringTable 特性

  1. public class StringTableTest {
  2. /**
  3. * stringTable["a", "b", "ab"]
  4. */
  5. public static void main(String[] args) {
  6. String a = "a";
  7. String b = "b";
  8. // 存在串池中
  9. String c = "ab";
  10. // 字符串变量拼接的原理是 StringBuilder
  11. // new StringBuilder().append("a").append("b").toString()
  12. // toString() 方法会使用 new 创建一个新的 String 对象,存在堆中
  13. String d = a + b;
  14. // 字符串常量拼接的原理是编译期优化,存在串池中
  15. String e = "a" + "b";
  16. // false
  17. System.out.println(c == d);
  18. // true
  19. System.out.println(c == e);
  20. }
  21. }
  1. Code:
  2. stack=2, locals=6, args_size=1
  3. 0: ldc #2 // String a
  4. 2: astore_1
  5. 3: ldc #3 // String b
  6. 5: astore_2
  7. 6: ldc #4 // String ab
  8. 8: astore_3
  9. 9: new #5 // class java/lang/StringBuilder
  10. 12: dup
  11. 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
  12. 16: aload_1
  13. 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  14. 20: aload_2
  15. 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  16. 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  17. 27: astore 4
  18. 29: ldc #4 // String ab
  19. 31: astore 5
  20. 33: return
  1. public class StringInternalTest1 {
  2. /**
  3. * new String("a") 调用的构造方法是 String(String original)
  4. * 所以这里有两个步骤: 首先从串池中查找 "a", 查找不到再创建后放入串池中, 然后在堆上 new String("a")
  5. * </br>
  6. * new String("a") + new String("b") 底层用到 StringBuilder.toString() 方法构造 String 对象, 这里调用的构造方法是 String(value, 0, count)
  7. * 注意这里传入的 value 是 char 数组, 不是字面量, 所以是直接分配到堆上
  8. */
  9. public static void main(String[] args) {
  10. // stringTable 串池: ["a", "b"]
  11. // 堆: new String("a"), new String("b"), new String("ab")
  12. String c = new String("a") + new String("b");
  13. // 调用 c.intern() 方法会尝试把 "ab" 放到串池中(如果存在就不会放入, 无论如何这个方法的返回值都是串池中的对象)
  14. String d = c.intern();
  15. // true, 因为串池中没有 "ab", 这里放入成功了
  16. System.out.println(c == "ab");
  17. // true, 都是串池中的对象
  18. System.out.println(d == "ab");
  19. }
  20. }
  1. public class StringInternalTest2 {
  2. public static void main(String[] args) {
  3. String e = "ab";
  4. // stringTable 串池: ["a", "b"]
  5. // 堆: new String("a"), new String("b"), new String("ab")
  6. String c = new String("a") + new String("b");
  7. // 调用 c.intern() 方法会尝试把 "ab" 放到串池中(如果存在就不会放入, 无论如何这个方法的返回值都是串池中的对象)
  8. String d = c.intern();
  9. // false, 因为串池中已经存在了 "ab", 这里放入失败了
  10. System.out.println(c == "ab");
  11. // true, 都是串池中的对象
  12. System.out.println(d == "ab");
  13. }
  14. }

常量池中的字符串仅是符号,第一次用到时才变为对象,会先从串池中查找对象,如果查找不到,会把生成的对象放入到串池中

利用串池的机制,来避免重复创建字符串对象(串池中的对象只会存在一份)

字符串变量拼接的原理是 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 在内存紧张时,会发生垃圾回收

  1. public class StringTableGarbageCollection {
  2. /**
  3. * 添加 VM 参数 -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
  4. */
  5. public static void main(String[] args) {
  6. try {
  7. for (int j = 0; j < 10000; j++) {
  8. String.valueOf(j).intern();
  9. }
  10. } catch (Throwable e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }
  1. # 这里触发了一次 GC
  2. [GC (Allocation Failure) [PSYoungGen: 2048K->448K(2560K)] 2048K->448K(9728K), 0.0040281 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  3. Heap
  4. PSYoungGen total 2560K, used 546K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
  5. eden space 2048K, 4% used [0x00000007bfd00000,0x00000007bfd18840,0x00000007bff00000)
  6. from space 512K, 87% used [0x00000007bff00000,0x00000007bff70000,0x00000007bff80000)
  7. to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  8. ParOldGen total 7168K, used 0K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
  9. object space 7168K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfd00000)
  10. Metaspace used 3204K, capacity 4496K, committed 4864K, reserved 1056768K
  11. class space used 355K, capacity 388K, committed 512K, reserved 1048576K
  12. SymbolTable statistics:
  13. Number of buckets : 20011 = 160088 bytes, avg 8.000
  14. Number of entries : 12438 = 298512 bytes, avg 24.000
  15. Number of literals : 12438 = 478280 bytes, avg 38.453
  16. Total footprint : = 936880 bytes
  17. Average bucket size : 0.622
  18. Variance of bucket size : 0.625
  19. Std. dev. of bucket size: 0.791
  20. Maximum bucket size : 6
  21. StringTable statistics:
  22. Number of buckets : 60013 = 480104 bytes, avg 8.000 # 默认是 60013 个 HashTable 桶
  23. Number of entries : 1442 = 34608 bytes, avg 24.000
  24. Number of literals : 1442 = 86920 bytes, avg 60.277 # 常量池中的字面量总个数,如果不触发 GC,理论上应该有 10000+
  25. Total footprint : = 601632 bytes # 常量池中的总大小 4M 左右
  26. Average bucket size : 0.024
  27. Variance of bucket size : 0.024
  28. Std. dev. of bucket size: 0.155
  29. Maximum bucket size : 2

6.5.4 StringTable 性能调优

因为 StringTable 是由 HashTable 实现的,所以可以适当增加 HashTable 桶的个数,来减少字符串放入串池所需要的时间

  1. -XX:StringTableSize=20000

考虑是否需要将字符串对象入池,可以通过 intern() 方法减少重复入池

7. 直接内存

7.1 定义

常见于 NIO 操作,用于数据缓冲区

分配回收成本较高,但读写性能高

不受 JVM 内存回收管理

7.2 直接内存溢出

  1. public class DirectBufferMemoryDemo {
  2. public static void main(String[] args) {
  3. List<ByteBuffer> list = new ArrayList<>();
  4. try {
  5. while (true) {
  6. ByteBuffer memory = ByteBuffer.allocateDirect(100 * 1024 * 1024);
  7. list.add(memory);
  8. }
  9. } catch (Throwable e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. }
  1. java.lang.OutOfMemoryError: Direct buffer memory
  2. at java.nio.Bits.reserveMemory(Bits.java:695)
  3. at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
  4. at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
  5. at org.masteryourself.tutorial.jvm.directmemory.DirectBufferMemoryDemo.main(DirectBufferMemoryDemo.java:22)

7.3 分配和回收原理

使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法

ByteBuffffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean() 方法调用 freeMemory() 来释放直接内存

  1. public class DirectMemoryAllocate {
  2. public static void main(String[] args) {
  3. int ONE_G = 1 * 1024 * 1024 * 1024;
  4. Unsafe unsafe = getUnsafe();
  5. // 利用 unsafe 分配 1G 内存
  6. // 这个方法返回的是刚刚分配的内存地址, 需要结合 setMemory() 方法使用
  7. long memoryAddress = unsafe.allocateMemory(ONE_G);
  8. unsafe.setMemory(memoryAddress, ONE_G, (byte) 0);
  9. // 释放内存
  10. unsafe.freeMemory(memoryAddress);
  11. }
  12. private static Unsafe getUnsafe() {
  13. try {
  14. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
  15. theUnsafe.setAccessible(true);
  16. // 静态变量不需要传入实例
  17. return (Unsafe) theUnsafe.get(null);
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. return null;
  22. }
  23. }