一. 前言
1、什么是 JVM?
1)定义:
Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)
2)好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
3)比较:
jvm jre jdk的关系如下图
2、学习 JVM 有什么用 ?
- 面试
- 理解底层的实现原理
- 中高级程序员的必备技能
3、常见的JVM
4、学习路线
二、内存结构
1、程序计数器
1)定义
Program Counter Register 程序计数器(寄存器)
作用,是记住下一条jvm指令的执行地址
特点
- 是线程私有的
- 不会存在内存溢出
2)作用
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
- 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
- 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。
2、虚拟机栈
1)定义
Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析:
- 垃圾回收是否涉及栈内存?
不涉及栈内存,栈内存是在方法调用时产生的,栈帧在每次弹出栈后,会被自动回收掉。 - 栈内存分配越大越好吗?
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。 - 方法内的局部变量是否线程安全
- 如果方法内部的变量没有逃离方法的作用访问,它是线程安全的-
- 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。
2)栈内存溢出
报错: java.lang.StackOverFlowError
- 栈帧过多会导致栈内存溢出。常发生在递归调用过多,或循环引用问题
- 栈帧过大会导致栈内存溢出
-Xss
指令可以为虚拟机栈分配内存大小。
3)线程运行诊断
案例:cpu 占用过多
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id
- 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
3、本地方法栈
java 语言调用其他语言的方法或接口,实现更底层的应用和操作,本地方法栈就是用来存储的。例如 java 中的 native 关键字所引用的就是 c 或 c++ 的方法。
4、堆
1)定义
Heap 堆
- 对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所
有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java
世界里"几乎"所有的对象实例都在这里分配内存。
特点
- 它是线程共享的堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
2)堆内存溢出
报错:java.lang.OutOfMemoryError: Java heap space
-Xmx
指令可以指定堆内存大小。
3)堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测 - jvisualvm 工具
5、方法区
1)定义
方法区和堆一样,是各个线程共享的内存区域,存储了每个类的结构,例如成员变量、静态变量、方法数据、成员方法、构造器和运行时常量池。
虽然方法区逻辑上是堆的一部分,但是简单的实现可以不同,方法区只是一个规范,例如jdk8之前hotspot虚拟机的"永久代"就是方法区的实现方式之一。jdk8后,使用hotspot虚拟机使用"元空间"的方式实现方法区,也就是在本地内存来实现元空间。
2)组成
3)方法区内存溢出
jdk1.8之前 报错:java.lang.OutOfMemoryError: PermGen space
jdk1.8之后 报错:java.lang.OutOfMemoryError: Meta space
使用 -XX:MaxPermSize=8m
指定永久代内存大小
使用 -XX:MaxMetaspaceSize=8m
指定元空间大小
场景:
- spring
- mybatis cglib
4)运行时常量池
运行时常量池 (Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字
段、方法、接口等描述信息外,还有一项信息是常量池表 (Constant Pool Table),用于存放编译期生
成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
在
*.class 文件目录下使用javap -v ./HelloWorld.class
查看二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息 - 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量
池,并把里面的符号地址变为真实地址
5)StringTable
StringTable 底层是 HashTable ,存储的字符串是唯一的,不能扩容。
常量池中的信息,都会被加载到运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
java
public static void main(String[] args) {
String s1 = "a"; // 默认懒加载
String s2 = "b";
String s3 = "ab"; // 在 StringTable 字符串常量池中创建
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString(); => new String("ab"); 在堆中创建的对象
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期间确定为 ab,即 s5 == s3 -> true
}
6)StringTable 的特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量 拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则直接返回串池中的对象,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则直接返回串池中的对象,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
例1:
java
// 在 jdk1.8中
public class Main{
public static void main(String[] args) {
// 串池StringTable["a", "b"]
// 堆 new String("a"), new String("b"), new StringBuilder() * 3, new String("ab")
String s = new String("a") + new String("b"); //一共创建了 6 个对象
// TODO: 字符串对象s调用intern()方法, 由于串池中没有"ab",将 s 放入串池中,直接返回串池中的对象
String s2 = s.intern();
String x = "ab"; // 取出串池中的 "ab"
System.out.println( s2 == x); // true
System.out.println( s == x ); // true
}
}
例2:
java
// 在 jdk1.6中
public class Main{
public static void main(String[] args) {
// 串池StringTable["a", "b"]
// 堆 new String("a"), new String("b"), new StringBuilder() * 3, new String("ab")
String s = new String("a") + new String("b"); //一共创建了 6 个对象
// TODO: 字符串对象s调用intern()方法, 由于串池中没有"ab", 字符串对象s会被复制一份放到串池中,返回串池中的对象
String s2 = s.intern();
String x = "ab"; // 取出串池中的 "ab"
System.out.println( s2 == x); // true
System.out.println( s == x ); // false
}
}
7)StringTable的位置
jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。
8)StringTable垃圾回收
添加虚拟机参数:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
java
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 10000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
shell
[GC (Allocation Failure) [PSYoungGen: 2048K->496K(2560K)] 2048K->720K(9728K), 0.0015984 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
10000 # 10000个字符被GC回收了一部分
...
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 5671 = 136104 bytes, avg 24.000
Number of literals : 5671 = 346048 bytes, avg 61.021 # 字符串常量池中的字符串
Total footprint : = 962256 bytes
9)StringTable性能调优
如果需要添加的字符串常量的数量很多,可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间。
java
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
- 考虑将字符串对象是否入池
- 可以通过 intern 方法减少重复入池
6、直接内存
1)定义
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
2)分配和回收原理
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦
ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调
用 freeMemory 来释放直接内存。
在进行JVM调优时,尝试用-XX:+DisableExplicitGC
指令,可以防止我们自己手动的进行垃圾回收 (System.gc())。
但是在直接内存中,JVM无法自动对直接内存进行垃圾回收,我们可以通过 Unsafe 中的 freeMemory 方法手动释放内存