方法区(Method Area):存放类
堆(Heap):存放类的实例/对象
虚拟机栈(JVM Stacks):调用方法会用到
程序计数器(PC Register):调用方法会用到
本地方法栈(Native Method Stacks):调用方法会用到
本地方法接口:调用操作系统提供的方法
解释器(Interpreter):每行代码
即时编译器(JIT Compiler):热点代码
垃圾回收(GC):对堆中不再引用的对象进行回收

引言

定义:Java Virtual Machine - Java程序的运行环境 (Java二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

学习路线:
image-20240228013512368

内存结构

① Program Counter Register (程序计数器)

image-20240228014942129

物理上通过寄存器实现。
作用:记住下一条jvm指令的执行地址。
特点:

  • 是线程私有的
  • 不存在内存溢出的问题

作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 Virtual Machine Stacks (Java虚拟机栈)

image-20240228020325378

  • 每个线程运行时需要的内存,称为虚拟机栈。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

问题辨析

  1. 垃圾回收是否涉及栈内存?

否。因为栈内存无非就是一次次的方法调用所产生的,而栈帧内存在每一次方法调用结束后都会被弹出栈,也就自动被回收掉了,所以栈内存根本不需要垃圾回收来管理。

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

    不是。内存划分得大,通常只是能进行更多次地方法递归调用,但是不会提高运行效率。反而还会使线程的数目减少。不建议采用过大的栈内存。

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

是。因为这些局部变量是线程私有的,互不干扰,除非变量是static修饰的。

看一个变量是否线程安全,不仅是看它是不是局部变量,还要看它是否逃离了这个方法的作用范围。如果逃离了就有可能被其他线程访问到,那它就不是线程安全的。总结:如果方法内局部变量没有逃离方法的作用范围,那它就是线程安全的;如果是局部变量引用了对象,并逃离方法的作用范围,就需要考虑线程安全的问题。

栈内存溢出

栈内存溢出报错:java.lang.StackOverFlowError

  • 栈帧过多导致栈内存溢出

  • 栈帧过大导致栈内存溢出

线程运行诊断

案例1:CPU占用过多

  • 用top命令定位哪个进程对CPU的占用过高

  • 用ps命令进一步定位是哪个线程引起的CPU占用过高
    ps H -eo pid,tid,%cpu | grep 进程id

  • jstack 进程id

    可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号。

案例2:程序运行很长时间没有结果

③ 本地方法栈

image-20240228023317882

Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法(使用C/C++实现)的调用。本地方法栈也是线程私有的。

④ Heap (堆)

image-20240228023343186

通过new关键字创建对象都会使用堆内存。

特点:

  1. 它是线程共享的,堆中对象都需要考虑线程安全的问题。(有例外)
  2. 有垃圾回收机制。

堆内存溢出

堆内存溢出报错:java.lang.OutOfMemoryError: Java heap space

排查堆内存溢出的问题,可以把虚拟机内存设置小一些,方便尽早暴露出堆内存溢出的问题。(比如-Xmx8m 改成8mb内存)

堆内存诊断

  • jps工具
    查看当前系统中有哪些java进程
  • jmap工具
    查看堆内存占用情况 jmap -heap 进程id
  • jconsole工具
    图形界面的,多功能的检测工具,可以连续监测

案例:

  • 垃圾回收后,内存占用仍然很高。

⑤ 方法区 (Method Area)

image-20240228025012488

定义

The Method Area is a shared data area in the JVM that stores class and interface definitions. It is created when the JVM starts, and it is destroyed only when the JVM exits. Concretely, the class loader loads the bytecode of the class and passes it to the JVM.

方法区是线程共享的。

“逻辑上”是堆的一部分,具体实现上,不同JVM厂商不一样。

组成

image-20240228025144019
image-20240228025205998

方法区内存溢出

1.8以前:会导致永久代内存溢出:java.lang.OutOfMemoryError: PermGen space
1.8:会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace

各类溢出:

  • 方法区溢出:不断创建代理类 / 不断调用字符串.intern
  • 栈溢出:递归
  • 堆溢出:list不断新增

不会出现溢出:

  • 程序计数器

运行时常量池

二进制字节码的组成:类基本信息、常量池、类方法定义(包含了虚拟机指令)。

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

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

StringTable(串池)

StringTable [ “a”, “b”, “ab” ] —-是hashtable结构,不能扩容。

常量池中的信息,都会被加载到运行时常量池中,这时还是常量池中符号,还没有变为java字符串对象。(懒惰模式)

ldc #2 会把a符号变为”a”字符串对象
ldc #3 会把b符号变为”b”字符串对象
ldc #4 会把ab符号变为”ab”字符串对象

1
2
3
4
5
6
7
8
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac在编译期间的优化,结果已经在编译器确定为ab

System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true

StringTable特性

  1. 常量池中的字符串仅是符号,第一次用到时才变为对象。
  2. 利用串池的机制,来避免重复创建字符串对象。
  3. 字符串变量拼接的原理是StringBuilder(1.8)
  4. 字符串常量拼接的原理是编译器优化。
  5. 可以使用intern方法,主动将串池中还没有的字符串对象放入串池。
  • 1.8 将这个字符串对象尝试放入串池,如果有则不放入,如果没有则放入串池,会把串池中的对象返回。
  • 1.6 将这个字符串对象尝试放入串池,如果有则不放入,如果没有则会把此对象复制一份,放入串池,会把串池中的对象返回。

StringTable位置

StringTable 性能调优

  • 调整桶个数:-XX:StringTableSize=
  • 考虑是否将字符串对象入池

⑥ Direct Memory (直接内存)

定义

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

分配和回收原理

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
  • ByteBuffer的实现类内部,使用了Cleaner (虚引用) 来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。