JVM内存结构
一 程序计数器
1.1 定义
Program Counter Register 程序计数器(寄存器)
作用:记住下一条jvm指令的执行地址
特点:
- 是线程私有的
- 不会存在内存溢出
1.2 作用
- 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
- 多线程的环境下,如果两个线程发生了上下文切换,由于程序计数器是线程私有的,不同的线程有不同的程序计数器记录各自的执行指令地址,所以程序计数器会取线程下一行指令的地址行号,以便于接着往下执行。
二 虚拟机栈
2.1 定义
Java Virtual Machine Stacks(Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法(栈顶)
注:
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
问题辨析:
1 垃圾回收是否涉及栈内存?
不会,当方法调用时,会同步创建栈帧,当方法执行完毕后,就会自动出栈。
2 栈内存分配越大越好吗?
不是,物理内存是一定的,栈内存分配多了,可以支持方法更多的递归调用,但是由于单个线程占用内存的提高,线程数会减少,运行会变慢。
3 方法内的局部变量是否线程安全?
- 首先,变量的线程安全,可以简单理解为并发情况下,该变量不会受多个线程的影响。
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
2.2 栈内存溢出
栈帧过多导致栈内存溢出(较常见,比如递归过多)
栈帧过大导致栈内存溢出(较少见)
注:栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError
,使用 -Xss256k
指定栈内存大小!(-Xss256k
为VM options,虚拟机参数的一种,可以设置栈内存的大小)
2.3 线程运行诊断
案例一:cpu 占用过多
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程
- 用
top
指令定位哪个进程对cpu的占用过高 ps H -eo pid,tid,%cpu | grep 进程id
(用ps命令进一步定位是哪个线程引起的cpu占用过高)jstack 进程id
可以根据 线程id 找到有问题的线程,进一步定位到问题代码的源码行号
三 本地方法栈
- 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的。
- 其区别只是虚拟机栈为虚拟机执行
Java方法
(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法
服务。 - 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出
StackOverflowError
和OutOfMemoryError
异常。
什么是本地(Native)方法?
- 一些带有
native
关键字的方法就是本地方法,这类方法很多,比如Object
中的hashCode
和clone
方法。
- 因为 Java 有时候没法直接和操作系统底层交互,只能调用
本地的C或者C++方法
(也就是native方法),间接和底层交互。
四 堆
4.1 定义
Heap 堆
- 通过new关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
4.2 堆内存溢出
java.lang.OutofMemoryError :java heap space.
堆内存溢出
可以使用 -Xmx8m
来指定堆内存大小。
4.3 堆内存诊断工具
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况jmap - heap 进程id
,查看的是当前堆内存占用情况,非连续 - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测 - jvisualvm 工具
和jconsole
一样是图形界面的,可以通过堆转储dump
功能抓取内存快照,分析占用内存最多的那些对象
五 方法区
5.1 定义
Method Area 方法区
- 方法区是线程共享的。
- 方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的“文本”段。
- 它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化和接口初始化中使用的特殊方法
特点
- 尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。
- 虚拟机规范不强制指定方法区的位置或用于管理已编译代码的策略。
- 方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。
- 方法区域的内存不需要是连续的!
5.2 组成
- HotSpot虚拟机在JDK1.6时方法区的实现是永久代(PermGen)
- HotSpot虚拟机在JDK 1.8时改用与JRockit、J9一样在本地内存中实现的元空间(MetaSpace)来代替
5.3 方法区溢出
- JDK1.8 之前会导致永久代内存溢出,抛出
java.lang.OutOfMemoryError: PermGen space
错误- 使用
-XX:MaxPermSize=8m
指定永久代内存大小
- 使用
- JDK1.8 之后会导致元空间内存溢出,抛出
java.lang.OutOfMemoryError: Metaspace
错误- 使用
-XX:MaxMetaspaceSize=8m
指定元空间大小
- 使用
5.4 运行时常量池
*.class 文件(二进制字节码文件)的信息
先给出一段简单的代码,运行该代码
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
然后使用 javap -v HelloWorld.class
命令反编译查看结果。
可以发现,二进制字节码主要包括类基本信息,常量池,类方法定义(包含了虚拟机指令)。
其中常量池就是一张表,包含地址和符号的对应关系。
在看到类方法定义中的主函数,其中每条虚拟机指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。
常量池和运行时常量池
经过上述过程的反编译,我们已经知道了二级制字节码包括的信息,特别是理解了常量池是什么,下面就可以知道运行时常量池和常量池之间的关系了。
常量池:
就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池:
常量池是 *.class
文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.5 StringTable
StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
StringTable 面试题
根据上述StringTable(串池)的特性,分析下面代码的输出。
// jdk 1.8环境
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";
// 上述三行代码执行完后,串池的情况如下
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
// 字符串变量拼接,相当于执行了下面的代码
// new StringBuilder().append("a").append("b").toString()
// toString()内部其实调用了new String("ab")
String s4 = s1 + s2;
// 字符串常量拼接,javac在编译期间的优化,结果已经在编译期确定为ab
String s5 = "a" + "b";
// 已有,不放入,返回串池中的ab
String s6 = s4.intern();
// 问
// false,s3串池中,s4堆中
System.out.println(s3 == s4);
// true,s3和s5都是串池中的,同一个
System.out.println(s3 == s5);
// true
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
// 已有,不放入
x2.intern();
// false
System.out.println(x1 == x2);
}
}
intern在jdk1.6和jdk1.8的区别
例子1——jdk1.8环境
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
}
}
例子2——jdk1.6环境
public class Demo1_23 {
// ["a", "b", "ab"]
public static void main(String[] args) {
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
// 将这个字符串对象尝试放入串池,如果有则并不会放入;
// 如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
String s2 = s.intern();
// s 拷贝一份,放入串池
String x = "ab";
System.out.println( s2 == x); // false
System.out.println( s == x ); // true
}
}
5.6 StringTable的位置
- jdk1.6 StringTable 位置是在永久代中。
- jdk1.8 StringTable 位置是在堆中。
5.7 StringTable垃圾回收
-Xmx10m
指定堆内存大小
-XX:+PrintStringTableStatistics
打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc
打印 gc 的次数,耗费时间等信息
/**
* 演示 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 < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
5.8 StringTable调优
- 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
- 可以通过设置虚拟机参数
-XX:StringTableSize=桶个数
来调整HashTable桶的个数(最少设置为 1009 以上) - 考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池
六 直接内存
6.1 定义
Direct Memory 直接内存
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但是读写性能高
- 不受JVM内存回收管理
6.2 分配和回收原理
代码演示
public class Code_06_DirectMemoryTest {
public static int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
// method();
method1();
}
// 演示 直接内存的分配和释放
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
System.gc(); // 手动 gc
System.in.read();
}
// 演示 直接内存 是被 unsafe 创建与回收(底层原理)
private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
// 通过反射获取Unsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base,_1GB, (byte)0);
System.in.read();
unsafe.freeMemory(base);
System.in.read();
}
}
直接内存_释放原理
直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
① 查看 allocateDirect
的实现
可以发现其内部创建了一个DirectByteBuffer
对象。
② 查看 DirectByteBuffer
类
- 可以发现内部确实是通过unsafe的
allocateMemory
和setMemory
分配直接内存 - 接着调用了一个 Cleaner 的
create
方法,传入两个参数,第一个是关联的DirectByteBuffer对象,第二个是回调任务对象Deallocator(在内部实现了run方法,通过unsafe.freeMemory 来手动释放内存)
③ 查看Cleaner
类
- Cleaner是一个虚引用类型,当它所关联的对象被回收时,Cleaner会触发
clean
方法 - 后台线程会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的
clean
方法,来清除直接内存中占用的内存。
public void clean() {
if (remove(this)) {
try {
// 都用函数的 run 方法, 释放内存
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
- 可以看到关键的一行代码,
this.thunk.run()
,thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法去释放内存。
直接内存的回收机制总结
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用
freeMemory
方法 - ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦
ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的clean
方法调用freeMemory
来释放直接内存
6.3 禁止显式回收
/**
* -XX:+DisableExplicitGC 显示的
*/
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
System.gc(); // 手动 gc 失效
System.in.read();
}
一般用 jvm 调优时,会加上下面的参数:
-XX:+DisableExplicitGC // 静止显示的 GC
意思就是禁止我们手动的 GC,比如手动 System.gc()
无效,它是一种 full gc
,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory
的方式释放内存。