OOM分析
Java堆内存溢出
启动参数:
- -Xms250m -Xmx250m ,最大最小堆内存250m,禁止自动扩展内存
- -XX:+HeapDumpOnOutOfMemoryError:在发生OOM时进行堆内存Dump生成快照
public class oomController {
@RequestMapping(value = "/test", method = RequestMethod.GET)
public @ResponseBody
void test() {
List<TestInst> testInstList = new ArrayList<>();
Long i = 0L;
while (true) {
TestInst testInst = new TestInst();
testInstList.add(testInst);
}
}
}
class TestInst {
String name;
String desc;
}
- 不不断的创建对象,并且保证GC Roots到对象之间有 可达路路径来避免垃圾回收机制清除这些对象
- 启动项目并调用上述方法,项目将马上报错内存溢出:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid98284.hprof ...
Heap dump file created [338473308 bytes in 1.782 secs]
七月 19, 2021 11:09:28 下午 org.apache.catalina.core.StandardWrapperValve invoke
严重: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] with root cause
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.example.gangtie.controller.oomController.test(oomController.java:34)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
- 分析生成的堆dump文件java_pid98284.hprof
使用JVM自带工具jvisualvm,一般其路径位于$JAVA_PATH/jdk/bin中
元空间/方法区溢出
- JDK8 中将永久代移除,使用 MetaSpace 来保存类加载之后的类信息
- JDK 8 中将字符串常量池也被移动到 Java 堆,将原有的永久代移动到了本地堆中成为 MetaSpace
- Java7
抛出永久代溢出 java.lang.OutOfMemoryError:PermGen Space - Java8
抛出元空间溢出 java.lang.OutOfMemoryError:Metaspace
- 借助CGLib直接操作字节码运⾏时产⽣生⼤量的动态类, 最终导致内存溢出
启动参数:
- -XX:MaxMetaspaceSize=100M
@RequestMapping(value = "/test2", method = RequestMethod.GET)
public @ResponseBody
void test2() {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(oomController.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o, objects);
}
});
enhancer.create();
}
}
线程Dump
- 调试排错 - Java线程Dump分析
一般当服务器挂起,崩溃或者性能低下时,就需要抓取服务器的线程堆栈(Thread Dump)用于后续的分析。在实际运行中,往往一次 dump的信息,还不足以确认问题。为了反映线程状态的动态变化,需要接连多次做thread dump,每次间隔10-20s,建议至少产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性。
- 获取PID
jps 或 ps –ef | grep java - 获取ThreadDump
jstack [-l ] < pid > | tee -a jstack.log
ThreadDump信息:
1. "Timer-0" daemon prio=10 tid=0xac190c00 nid=0xaef in Object.wait() [0xae77d000]
2. java.lang.Thread.State: TIMED_WAITING (on object monitor)
3. at java.lang.Object.wait(Native Method)
4. -waiting on <0xb3885f60> (a java.util.TaskQueue) # 继续wait
5. at java.util.TimerThread.mainLoop(Timer.java:509)
6. -locked <0xb3885f60> (a java.util.TaskQueue) # 已经locked
7. at java.util.TimerThread.run(Timer.java:462)
第一行信息:
- 线程名称:Timer-0;
- 线程类型:daemon;
- 优先级: 10,默认是5;
- JVM线程id:tid=0xac190c00,JVM内部线程的唯一标识(通过java.lang.Thread.getId()获取,通常用自增方式实现)。
- 对应系统线程id(NativeThread ID):nid=0xaef,和top命令查看的线程pid对应,不过一个是10进制,一个是16进制。(通过命令:top -H -p pid,可以查看该进程的所有线程信息)
- 线程状态:in Object.wait();
- 起始栈地址:[0xae77d000],对象的内存地址,通过JVM内存查看工具,能够看出线程是在哪儿个对象上等待;
第二行信息:
表示线程状态:TIMED_WAITING
2-7行的信息:
Java thread statck trace:到目前为止这是最重要的数据,Java stack trace提供了大部分信息来精确定位问题根源。堆栈信息应该逆向解读:程序先执行的是第7行,然后是第6行,依次类推。也就是说对象先上锁,锁住对象0xb3885f60,然后释放该对象锁,进入waiting状态
使用场景
- CPU飙高,load高,响应很慢
一个请求过程中多次dump;
对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了; - 查找占用CPU最多的线程
使用命令:top -H -p pid(pid为被测系统的进程号),找到导致CPU高的线程ID,对应thread dump信息中线程的nid,只不过一个是十进制,一个是十六进制;
在thread dump中,根据top命令查找的线程id,查找对应的线程堆栈信息; - CPU使用率不高但是响应很慢
进行dump,查看是否有很多thread struck在了i/o、数据库等地方,定位瓶颈原因; - 请求无法响应
多次dump,对比是否所有的runnable线程都一直在执行相同的方法,如果是的,恭喜你,锁住了! - 死锁
死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的CPU占用率为零,很快会从top或prstat的输出中消失。
比如在下面这个示例中,是个较为典型的死锁情况:
"Thread-1" prio=5 tid=0x00acc490 nid=0xe50 waiting for monitor entry [0x02d3f000
..0x02d3fd68]
at deadlockthreads.TestThread.run(TestThread.java:31)
- waiting to lock <0x22c19f18> (a java.lang.Object)
- locked <0x22c19f20> (a java.lang.Object)
"Thread-0" prio=5 tid=0x00accdb0 nid=0xdec waiting for monitor entry [0x02cff000
..0x02cff9e8]
at deadlockthreads.TestThread.run(TestThread.java:31)
- waiting to lock <0x22c19f20> (a java.lang.Object)
- locked <0x22c19f18> (a java.lang.Object)
附. 线程状态
- NEW 新建线程对象
Thread t = new Thread();当刚刚在堆内存中创建Thread对象,还没有调用t.start()方法之前,线程就处在NEW状态。
在这个状态上,线程与普通的java对象没有什么区别,就仅仅是一个堆内存中的对象。 - RUNNABLE 等待运行+正在运行:
表示线程在运行队列中等待CPU等资源的调度、或者正在运行。 这个状态的线程比较正常,但如果线程长时间停留在在这个状态就不正常了,这说明线程运行的时间很长(存在性能问题),或者是线程一直得不到执行的机会(存在线程饥饿的问题)。 - BLOCKED
线程正在等待获取java对象的Minor(也叫内置锁),即线程正在等待进入由synchronized保护的方法或者代码块。 - WAITING
处在该线程的状态,正在等待某个事件的发生,只有特定的条件满足,才能获得执行机会。而产生这个特定的事件,通常都是另一个线程。
比如: A线程调用了obj对象的obj.wait()方法,如果没有线程调用obj.notify或obj.notifyAll,那么A线程就没有办法恢复运行;如果A线程调用了LockSupport.park(),没有别的线程调用LockSupport.unpark(A),那么A没有办法恢复运行。 - TIMED_WAITING
如果线程进入了WAITING状态,一定要特定的事件发生才能恢复运行;而处在TIMED_WAITING的线程,如果特定的事件发生或者是时间流逝完毕,都会恢复运行。 - TERMINATED
线程执行完毕,执行完run方法正常返回,或者抛出了运行时异常而结束,线程都会停留在这个状态。这个时候线程只剩下Thread对象了,没有什么用了。
Q:线程的 BLOCKED 和 WAITING 状态的区别?
- 阻塞态:是指当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。在Java中,阻塞态专指请求锁失败时进入的状态。由一个**同步队列(阻塞队列)**存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。
- 等待态:当前线程中调用wait、join、park函数时,当前线程就会进入等待态,只能等待其他线程的指示才能继续运行。进入等待态的线程会释放CPU执行权,并释放资源(比如锁)。也有一个等待队列存放所有等待态的线程。
- 当前线程调用object.wait方法后,释放对象锁,这个状态就是WAITING状态,线程处于等待队列,等待其他线程同一个对象调用notify或者notifyAll方法。
在调用notify或者notifyAll方法后,调用wait的等待线程不会立刻从等待队列返回,而是从等待队列移动到同步队列,准备竞争对象监视器的这种状态就是BLOCKED。