JVM内存结构
1. 程序计数器 Program Counter Register
作用: 记住 当前线程 的下一条 JVM字节码指令 的 执行地址 ,便于进行 线程切换
特点:
- 是 线程私有 的,保证了各线程不会互相影响
- 不会存在内存溢出
为什么要使用PC寄存器记录当前线程的执行地址呢?
答:因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
PC寄存器为什么会被设定为线程私有?
答:多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。
为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,
让每个线程都独立计算,不会互相影响。
2. Java虚拟机栈 JVM Stacks
虚拟机栈是 线程运行 需要的内存空间
一个栈由多个 栈帧 组成,栈帧对应 方法调用 时占用的内存空间,每个线程只能有一个 活动栈帧
栈帧用于存放基本数据类型、对象的引用、方法出口等,是 线程私有 的
问题1:垃圾回收是否涉及栈内存?
答:不涉及,栈内存是JVM自动管理的,方法调用时入栈,方法运行结束后栈帧出栈,内存自动释放。
问题2:栈内存分配越大越好吗?
答:不是,因为物理内存的大小是一定的,所以栈内存越大,线程数就越小。
问题3:方法内的局部变量是否是线程安全的?
如果方法内的局部变量没有逃离方法的作用范围,它就是线程安全的。
如果局部变量引用了对象并逃离了方法的作用范围,它就有线程安全问题。
栈内存溢出:StackOverflowError
方法过多导致栈内存溢出: 无递归边界的递归调用
栈帧过大导致栈内存溢出
分配栈内存的大小:
-Xss1m
线程运行诊断:
案例1:cpu占用过高
步骤1:top命令查看进程的cpu占用情况,锁定进程id
步骤2:用如下命令进一步定位占用出问题的线程id:
ps H -eo pid,tid,%cpu | grep 进程id
步骤3:将10进制的线程id转成16进制,比如32665 ==> 7F99
步骤4:使用jstack命令查看进程信息,根据线程id 7F99查找线程,可看到线程的详细信息,进而定位出问题代码的行数
jstack 进程id
3. 本地方法栈 Native Method Stacks
本地方法栈 是JVM给 本地方法 的调用提供内存空间的栈
本地方法 是由其他语言(C、C++ 、汇编语言)编写的与操作系统底层交互的api
4. 堆 Heap
通过 new 关键字创建的对象都会使用堆内存
堆是 线程共享 的,堆中对象需要 考虑线程安全问题
有 垃圾回收机制 ,堆中不再被引用的对象将被回收释放
堆内存溢出:java.lang.OutOfMemoryError:java heap space
public static void main(String[] args) {
int i = 0;
ArrayList<String> list = new ArrayList<>();
String a = "BLU";
try {
while(true) {
list.add(a);
a = a+a;
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
java.lang.OutOfMemoryError: Java heap space
26
分配栈空间的大小:
-Xmx4g
堆内存诊断:
jps工具查看当前系统有哪些java进程
jmap工具查看堆内存的占用情况
jconsole工具:是图形界面的多功能的监测工具
示例代码:
public static void main(String[] args) throws InterruptedException {
System.out.println("1.....");
Thread.sleep(30000);
byte[] array = new byte[1024*1024*10];
System.out.println("2.....");
Thread.sleep(30000);
array = null;
System.gc();
System.out.println("3.....");
Thread.sleep(400000);
}
使用jmap工具测试实例:
运行开始,控制台打印1......
jps命令查看进程id:
C:\Users\73691>jps
12080 demo02
jmap -heap 12080查看堆内存占用情况(堆内存使用1.68MB):
Heap Usage:
PS Young Generation
Eden Space:
capacity = 29360128 (28.0MB)
used = 1762032 (1.6804046630859375MB)
free = 27598096 (26.319595336914062MB)
6.001445225306919% used
30s后,控制台打印2......,此时byte数组对象创建完毕
jmap -heap 12080查看堆内存占用情况(堆内存使用11.68MB):
Heap Usage:
PS Young Generation
Eden Space:
capacity = 29360128 (28.0MB)
used = 12247808 (11.680419921875MB)
free = 17112320 (16.319580078125MB)
41.715785435267854% used
30s后,控制台打印3......,此时byte数组对象被垃圾回收
jmap -heap 12080查看堆内存占用情况(堆内存使用0.56MB):
Heap Usage:
PS Young Generation
Eden Space:
capacity = 29360128 (28.0MB)
used = 587224 (0.5600204467773438MB)
free = 28772904 (27.439979553222656MB)
2.000073024204799% used
使用jconsole工具测试示例:
运行示例代码,使用jconsole命令打开监测工具,连接进程后即可实时查看堆内存使用情况:
使用jvisualvm监测:
示例代码:
public class demo03 {
public static void main(String[] args) throws InterruptedException {
List<Student> list = new ArrayList<Student>();
for (int i = 0; i < 200; i++) {
list.add(new Student());
}
Thread.sleep(100000000);
}
}
class Student {
private byte[] big = new byte[1024*1024];
}
执行,使用jconsole工具查看堆内存使用量,点击执行GC:
堆内存占用依然很高:
使用 jmap 查看详细信息(老年代Old Generation占用202.11MB):
Heap Usage:
PS Young Generation
Eden Space:
capacity = 106430464 (101.5MB)
used = 7077904 (6.7500152587890625MB)
free = 99352560 (94.74998474121094MB)
6.650261338708436% used
From Space:
capacity = 4718592 (4.5MB)
used = 0 (0.0MB)
free = 4718592 (4.5MB)
0.0% used
To Space:
capacity = 4718592 (4.5MB)
used = 0 (0.0MB)
free = 4718592 (4.5MB)
0.0% used
PS Old Generation
capacity = 304087040 (290.0MB)
used = 211930720 (202.11288452148438MB)
free = 92156320 (87.88711547851562MB)
69.69409811085669% used
使用jvisualvm工具,点击堆 Dump(堆转储)
点击查找 20 保留大小最大的对象,可以查看占用堆内存较大的对象信息:
5. 方法区 method Area
方法区是线程共享的区,存储了类结构相关信息:类的成员变量、方法数据、方法和构造器代码,还有一个运行时常量池
方法区在虚拟机启动时被创建,方法区逻辑上是堆的组成部分
方法区也会内存溢出
串池:StringTable
作用:避免字符串重复创建,提升性能,减少内存的开销
https://blog.csdn.net/soonfly/article/details/70147205
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
问:
System.out.println(s3==s4);
System.out.println(s3==s5);
System.out.println(s3==s6);
String s1 = "a"; 串池中存入a
String s2 = "b"; 串池中存入b
String s3 = "a"+"b"; 常量的拼接,在编译期值已经确定是ab,串池中存入ab
String s4 = s1 + s2; 相当于:new StringBuilder().append("a").append("b").toString(); new 的对象存在堆中
所以System.out.println(s3==s4); 为false
String s5 = "ab"; 串池中已有ab,引用即可
所以System.out.println(s3==s5); 为true
String s6 = s4.intern(); 尝试将字符串对象ab存入串池,并返回串池中的对象ab,所以s6引用的对象为串池中的ab
所以System.out.println(s3==s6); 为true
--------------------------------------------------------------------------------------------------------------------
String x2 = new String("c")+ new String("d");
String x1 = "cd";
x2.intern();
问:
System.out.println(x1==x2);
如果调换了最后两行代码的位置呢?
如果是jdk1.6呢?
StringTable的位置:在1.6中,StringTable是JVM常量池的一部分,在永久代中,1.7后,StringTable转移至堆中
原因:永久代在 Full GC 时才会触发垃圾回收,触发事件晚,内存回收效率不高。而堆只要 Minor GC 就会触发垃圾回收
6. 堆外内存(直接内存)
堆外内存定义: 内存对象分配在Java虚拟机的堆以外的内存,这些内存直接 受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。使用未公开的Unsafe和NIO包下ByteBuffer来创建堆外内存。
7. 垃圾回收
如何判断对象可以回收:
- 引用计数法(Lisp,Python,Ruby等语言使用的垃圾收集算法)
引用计数法的原理:
在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
引用计数法存在的问题:
- 需要额外的空间来存储计数器,以及繁琐的更新操作。
- 不能处理环形数据。如果有两个对象相互引用,那么这两个对象就不能被回收,因为它们的引用计数始终为1。这就是“内存泄漏”问题。
- 可达性分析算法(java语言使用的垃圾收集算法)
可达性分析算法的原理:
将一系列GC Roots作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该合集引用到的对象,并将其加入到该和集中,这个过程称之为标记(mark)。 最终,未被探索到的对象便是死亡的,是可以回收的。