自动内存管理机制
1.Java内存区域与内存溢出异常
程序计数器
- 如果正在执行的方法是Java方法,那么记录的是正在执行的虚拟机字节码指令的地址。但如果执行的本地方法,那么值会为空
Java虚拟机栈
存局部变量表、操作栈、动态链接、方法出口
-
- 局部变量表
-
-
- 编译期可知的基本数据类型(long、double占2个局部变量空间,其他占1个)
- 对象引用
-
-
-
- returnAddress
- 局部变量表需要的内存空间,在编译期完成分配
-
本地方法栈
堆
几乎所有对象、数组都在堆上分配(不一定所有)
- 划分:新生代、老年代
- 从内存分配角度划分:可划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
方法区
存虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
运行时常量池:
-
- class文件中有常量池,放了编译期产生的字面量、符号引用。这些在类加载后,会放入运行时常量池
- 相比于class文件中的常量池,具备动态性。即不要求一定要先预置入class文件常量池,再进运行时常量池。运行期间也可能放进新的常量到池里。应用:比如String.intern()
直接内存
并不属于虚拟机运行时数据区。
NIO类里,可以直接使用native函数直接分配堆外内存,通过Java堆里的DirectByteBuffer对象作为这块内存的引用。这样做可以避免在Java堆和Native堆里来回复制对象,可以提高性能
所以分配内存的时候,不能只考虑Java堆的大小,还要考虑直接内存。否则受物理内存和处理器寻址空间的限制,同样会内存溢出异常
对象访问
Object obj = new Object();其中,Object obj指的是,发生在本地变量表,作为一个reference类型数据,new Object()发生在堆,实例数据。方法区中会存储此对象的类型数据,如对象类型、父类、实现的接口、方法等
不同虚拟机有不同的实现,分为句柄访问、指针访问
句柄访问:
-
- 堆中划分出一块内存--句柄池,reference中存储的就是对象的句柄地址。
- 句柄中有指向方法区中类型数据的指针,有指向堆中实例数据的指针
-
- 优点是对象移动的时候(垃圾回收时发生很普遍),reference本身不需要修改,只需要改动指针指向的地址
指针访问:
-
- reference存储的就是对象地址
- 优点是速度更快,因为节省了一次指针定位的开销
-
- Sun HotSpot采用
2.垃圾收集器与内存分配策略
关注的目标:
-
- 每个栈帧中分配多少内存,在类结构确定时,基本就是确定(不考虑JIT优化),程序计数器、本地方法栈、虚拟机栈随着方法退出,这些区域的内存分配和回收是确定的,不需过多关心
- 堆和方法区中的内存,只有运行的时候才知道,内存的分配和回收时动态的。因此垃圾收集器关注的是这部分内存
如何确定对象已死,需要回收?
引用计数法
给对象一个引用计数器,初始值为1,当有地方引用它,计数器值+1,引用失效,计数器值-1,任何时刻只要计数器的值为0,这个对象就是不可能再被使用的,可以被回收
缺陷:
-
- 无法解决循环引用的问题,即A、B两个对象互相引用,但是没有外部引用指向他们
测试:
public class TestCounter {
static class Student{
private Object instance;
// 占用一点内存
private static final int _1MB = 1024 * 1024;
private static byte[] b = new byte[20 * _1MB];
}
public static void main(String[] args) {
Student studentA = new Student();
Student studentB = new Student();
studentA.instance = studentB;
studentB.instance = studentA;
studentA = null;
studentB = null;
System.gc();
}
}
[GC (System.gc()) [PSYoungGen: 24289K->872K(73728K)] 24289K->21360K(241664K), 0.0182375 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
[Full GC (System.gc()) [PSYoungGen: 872K->0K(73728K)] [ParOldGen: 20488K->21201K(167936K)] 21360K->21201K(241664K), [Metaspace: 3299K->3299K(1056768K)], 0.0072952 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
Heap
PSYoungGen total 73728K, used 635K [0x000000076e000000, 0x0000000773200000, 0x00000007c0000000)
eden space 63488K, 1% used [0x000000076e000000,0x000000076e09ecf8,0x0000000771e00000)
from space 10240K, 0% used [0x0000000771e00000,0x0000000771e00000,0x0000000772800000)
to space 10240K, 0% used [0x0000000772800000,0x0000000772800000,0x0000000773200000)
ParOldGen total 167936K, used 21201K [0x00000006ca000000, 0x00000006d4400000, 0x000000076e000000)
object space 167936K, 12% used [0x00000006ca000000,0x00000006cb4b45a0,0x00000006d4400000)
Metaspace used 3306K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
由此可以看到,循环引用的A、B仍然被GC回收了,所以JVM用的不是引用计数法
根搜索算法
即可达性分析。从一些GCRoot出发往下搜索,走过的路径称为引用链。当一个对象没有任何引用链相连(图论里称为不可达),则该对象是不可用的。
GCRoot:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中本地方法引用的对象
四种引用
强引用:直接引用
软引用:内存不够不会直接抛错误,而是回收软引用,如果还是不够才抛错误
弱引用:不管内存够不够,下一次gc一定会回收
虚引用:用来回收的时候收到一个通知
finalize()
当对象不可达的时候,会进行第一次筛选,并第一标记。判断对象是否有必要执行finalize()方法。如果没必要(1.该对象没有重写finalize()方法或2.方法已执行过一次),直接回收;如果有必要,会进一个F-Queue队列,稍后虚拟机建立一个低优先级的finalizer线程去执行这个方法(虚拟机不会等这个方法执行完成),有一次拯救自己的机会,所以只要有任何一个引用指向它,就会被标记,这样就移出了即将回收的集合。
/**
* 2022/2/13
*/
public class FinalizeEscapeGc {
public static FinalizeEscapeGc SAVE_HOOK = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed");
FinalizeEscapeGc.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGc();
// 第一次自救
escape();
// 再来一次
escape();
}
private static void escape() throws InterruptedException {
SAVE_HOOK = null;
System.gc();
// finalize优先级低 这里先暂停一下
Thread.sleep(500);
if (SAVE_HOOK != null) {
System.out.println("yes i am alive");
} else {
System.out.println("no i am dead");
}
}
}
上述代码可以看出,对象执行finalize有一次自救的机会
垃圾收集算法
标记-清除
最基础,其他的算法基于标记清除改进
缺点:产生较多内存碎片,如果有大对象就无法分配,只能触发一次额外GC
复制算法
把内存划分成两块,保留一块不用,只有其中一块。每次只要这块内存用完了,就把存活的对象复制到另一块,然后一次清除掉用过的内存
优点:内存分配不用考虑内存碎片,只要移动堆顶指针,按顺序分配内存。简单高效
缺点:可用内存缩小。
新生代是采用复制算法
Eden : Survivor = 8 : 1 : 1
每次保留其中一块survivor不用,可用空间为90%。新生代大部分对象朝生夕死
分配担保:当survivor没法放下存活的对象时,可以通过分配担保机制进入老年代
标记整理
让所有存活的对象向一端移动,直接清理掉端边界以外的内存
适合老年代
垃圾收集器
Serial收集器
单线程,垃圾收集时会Stop The World,后台停止用户线程,直到收集结束
虚拟机运行在client模式下的默认收集器,因为分配的内存不会很大,垃圾收集时间往往只需要几十毫秒,只要不频繁,完全可以接受。
ParNew收集器
Serial收集器的多线程版本,很多特性共用。
运行在Server模式下虚拟机的首选收集器,因为除了Serial,只有ParNew新生代收集器,是可以和真正并发的收集器--CMS收集器配合。因为CMS是无法与新生代收集器Parallel Scavenge收集器配合。
-XX:+UseConcMarkSweepGC,默认新生代收集器为ParNew
-XX:+UseParNewGC,强制使用ParNew收集器
ParNew在单CPU环境下可能效果并不比Serial要好,但是在多CPU环境下,GC时对系统资源利用有好处,默认线程数与CPU数相同
-XX:ParallelGCThread可以限制垃圾收集的线程数
Parallel Scavenge收集器
与Serial、ParNew一样是使用复制算法的新生代收集器,多线程。
不同之处在于,其他收集器关注尽可能缩短用户线程的停顿时间,而Parallel Scavenge收集器则致力于实现可控制的吞吐量
三个参数:
-XX:MaxGCPauseMills 垃圾收集最大停顿时间。缩短停顿时间是以牺牲吞吐量、新生代内存空间为代价。系统把新生代调小,那么收集更少的空间,需要停顿的时间也就更短,但这会导致GC更加频繁,可能反而总的GC时间更多,吞吐量降低
-XX:GCTimeRatio 直接设置吞吐量。如果设置19,那么GC时间是1/20=5%,吞吐量95%。如果设置99,那么GC时间是1/100=1%,吞吐量99%
-XX:+UseAdaptiveSizePolicy 这个参数,可以设置GC自适应调节策略。不需要指定新生代大小、Eden与Survivor比例、晋升老年代对象年龄等细节参数,只需要设置最大最小堆大小,设置一直关注的目标,-XX:MaxGCPauseMills或-XX:GCTimeRatio,让虚拟机动态调整参数提供最合适的停顿时间或吞吐量
Serial Old收集器
Serial收集器的老年代版本,使用标记-整理算法,单线程,同样在client模式下虚拟机使用
如果在server模式下使用,有两种用途:1.配合Parallel Scavenge 2.作为CMS的后背预案
Parallel Old收集器
Parallel Scavenge的老年代版本。多线程,标记-整理。
主要用途是配合Parallel Scavenge。因为Parallel Scavenge无法与CMS配合,在Parallel Old出现前,只能和Serial Old这种单线程收集器配合使用,服务端性能拖累
CMS收集器
四个阶段:
-
初始标记:标记GCRoot能直接关联到的对象,速度很快
并发标记:GC Roots Tracing
-
重新标记:修正并发标记期间,由于用户程序运行导致标记变动的那部分对象。停顿时间比初始标记长,但远比并发标记短
并发清除:
其中,初始标记、重新标记,仍然需要Stop the World。
最耗时的是并发标记、并发清除
CMS收集器采用标记清除算法
优点:
- 设计目标:最短回收停顿时间
缺点:
CPU资源敏感
无法处理浮动垃圾
产生内存碎片
G1收集器
与CMS相比改进点:
-
基于标记-整理算法,不会产生碎片
精确控制停顿。即可以指定在长度为M毫秒的时间段内,GC时间不超过N毫秒
为什么能够实现不牺牲吞吐量,完成低停顿回收?
避免全区域GC。把整个Java堆,划分为几个独立区域,跟踪垃圾堆积密度,后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。
GC常用参数
内存分配与回收策略
对象优先在Eden分配
/**
* 2022/2/26
* 参数 -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC
* Eden:10M,其中9M可用
* Serial+Serial Old组合
*/
public class TestAllocation {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] a1,a2,a3,a4,a5;
a1 = new byte[2 * _1MB];
a2 = new byte[2 * _1MB];
a3 = new byte[2 * _1MB]; // 出现一次Minor GC
a4 = new byte[4 * _1MB];
a5 = new byte[2 * _1MB];
}
}
这里的GC日志与书中的日志不相符。
书中描述,当分配a4的时候,由于共9M的eden空间不足,于是触发MinorGC,但是由于Survivor区只有1M,空间不足以放a1、a2、a3,于是通过分配担保机制,这6M大小进入老年代,4M的a4会分配在Elden
但是实测下来,分配了5M之前都是在Eden区,但是a1、a2、a3共6M分配之后,就已经开始出现MinorGC。a1、a2共4M进入了老年代,a3分配到了eden
[GC (Allocation Failure) [DefNew: 6444K->810K(9216K), 0.0042813 secs] 6444K->4906K(19456K), 0.0043266 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 3188K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 29% used [0x00000000fec00000, 0x00000000fee52998, 0x00000000ff400000)
from space 1024K, 79% used [0x00000000ff500000, 0x00000000ff5ca8f8, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 3303K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
分配完a4的日志如下:可以看出a3、a4共计6M分配在Eden区,老年代4M
[GC (Allocation Failure) [DefNew: 6444K->797K(9216K), 0.0033512 secs] 6444K->4893K(19456K), 0.0034188 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 7429K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 80% used [0x00000000fec00000, 0x00000000ff279e48, 0x00000000ff400000)
from space 1024K, 77% used [0x00000000ff500000, 0x00000000ff5c76a8, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
如果再分配1M的a5,仍然在Eden区,共7M,这里很奇怪,之前6M就已经触发MinorGC,这时没有触发,如果a5大小为2M,会触发第二次MinorGC,a3也会进入老年代,剩下a4、a5共6M在Eden
[GC (Allocation Failure) [DefNew: 6444K->800K(9216K), 0.0035625 secs] 6444K->4897K(19456K), 0.0036098 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew (promotion failed) : 7350K->6570K(9216K), 0.0019683 secs][Tenured: 6845K->6845K(10240K), 0.0024806 secs] 11446K->10962K(19456K), [Metaspace: 3258K->3258K(1056768K)], 0.0045052 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 6466K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 78% used [0x00000000fec00000, 0x00000000ff250950, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 6845K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 66% used [0x00000000ff600000, 0x00000000ffcaf558, 0x00000000ffcaf600, 0x0000000100000000)
Metaspace used 3265K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
- MinorGC:新生代的GC,新生代对象朝生夕死,回收速度比较快
- MajorGC/FullGC:老年代的GC,出现了MajorGC,一般伴随着MinorGC(不绝对,ParallelScavenge可以选择直接MajorGC)。MajorGC速度比MinorGC慢10倍以上
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象,比如上面的byte[],我们应该尽量避免大对象,因为会容易导致内存中海油不少空间就提前触发GC来存放大对象
-XX:PretenureSizeThreshold=10M
可以设置阈值,大于阈值的对象,会直接分配在老年代
避免Eden区与Survivor区之间发生大量内存拷贝
只对Serial、ParNew两款收集器有效
长期存活的对象将进入老年代
出生在Eden区的对象,对象年龄为0,经过第一次MinorGC且能被Survivor容纳的话,对象年龄为1。然后每熬过一轮MinorGC,对象年龄+1,当达到阈值(默认15),将进入老年代。阈值通过参数-XX:MaxTenuringThreshold设置
/**
* 2022/2/27
* 进入老年代的阈值-XX:MaxTenuringThreshold
* -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
*/
public class TestTenuringThreshold {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] a1,a2,a3;
a1 = new byte[_1MB / 4];
a2 = new byte[4 * _1MB];
a3 = new byte[4 * _1MB];
a3 = null;
a3 = new byte[4 * _1MB];
}
}
实测下来,现象与书中不一致,原因暂时没有想明白。
MaxTenuringThreshold=1:
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 1048576 bytes, 1048576 total
: 6700K->1024K(9216K), 0.0052175 secs] 6700K->5173K(19456K), 0.0052856 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 1792 bytes, 1792 total
: 5368K->1K(9216K), 0.0016983 secs] 9517K->5109K(19456K), 0.0017603 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4235K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff022798, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400700, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 5107K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 49% used [0x00000000ff600000, 0x00000000ffafce90, 0x00000000ffafd000, 0x0000000100000000)
Metaspace used 3324K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
书中描述:a1大小256k,分配a2的时候触发第一次MinorGC,Survivor足够容纳,进入Survivor,对象年龄为1。第二次MinorGC时,因为阈值为1,此时进入老年代。新生代会干净
MaxTenuringThreshold=15:
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 1048576 bytes, 1048576 total
: 6700K->1024K(9216K), 0.0029448 secs] 6700K->5148K(19456K), 0.0029874 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age 1: 1840 bytes, 1840 total
: 5368K->1K(9216K), 0.0012153 secs] 9492K->5078K(19456K), 0.0012501 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4235K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff022568, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400730, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 5076K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 49% used [0x00000000ff600000, 0x00000000ffaf52d0, 0x00000000ffaf5400, 0x0000000100000000)
Metaspace used 3261K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
书中描述:当第二次MinorGC后,a1仍然留在Survivor,仍然有404k空间被占用。a1对象年龄为2
空间分配担保
MinorGC时,虚拟机检测之前每次晋升到老年代的对象平均大小,和老年代剩余空间比较,
-
如果大于,说明老年代很可能剩余空间不够这次晋升了,发生FullGC
如果小于
-
-
-XX:-HandlerPromotionFailure,开关打开,只会进行MinorGC。
开关关闭,FullGC
-
这个开关打开的话,就是认为不怕担保失败,认为这次大概率小于平均值,可以担保成功。如果关闭的话,就是悲观地认为空间肯定不够,干脆直接FullGC
如果本次对象大小突增,远高于平均值,那么就导致担保失败,失败之后会进行一次FullGC
大部分情况下,会把开关打开,避免频繁FullGC
3.虚拟机性能监控与故障处理工具
jps
D:\Java\DemoCode\JVM>jps
543116 Launcher
173156
250948 TestDeadLock
260692 Jps
476140 Launcher
可以列出正在运行的进程。由LVMID(Local Virtual Machine Identifier)、主类名称组成
对于本地虚拟机进程:LVMID和操作系统的进程ID一致
后缀 | 功能 |
---|---|
-q | 只输出LVMID |
-m | 输出传给main()函数的参数 |
-l | 类的全名,如果执行的是Jar包,输出Jar包路径 |
-v | 进程启动时JVM参数 |
jstat
格式:jstat option vmid [interval[s|ms] [count]]
-
本地虚拟机vmid和lvmid一致, interval表示查询频率,默认ms,count表示查询次数
如:jstat -gc 250948 250 20
- -gcutil
D:\Java\DemoCode\JVM>jstat -gcutil 250948
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 33.73 43.81 0.01 94.27 88.95 1 0.005 0 0.000 0.005
含义:
S0 | S1 | E | O | M | CCS | YGC | YGCT | FGC | FGCT | GCT |
---|---|---|---|---|---|---|---|---|---|---|
Survivor | Eden区 | 永久代 | YoungGC次数 | YoungGC耗时 | FullGC次数 | FullGC耗时 | 总GC耗时 |
如果是P,表示永久代(Permanent)
jinfo
jinfo [option] pid
如:jinfo 250948
jmap
可以获得堆转存储快照(heapdump/dump文件)
获得dump文件的方式:
-
- kill -3,恐吓虚拟机
- -XX:+HeapDumpOnOutOfMemoryError,可以在OOM之后,自动生成dump文件
- 命令格式:jmap [option] vmid
jhat
dump文件的分析工具,一般不用,不会在服务器上直接分析dump文件,因为比较消耗硬件资源,而且功能也少
一般用:Visual VM或者专业分析dump工具,如Eclipse Memory Analyzer、IBM HeapAnalyzer
jstack
用于生成线程堆栈快照(threaddump文件或javacore文件),目的是定位线程长时间停顿原因,如线程死锁、死循环、请求外部资源导致的长时间等待
jstack [option] vmid
- 实际项目中,可以通过下面的方法,输出堆栈信息,做成管理员页面
public static void main(String[] args) {
for (Map.Entry threadEntry : Thread.getAllStackTraces().entrySet()) {
Thread thread = threadEntry.getKey();
StackTraceElement[] stackTrace = threadEntry.getValue();
if (thread.equals(Thread.currentThread())){
continue;
}
System.out.print("\n线程:"+ thread.getName()+"\n");
for (StackTraceElement stackTraceElement : stackTrace) {
System.out.print("\t"+ stackTraceElement+"\n");
}
}
}
可视化工具
Jconsole
1.内存监控
/**
* 2022/3/6
* -Xms100m -Xmx100m -XX:+UseSerialGC
*/
public class TestMonitorMemory {
static class OOMObject{
public byte[] placeHolder = new byte[64 * 1024];
}
public static void main(String[] args) throws InterruptedException {
fillHeap(1000);
System.out.println("执行完了方法");
}
public static void fillHeap(int num) throws InterruptedException {
List list = new ArrayList<>();
for (int i = 0; i < num; i++) {
Thread.sleep(50);
list.add( new OOMObject());
}
System.gc();
}
}
运行代码,指定堆内存100M。每次生成一个64K的对象,大概1600个对象会把堆填充满,然后发生OOM
Eden区表现为折线图,一直在增加,满了就回收
上图中,堆内存表现为一直上涨,即时是循环了1000次,然后执行了System.gc(),被填充到堆中的对象还活着。
这是因为执行的时候,fillHeap方法仍然没有退出,list对象在执行的时候仍然处于作用域内。所以要把它放在fillHeap方法外执行
如果执行2000次,会发生下面的结果,直到OOM,把堆内存清空
如下,把System.gc()放在方法外执行
/**
* 2022/3/6
* -Xms100m -Xmx100m -XX:+UseSerialGC
*/
public class TestMonitorMemory {
static class OOMObject{
public byte[] placeHolder = new byte[64 * 1024];
}
public static void main(String[] args) throws InterruptedException {
fillHeap(1000);
System.gc();
System.out.println("执行完了gc");
Thread.sleep(100000);
}
public static void fillHeap(int num) throws InterruptedException {
List list = new ArrayList<>();
for (int i = 0; i < num; i++) {
Thread.sleep(50);
list.add( new OOMObject());
}
}
}