360抢票王五代-windows7主题包
![stackoverflowerror](/uploads/image/0292.jpg)
2023年4月3日发(作者:内存卡不能格式化)
快速定位Java内存OOM的问题
Java服务出现了OOM(OutOfMemory)问题,总结了⼀些相对通⽤的⽅案,希望能帮助到Java技术栈的同学。
某Java服务(假设PID=10765)出现了OOM,最常见的原因为:
有可能是内存分配确实过⼩,⽽正常业务使⽤了⼤量内存
某⼀个对象被频繁申请,却没有释放,内存不断泄漏,导致内存耗尽
某⼀个资源被频繁申请,系统资源耗尽,例如:不断创建线程,不断发起⽹络连接
画外⾳:⽆⾮“本⾝资源不够”“申请资源太多”“资源耗尽”⼏个原因。
更具体的,可以使⽤以下⼯具逐⼀排查。
⼀、确认是不是内存本⾝就分配过⼩
⽅法:jmap-heap10765
如上图,可以查看新⽣代,⽼⽣代堆内存的分配⼤⼩以及使⽤情况,看是否本⾝分配过⼩。
⼆、找到最耗内存的对象
⽅法:jmap-histo:live10765|more
如上图,输⼊命令后,会以表格的形式显⽰存活对象的信息,并按照所占内存⼤⼩排序:
实例数
所占内存⼤⼩
类名
是不是很直观?对于实例数较多,占⽤内存⼤⼩较多的实例/类,相关的代码就要针对性review了。
上图中占内存最多的对象是RingBufferLogEvent,共占⽤内存18M,属于正常使⽤范围。
如果发现某类对象占⽤内存很⼤(例如⼏个G),很可能是类对象创建太多,且⼀直未释放。例如:
申请完资源后,未调⽤close()或dispose()释放资源
消费者消费速度慢(或停⽌消费了),⽽⽣产者不断往队列中投递任务,导致队列中任务累积过多
画外⾳:线上执⾏该命令会强制执⾏⼀次fgc。另外还可以dump内存进⾏分析。
三、确认是否是资源耗尽
⼯具:
pstree
netstat
查看进程创建的线程数,以及⽹络连接数,如果资源耗尽,也可能出现OOM。
这⾥介绍另⼀种⽅法,通过
/proc/${PID}/fd
/proc/${PID}/task
可以分别查看句柄详情和线程数。
例如,某⼀台线上服务器的sshd进程PID是9339,查看
ll/proc/9339/fd
ll/proc/9339/task
如上图,sshd共占⽤了四个句柄
0->标准输⼊
1->标准输出
2->标准错误输出
3->socket(容易想到是监听端⼝)
sshd只有⼀个主线程PID为9339,并没有多线程。
所以,只要
ll/proc/${PID}/fd|wc-l
ll/proc/${PID}/task|wc-l(效果等同pstree-p|wc-l)
就能知道进程打开的句柄数和线程数。
补充:Java内存溢出OOM
Java内存溢出OOM
经典错误
JVM中常见的两个错误
StackoverFlowError:栈溢出
OutofMemoryError:javaheapspace:堆溢出
除此之外,还有以下的错误
verflowError
emoryError:javaheapspace
emoryError:GCoverheadlimitexceeeded
emoryError:Directbuffermemory
emoryError:unabletocreatenewnativethread
emoryError:Metaspace
架构
OutOfMemoryError和StackOverflowError是属于Error,不是Exception
StackoverFlowError
堆栈溢出,我们有最简单的⼀个递归调⽤,就会造成堆栈溢出,也就是深度的⽅法调⽤
栈⼀般是512K,不断的深度调⽤,直到栈被撑破
publicclassStackOverflowErrorDemo{
publicstaticvoidmain(String[]args){
stackOverflowError();
}
/**
*栈⼀般是512K,不断的深度调⽤,直到栈被撑破
*Exceptioninthread"main"verflowError
*/
privatestaticvoidstackOverflowError(){
stackOverflowError();
}
}
运⾏结果
Exceptioninthread"main"verflowError
verflowError(:17)
OutOfMemoryError
javaheapspace
创建了很多对象,导致堆空间不够存储
/**
*Java堆内存不⾜
*/
publicclassJavaHeapSpaceDemo{
publicstaticvoidmain(String[]args){
//堆空间的⼤⼩-Xms10m-Xmx10m
//创建⼀个80M的字节数组
byte[]bytes=newbyte[80*1024*1024];
}
}
我们创建⼀个80M的数组,会直接出现Javaheapspace
Exceptioninthread"main"emoryError:Javaheapspace
GCoverheadlimitexceeded
GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过了98%的时间⽤来做GC,并且回收了不到2%的堆内存
连续多次GC都只回收了不到2%的极端情况下,才会抛出。假设不抛出GCoverheadlimit错误会造成什么情况呢?
那就是GC清理的这点内存很快会再次被填满,迫使GC再次执⾏,这样就形成了恶性循环,CPU的使⽤率⼀直都是100%,⽽GC却没有任何成果。
代码演⽰:
为了更快的达到效果,我们⾸先需要设置JVM启动参数
-Xms10m-Xmx10m-XX:+PrintGCDetails-XX:MaxDirectMemorySize=5m
这个异常出现的步骤就是,我们不断的像list中插⼊String对象,直到启动GC回收
/**
*GC回收超时
*JVM参数配置:-Xms10m-Xmx10m-XX:+PrintGCDetails
*/
publicclassGCOverheadLimitDemo{
publicstaticvoidmain(String[]args){
inti=0;
List
try{
while(true){
//1.6时intern()⽅法发现字符串常量池(存储永久代)没有就复制,物理拷贝
//1.7时intern()⽅法发现字符串常量池(存储堆)没有就在保存地址值映射实际堆内存对象
(f(++i).intern());
}
}catch(Exceptione){
n("***************i:"+i);
tackTrace();
throwe;
}finally{
}
}
}
运⾏结果
[FullGC(Ergonomics)[PSYoungGen:2047K->2047K(2560K)][ParOldGen:7106K->7106K(7168K)]9154K->9154K(9728K),[Metaspace:3504K->3504K(1056768K)],0.0311093secs][Times:user=0.13sys=0.00,real=0.03secs]
[FullGC(Ergonomics)[PSYoungGen:2047K->0K(2560K)][ParOldGen:7136K->667K(7168K)]9184K->667K(9728K),[Metaspace:3540K->3540K(1056768K)],0.0058093secs][Times:user=0.00sys=0.00,real=0.01secs]
Heap
PSYoungGentotal2560K,used114K[0x00000000ffd00000,0x0000,0x0000)
edenspace2048K,5%used[0x00000000ffd00000,0x00000000ffd1c878,0x00000000fff00000)
fromspace512K,0%used[0x00000000fff80000,0x00000000fff80000,0x0000)
tospace512K,0%used[0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGentotal7168K,used667K[0x00000000ff600000,0x00000000ffd00000,0x00000000ffd00000)
objectspace7168K,9%used[0x00000000ff600000,0x00000000ff6a6ff8,0x00000000ffd00000)
Metaspaceused3605K,capacity4540K,committed4864K,reserved1056768K
classspaceused399K,capacity428K,committed512K,reserved1048576K
Exceptioninthread"main"emoryError:GCoverheadlimitexceeded
ng(:403)
f(:3099)
(:18)
我们能够看到多次FullGC,并没有清理出空间,在多次执⾏GC操作后,就抛出异常GCoverheadlimit
Directbuffermemory
Netty+NIO:这是由于NIO引起的
写NIO程序的时候经常会使⽤ByteBuffer来读取或写⼊数据,这是⼀种基于通道(Channel)与缓冲区(Buffer)的I/O⽅式,它可以使⽤Native函数库直接分配堆外内存,然后通过⼀个存储在
Java堆⾥⾯的DirectByteBuffer对象作为这块内存的引⽤进⾏操作。这样能在⼀些场景中显著提⾼性能,因为避免了在Java堆和Native堆中来回复制数据。
te(capability):第⼀种⽅式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
eDirect(capability):第⼆种⽅式是分配OS本地内存,不属于GC管辖范围,由于不需要内存的拷贝,所以速度相对较快
但如果不断分配本地内存,堆内存很少使⽤,那么JVM就不需要执⾏GC,DirectByteBuffer对象就不会被回收,这时候堆内存充⾜,但本地内存可能已经使⽤光了,再次尝试分配本地内存
就会出现OutOfMemoryError,那么程序就崩溃了。
⼀句话说:本地内存不⾜,但是堆内存充⾜的时候,就会出现这个问题
我们使⽤-XX:MaxDirectMemorySize=5m配置能使⽤的堆外物理内存为5M
-Xms20m-Xmx20m-XX:+PrintGCDetails-XX:MaxDirectMemorySize=5m
然后我们申请⼀个6M的空间
//只设置了5M的物理内存使⽤,但是却分配6M的空间
ByteBufferbb=teDirect(6*1024*1024);
这个时候,运⾏就会出现问题了
配置的maxDirectMemory:5.0MB
[GC(())[PSYoungGen:2030K->488K(2560K)]2030K->796K(9728K),0.0008326secs][Times:user=0.00sys=0.00,real=0.00secs]
[FullGC(())[PSYoungGen:488K->0K(2560K)][ParOldGen:308K->712K(7168K)]796K->712K(9728K),[Metaspace:3512K->3512K(1056768K)],0.0052052secs][Times:user=0.09sys=0.00,real=0.00secs]
Exceptioninthread"main"emoryError:Directbuffermemory
eMemory(:693)
ByteBuffer.
teDirect(:311)
(:19)
unabletocreatenewnativethread
不能够创建更多的新的线程了,也就是说创建线程的上限达到了
在⾼并发场景的时候,会应⽤到
⾼并发请求服务器时,经常会出现如下异常emoryError:unabletocreatenewnativethread,准确说该nativethread异常与对应的平台有关
导致原因:
应⽤创建了太多线程,⼀个应⽤进程创建多个线程,超过系统承载极限
服务器并不允许你的应⽤程序创建这么多线程,linux系统默认运⾏单个进程可以创建的线程为1024个,如果应⽤创建超过这个数量,就会报emoryError:unabletocreate
newnativethread
解决⽅法:
想办法降低你应⽤程序创建线程的数量,分析应⽤是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
对于有的应⽤,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改linux服务器配置,扩⼤linux默认限制
/**
*⽆法创建更多的线程
*/
publicclassUnableCreateNewThreadDemo{
publicstaticvoidmain(String[]args){
for(inti=0;;i++){
n("**************i="+i);
newThread(()->{
try{
(_VALUE);
}catch(InterruptedExceptione){
tackTrace();
}
},f(i)).start();
}
}
}
这个时候,就会出现下列的错误,线程数⼤概在900多个
Exceptioninthread"main"emoryError:unabletoceratenewnativethread
如何查看线程数
ulimit-u
Metaspace
元空间内存不⾜,Matespace元空间应⽤的是本地内存
-XX:MetaspaceSize的初始化⼤⼩为20M
元空间是什么
元空间就是我们的⽅法区,存放的是类模板,类信息,常量池等
Metaspace是⽅法区HotSpot中的实现,它与持久代最⼤的区别在于:Metaspace并不在虚拟内存中,⽽是使⽤本地内存,也即在java8中,classmetadata(thevirtualmachinesinternal
presentationofJavaclass),被存储在叫做Matespace的nativememory
永久代(java8后背元空间Metaspace取代了)存放了以下信息:
虚拟机加载的类信息
常量池
静态变量
即时编译后的代码
模拟Metaspace空间溢出,我们不断⽣成类往元空间⾥灌输,类占据的空间总会超过Metaspace指定的空间⼤⼩
代码
在模拟异常⽣成时候,因为初始化的元空间为20M,因此我们使⽤JVM参数调整元空间的⼤⼩,为了更好的效果
-XX:MetaspaceSize=8m-XX:MaxMetaspaceSize=8m
代码如下:
/**
*元空间溢出
*
*/
publicclassMetaspaceOutOfMemoryDemo{
//静态类
staticclassOOMTest{
}
publicstaticvoidmain(finalString[]args){
//模拟计数多少次以后发⽣异常
inti=0;
try{
while(true){
i++;
//使⽤Spring的动态字节码技术
Enhancerenhancer=newEnhancer();
erclass();
Cache(false);
lback(newMethodInterceptor(){
@Override
publicObjectintercept(Objecto,Methodmethod,Object[]objects,MethodProxymethodProxy)throwsThrowable{
Super(o,args);
}
});
}
}catch(Exceptione){
n("发⽣异常的次数:"+i);
tackTrace();
}finally{
}
}
}
会出现以下错误:
发⽣异常的次数:201
emoryError:Metaspace
注意
在JDK1.7之前:永久代是⽅法区的实现,存放了运⾏时常量池、字符串常量池和静态变量等。
在JDK1.7:永久代是⽅法区的实现,将字符串常量池和静态变量等移出⾄堆内存。运⾏时常量池等剩下的还再永久代(⽅法区)
在JDK1.8及以后:永久代被元空间替代,相当于元空间实现⽅法区,此时字符串常量池和静态变量还在堆,运⾏时常量池还在⽅法区(元空间),元空间使⽤的是直接内存。
-XX:MetaspaceSize=N//设置Metaspace的初始(和最⼩⼤⼩)-XX:MaxMetaspaceSize=N//设置Metaspace的最⼤⼤⼩与永久代很⼤的不同就是,如果不指定⼤⼩的话,随着更多类的创
建,虚拟机会耗尽所有可⽤的系统内存。
以上为个⼈经验,希望能给⼤家⼀个参考,也希望⼤家多多⽀持。如有错误或未考虑完全的地⽅,望不吝赐教。
更多推荐
stackoverflowerror
发布评论