360抢票王五代-windows7主题包

stackoverflowerror
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;

Listlist=newArrayList<>();

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.(:123)

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