【JVM】常见调试排错场景、内存与线程Dump分析

目录

  • OOM分析
    • Java堆内存溢出
    • 元空间/方法区溢出
  • 线程Dump

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中
    【JVM】常见调试排错场景、内存与线程Dump分析_第1张图片

元空间/方法区溢出

  • JDK8 中将永久代移除,使用 MetaSpace 来保存类加载之后的类信息
  • JDK 8 中将字符串常量池也被移动到 Java 堆,将原有的永久代移动到了本地堆中成为 MetaSpace
  1. Java7
    抛出永久代溢出 java.lang.OutOfMemoryError:PermGen Space
  2. 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都指向同一个问题,我们才确定问题的典型性。
  1. 获取PID
    jps 或 ps –ef | grep java
  2. 获取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状态

使用场景

  1. CPU飙高,load高,响应很慢
    一个请求过程中多次dump;
    对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了;
  2. 查找占用CPU最多的线程
    使用命令:top -H -p pid(pid为被测系统的进程号),找到导致CPU高的线程ID,对应thread dump信息中线程的nid,只不过一个是十进制,一个是十六进制;
    在thread dump中,根据top命令查找的线程id,查找对应的线程堆栈信息;
  3. CPU使用率不高但是响应很慢
    进行dump,查看是否有很多thread struck在了i/o、数据库等地方,定位瓶颈原因;
  4. 请求无法响应
    多次dump,对比是否所有的runnable线程都一直在执行相同的方法,如果是的,恭喜你,锁住了!
  5. 死锁
    死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的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) 

附. 线程状态

  1. NEW 新建线程对象
    Thread t = new Thread();当刚刚在堆内存中创建Thread对象,还没有调用t.start()方法之前,线程就处在NEW状态。
    在这个状态上,线程与普通的java对象没有什么区别,就仅仅是一个堆内存中的对象。
  2. RUNNABLE 等待运行+正在运行
    表示线程在运行队列中等待CPU等资源的调度、或者正在运行。 这个状态的线程比较正常,但如果线程长时间停留在在这个状态就不正常了,这说明线程运行的时间很长(存在性能问题),或者是线程一直得不到执行的机会(存在线程饥饿的问题)。
  3. BLOCKED
    线程正在等待获取java对象的Minor(也叫内置锁),即线程正在等待进入由synchronized保护的方法或者代码块。
  4. WAITING
    处在该线程的状态,正在等待某个事件的发生,只有特定的条件满足,才能获得执行机会。而产生这个特定的事件,通常都是另一个线程。
    比如: A线程调用了obj对象的obj.wait()方法,如果没有线程调用obj.notify或obj.notifyAll,那么A线程就没有办法恢复运行;如果A线程调用了LockSupport.park(),没有别的线程调用LockSupport.unpark(A),那么A没有办法恢复运行。
  5. TIMED_WAITING
    如果线程进入了WAITING状态,一定要特定的事件发生才能恢复运行;而处在TIMED_WAITING的线程,如果特定的事件发生或者是时间流逝完毕,都会恢复运行。
  6. TERMINATED
    线程执行完毕,执行完run方法正常返回,或者抛出了运行时异常而结束,线程都会停留在这个状态。这个时候线程只剩下Thread对象了,没有什么用了。

Q:线程的 BLOCKEDWAITING 状态的区别?

  • 阻塞态:是指当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。在Java中,阻塞态专指请求锁失败时进入的状态。由一个**同步队列(阻塞队列)**存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。
  • 等待态:当前线程中调用wait、join、park函数时,当前线程就会进入等待态,只能等待其他线程的指示才能继续运行。进入等待态的线程会释放CPU执行权,并释放资源(比如锁)。也有一个等待队列存放所有等待态的线程。
  • 当前线程调用object.wait方法后,释放对象锁,这个状态就是WAITING状态,线程处于等待队列,等待其他线程同一个对象调用notify或者notifyAll方法。
    在调用notify或者notifyAll方法后,调用wait的等待线程不会立刻从等待队列返回,而是从等待队列移动到同步队列,准备竞争对象监视器的这种状态就是BLOCKED。

你可能感兴趣的:(Java,java,jvm)