JVM
先说一下,本文参考了大量黑马程序员与《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》上的内容,对于图片,有的是截屏的大佬博客里面的图片,有的是用的模板库,有的来源于网络,有的来源于自己画的......
若读者发现错误,请告知,作者会尽快修改
一、内存结构
1.1 前言
先放一张简洁的图,本章会围绕以下内容进行讲解
1.2 程序计数器
概念:
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
摘自《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》
为什么需要程序计数器呢?
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,所以,在任何时刻,处理器只会执行一条指令。当我们执行了另一个线程后,又回到刚刚的某个线程,这时为了能恢复到正确的执行位置,我们的程序计数器就起作用了。
特点
-
是线程私有的
每条线程都有独立的程序计数器,各条线程之间计数器互不影响,独立存储
-
不会存在内存溢出
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
1.3 Java虚拟机栈
概念:
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
摘自《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》
虚拟机栈 :每个线程运行时所需要的内存
栈帧: 栈是由多个栈帧组成的,栈帧对应着每次方法调用时的内存
活动栈帧: 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
两类异常状况
-
StackOverflowError
栈帧过多导致栈内存溢出
-
OutOfMemoryError
栈帧过大导致栈内存溢出
1.4 本地方法栈
概念:
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
两类异常状况
-
StackOverflowError
-
OutOfMemoryError
线程运行诊断
-
用top定位哪个进程对cpu的占用过高
-
ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)jstack 进程id
-
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
1.5 堆
1.5.1 概念:
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
摘自《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》
通过 new 关键字,创建对象都会使用堆内存
1.5.2 特点
- 是线程共享的,所以堆中的对象都要考虑线程安全问题
- 有垃圾回收机制
垃圾回收
从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现
“新生代” “老年代” “永久代” “**Eden空间 **” “From Survivor空间” “To Survivor空间”等名词,具体的后面说。
1.5.3 堆内存诊断
- jps工具
- 查看当前系统中的java进程
- jmap
- jmap -heap 进程id 可以查看堆内存使用情况
- jconsole
- 图形化界面,多功能的监测工具,可以连续监测,可检测死锁
- jvisualvm
- 功能更强的图形化界面
1.6 方法区
1.6.1 概念:
我们先来看下JDK8 中JVM的官方文档
1.6.2 特点
-
和堆一样,是各个线程共享的内存区域
-
有垃圾回收机制
1.6.3 作用
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
1.6.4 内存溢出
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError异常。
-
JDK 1.8前会导致永久代内存溢出
演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m
-
1.8 之后会导致元空间内存溢出
演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m
场景:spring,mybatis
1.6.5 永久代与元空间
到了JDK 8,完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta- space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
1.6.6 关于垃圾回收
-
可以选择不实现垃圾回收
-
相对而言,垃圾收集行为在这个区域较少出现,但并非数据进入了方法区就如永久代的名字一样“永久”存在了
-
该区域的内存回收目标主要是:常量池的回收和对类型的卸载,但回收效果通常令人不是很满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。
1.7 运行时常量池
1.7.1 概念
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
摘自《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》
-
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息
-
运行时常量池:常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量
池,并把里面的符号地址变为真实地址
咋们在Idea中可以通过插件Jclasslib来查看到如下信息
1.7.2 特点
-
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性
并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,
用得比较多的便是String类的intern()方法
1.7.3 内存溢出
- 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常
1.8 StringTable
1.8.1 概念
StringTable叫做字符串常量池,用于存放字符串常量
1.8.2 String的一些特性
在说StringTable的特性前,我们先来看看String的一些特性
String的不可变性
String str = "tian";
这种情况,"tian"是被存入了 StringTable中的
str 指向了StringTable中的 "tian"
我们稍微修改下代码
String str = "tian";
str = "cx";
我们接下来在电脑上跑一下,来验证不可变性
String str = "tian";
System.out.println(System.identityHashCode(str));
str = "cx";
System.out.println(System.identityHashCode(str));
str = "tian";
System.out.println(System.identityHashCode(str));
运行结果
通过图可以看出: 第一次和第三次的hash值一样,第二次指向了另一个字符串,所以hash值不一样,同时,我们也可以得出结论: str是指向了一个新的地址,并没有改变最开始指向地址的内容
那么为什么要设计为不可变性呢?
1、常量池
Java 中的字符串常量池的存在就是为了性能优化,当创建一个新字符串对象时,若常量池中已有,就不会创建新对象了,就直接引用常量池中的对象,这样减少了 JVM 的内存开销,提高效率。
2、hashcode 缓存
因为字符串不可变,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就使得字符串很适合作为 HashMap 中的 key,效率大大提高。
3、多线程安全
多线程中,可变对象的值很可能被其他线程改变,造成不可预期的结果。而不可变的 String 可以自由在多个线程之间共享,不需要同步处理
总的来说就两点
-
安全性
-
性能
通过阅读String类的源码,总结出String是怎么实现不可变性的
-
String是由final 修饰的,final修饰的类是最终类,不能被修改
-
成员变量由private,final修饰且没有set方法
-
通过构造对象时,成员变量是使用深拷贝来初始化的
在顺便看一下字符串拼接
代码来自黑马程序员,写得非常详细
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
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 == s5); //true
}
}
1.8.3 StringTable的一些特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
intern方法
在JDK 1.8中,intern方法的作用就是尝试将一个字符串放入StringTable中,如果不存在就放入StringTable并返回StringTable中的地址,如果存在的话就直接返回StringTable中的地址。
本来打算在后面放题目,想了下,这里还是有必要放道题
public class Demo1_23 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == x ); //true
System.out.println( s == x ); // false
}
}
1.8.4 StringTable的位置
先说结论: StringTable是放在堆中的
然后验证:
public static void main(String[] args) throws InterruptedException {
ArrayList list = new ArrayList();
String str = "hello";
for(int i = 0;i < Integer.MAX_VALUE;i++) {
String s = str + i;
str = s;
list.add(s.intern());
}
}
1.8.5 StringTable的垃圾回收机制
StringTable是有垃圾回收机制的
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
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);
}
}
}
当我们注释掉9~11行的代码时,我们可以看到字符串对象数量:如图
当我们取消注释9~11行的代码时,我们可以看到字符串对象数量:如图
本来应该是超过1万的,但这里确 一万 都没达到,这是为什么呢?
答案:
StringTable有垃圾回收机制,超过了设置的大小,就会进行垃圾回收
通过下面这个图也能看出来,触发了垃圾回收机制
1.8.6 StringTable性能调优
- 调整 -XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
1.9 直接内存
1.9.1 概念
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现
摘自《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》
Direct Memory
-
常见于 NIO 操作时,用于数据缓冲区
-
分配回收成本较高,但读写性能高
-
不受 JVM 内存回收管理
1.9.2 分配和回收原理
-
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
-
ByteBuffffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffffer 对象,一旦
ByteBuffffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调
用 freeMemory 来释放直接内存