JVM Thread Stack线程栈分析

上一章:

JVM-简介&垃圾回收&内存泄漏分析_jmap dump会触发gc吗_做测试的喵酱的博客-CSDN博客

相关文章:

监视器锁(Monitor Lock)_做测试的喵酱的博客-CSDN博客

 synchronized&锁_做测试的喵酱的博客-CSDN博客

java多线程&线程与进程区别_做测试的喵酱的博客-CSDN博客

一、哪些问题需要分析堆

场景一:跟内存相关的问题,需要分析堆。如:

  1. 内存泄漏(Memory Leaks):堆内存泄漏是指在程序中存在无法释放的对象,导致堆内存不断增长。通过分析堆内存可以了解到哪些对象占用较多内存、是否有持续增长的对象等,以找出内存泄漏的根本原因并进行修复。

  2. 内存溢出(Out of Memory):堆内存溢出是指应用程序在申请内存时,堆空间已满且没有足够的内存供分配。通过分析堆内存可以查看堆的使用情况、对象分配情况,从而定位引发内存溢出的原因。

场景二:tps/rt 随着压测时间的波峰波谷

压测时,响应时间、tps等曲线出现波峰波谷的现象,可能就是GC导致的,在GC时,进程是不能工作的,导致响应时间变长、tps变低,等GC完成后,进程又继续工作,响应时间变短,tps变大。再次gc时,tps又开始变低,响应时间变长。。。

扩展:tps与并发图

A图就是一个正常的,随着并发增加,tps增加,到一定程度后,tps趋于稳定

b图形成原因:

随着并发增加,用户线程/进程太多,每个线程拿到cpu时间片的间隔增大,响应时间增加,会导致进程/线程的被动中断,产生大量的被动中断,导致系统cpu增加,则用户态可使用cpu占比少。

二、 线程 thread

2.1 线程的基本概念

线程是操作系统提供的执行单元,它具有自己的线程标识、上下文环境、优先级等属性。线程可以独立执行,与其他线程并发运行。

扩展:

线程与进程的区别

java多线程&线程与进程区别_做测试的喵酱的博客-CSDN博客

2.2  Java线程6种状态

1、新建状态(New):当一个线程对象被创建但尚未调用start()方法时,线程处于新建状态。这个状态我们使用jstack进行线程栈dump的时候基本看不到,此时,线程已经分配了内存空间,但尚未启动执行。

2、就绪状态(Runnable):

 新建线程对象后,调用该线程的 start()方法就可以启动线程。当线程启动时,线程进入就绪状态(runnable)。此时,线程可以被CPU调度,并开始执行其任务。

由于还没有分配CPU,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。

当系统挑选一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态。系统挑选的动作称之为“CPU调度"。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

 从虚拟机的角度看,线程正在运行状态,状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等。

处于RUNNABLE状态的线程是不是一定会消耗cpu呢,不一定,像socket IO操作,线程正在从网络上读取数据,尽管线程状态RUNNABLE,但实际上网络io,线程绝大多数时间是被挂起的,只有当数据到达后,线程才会被唤起,挂起发生在本地代码(native)中,虚拟机根本不一致,不像显式的调用sleep和wait方法,虚拟机才能知道线程的真正状态,但在本地代码中的挂起,虚拟机无法知道真正的线程状态,因此一概显示为RUNNABLE。

注意:

Runnable 状态,包含running状态(当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run()方法。)

我们通过RUNNABLE状态,是不能区分出当前的线程正在执行或者等待cpu的调度。就绪状态与运行中状态,统称为RUNNABLE状态/就绪状态。

3、阻塞状态(Blocked):

线程处于阻塞状态,正在等待一个monitor lock(监视器锁)。通常情况下,是因为本线程与其他线程公用了一个锁。其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法,而本线程进入这个同步代码块也需要这个锁,最终导致本线程处于阻塞状态。

扩展:

a、监视器锁的解释

监视器锁(Monitor Lock)_做测试的喵酱的博客-CSDN博客

生活中的小例子:

去银行窗口办理业务,(多个人代表多个线程)一个窗口只能同时处理一个人的业务(线程拿到锁),其他的人(线程)就只能等待锁的释放,处于Blocked状态。

4、等待状态(Waiting):

当线程等待某个特定条件的发生时,它可以进入等待状态。在等待状态下,线程会暂时停止执行,直到接收到其他线程的通知。

这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll一遍该线程可以继续下一步操作。

 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在临界点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束,处于waiting状态的线程基本不消耗CPU。

举个例子:

银行窗口排队,你拿到了锁,正在办理你的业务。这时突然来了一个V V IP客户,插队了,打断了你的业务办理,先办理他的业务。你进入了WAITING状态。等VIP走后,才继续办理你的业务。

5、计时等待状态(Timed Waiting):

与等待状态类似,但是可以通过设置等待的时间来限制等待的时间段。线程会在指定的时间段内等待,如果超时仍未收到通知,线程将重新进入运行状态。

举个例子:

你是线程T1,你朋友是线程T2,你朋友跟你说11:00的时候,过来跟你借车(锁资源)。

结果你在10:50的时候,把车停下来(释放锁)等待你朋友过来找你。结果到了11:00 你朋友爽约了,他没来。 然后你就继续把车(锁)开走了。

调用了以下方法的线程会进入TIMED_WAITING:

  • 1、Thread#sleep()
  • 2、Object#wait() 并加了超时参数
  • 3、Thread#join() 并加了超时参数
  • 3、LockSupport#parkNanos()
  • 4、LockSupport#parkUntil()

TIMED_WAITING (parking)实例如下:

 从图中可以看出

  • “TIMED_WAITING (parking)”中的 timed_waiting 指等待状态,但这里指定了时间,到达指定的时间后自动退出等待状态;parking指线程处于挂起中。
  • “waiting on condition”需要与堆栈中的“parking to wait for  <0x00000000acd84de8> (a java.util.concurrent.SynchronousQueue$TransferStack)”结合来看。
  • 首先,本线程肯定是在等待某个条件的发生,来把自己唤醒。
  • 其次,SynchronousQueue 并不是一个队列,只是线程之间移交信息的机制,当我们把一个元素放入到 SynchronousQueue 中时必须有另一个线程正在等待接受移交的任务,因此这就是本线程在等待的条件。

6、终止状态(Terminated):

当线程的任务执行完毕或者发生了意外终止时,线程进入终止状态。一旦线程进入终止状态,它将不会再转换到其他状态。

三、线程栈thread stack

3.1 线程栈(Thread Stack)概念

Thread Stack :线程栈

Stack: 堆栈

线程栈(Thread Stack)简称为堆栈(Stack)

线程与线程栈:

线程栈是线程的一部分,用于存储线程执行过程中的方法调用和局部变量等信息。

线程是操作系统提供的执行单元,它具有自己的线程标识、上下文环境、优先级等属性。线程可以独立执行,与其他线程并发运行。

而线程栈是线程在内存中的一块区域,用于保存线程方法的调用信息。每个线程都有自己的线程栈,用来管理方法的调用顺序和局部变量等数据。

线程栈是线程的私有空间,在多线程程序中保证了线程之间的隔离和独立性。

在 Java 中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有 所不同,因此需要一个独立的线程栈。

每个线程栈,包含多个栈帧,每个栈帧都有局部变量、动态连接、操作栈、方法返回地址。

线程栈:

每个线程栈,包含多个栈帧,每个栈帧都有局部变量、动态连接、操作栈、方法返回地址。

线程栈(Thread Stack)是每个线程专有的内存区域,用于存储方法调用、局部变量和方法执行期间的状态信息。每当一个新线程被创建时,Java虚拟机(JVM)会为该线程分配一块独立的内存空间,即线程栈。

线程栈以栈(Stack)的数据结构组织数据,采用后进先出(LIFO)的方式。每个栈帧(Stack Frame)代表了一个方法的调用,包含了该方法的参数、局部变量以及方法的返回地址等信息。当方法被调用时,JVM会在线程栈上创建一个新的栈帧,并将其推入栈顶;而方法执行完毕后,栈帧会从栈顶弹出。

线程栈对于线程的执行非常重要。它保存了每个方法的执行上下文,包括方法参数、局部变量和返回值等。线程栈还记录了方法的调用关系,使得程序能够正确地返回到调用点。

线程栈,是虚拟机中线程(包括锁)状态的一个瞬间状态的快照,包括线程的调用堆栈,锁的持有情况。

打印线程栈的信息,一般包含:

1、线程名字,id,线程的数量等。

2、线程的运行状态,锁的状态(锁被哪个线程持有,哪个线程在等待锁等)

3、调用堆栈(即函数的调用层次关系)调用堆栈包含完整的类名,所执行的方法,源代码的行数。

3.2  哪些问题需要分析线程栈:

因为线程栈是瞬时快照包含线程状态以及调用关系,所以借助堆栈信息可以帮助分析很多问题,比如线程死锁,锁争用,死循环,识别耗时操作等等。线程栈是瞬时记录,所以没有历史消息的回溯,一般我们都需要结合程序的日志进行跟踪

1、系统无缘无故的cpu过高

2、系统挂起,无响应

3、系统运行越来越慢

4、性能瓶颈(如无法充分利用cpu等)

5、线程死锁,死循环等

6、由于线程数量太多导致的内存溢出(如无法创建线程等)

四、jstack命令

java是多线程,一个进程下,包含多个线程。

Java虚拟机提供了线程转储(thread dump)的后门,通过这个后门可以把线程堆栈打印出来。通常我们将堆栈信息重定向到一个文件中,便于我们分析,由于信息量太大,很可能超出控制台缓冲区的最大行数限制造成信息丢失。

jstack是jdk自带的打印线程栈的工具,jstack用于打印出给定的Java进程ID或core file或远程调试服务的Java堆栈信息。

jstack命令用于查看Java进程中的线程信息和堆栈跟踪。实际上,jstack主要是用来查看Java进程中各个线程的状态、堆栈信息、锁信息等。

使用jstack命令时,它会连接到指定的Java进程,并收集每个线程的信息。这些信息包括每个线程的ID、名称、优先级、状态以及线程调用栈的信息。通过分析线程的堆栈信息,您可以了解到线程在执行过程中经过的方法调用路径,以及当前线程被阻塞的原因。

因此,尽管jstack命令是在查看Java进程中线程的信息,但实际上它是通过操作系统提供的接口获取线程信息,并将其与Java进程进行关联。通过这种方式,您可以查看到Java进程中各个线程的详细信息。

jstack 打印线程栈,线程栈是一个瞬间状态的快照,程序一直在运行,所以每个时刻的瞬间状态的快照是变化的。我们使用jstack查看的信息,也是线程栈那一瞬间的数据。

所以分析的时候,我们需要多次使用jsatck命令,这样才能看到一个大概的连续过程。

4.1 jstack 打印线程栈

[root@ecs-39233 ~]# jstack
Usage:
    jstack [-l][-e] <pid>
        (to connect to running process)

Options:
    -l  long listing. Prints additional information about locks
    -e  extended listing. Prints additional information about threads
    -? -h --help -help to print this help message
You have new mail in /var/spool/mail/root

常用命令:

#后面直接跟pid,查看正在运行的进程,基本上用的这个命令

jstack pid >文件

pid为进程号

举例: 

查找java/tomcat进程的pid

 ps -ef|grep tomcat

收集栈数据, 

jstack 8766

一般数据会比较多,我们可以把这个数据收集到一个文件里。 

jstack 8766 >stack.log

stack.log里面就是线程栈。实际运行中,往往一次dump的信息,还不足以确认问题,建议产生三次dump信息,如果每次dump都指向同一个问题,我们才确定问题的典型性。

4.2 分析线程栈数据

4.2.1 举例

java程序

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            while (true) {
                System.out.println("Thread 1 is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        
        Thread thread2 = new Thread(() -> {
            while (true) {
                System.out.println("Thread 2 is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread2.start();

        Thread.sleep(5000);
        thread2.join();
    }
}

该程序创建了两个线程,分别打印 "Thread 1 is running" 和 "Thread 2 is running" 的消息。主线程等待 5 秒后,会等待 thread2 的执行完毕。

现在,我们可以使用 jstack 命令来获取线程栈信息。首先,我们运行程序并记下主线程的 ID(PID)。

然后,在终端中执行以下命令(假设 PID 为 12345):

jstack 12345 > stack.txt

这将把线程栈信息保存到名为 stack.txt 的文件中。

接下来,我们打开 stack.txt 文件,可以看到类似以下的线程栈信息:

"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001bba2800 nid=0x344 waiting on condition [0x000000001c35f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at Main.lambda$main$0(Main.java:8)
        ...(省略部分信息)...
        
"Thread-2" #13 prio=5 os_prio=0 tid=0x000000001bba3800 nid=0x302 waiting on condition [0x000000001c45f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at Main.lambda$main$1(Main.java:19)
        ...(省略部分信息)...

"main" #1 prio=5 os_prio=0 tid=0x000000000241b800 nid=0x3bb0 waiting on condition [0x000000000249e000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at Main.main(Main.java:29)
        ...(省略部分信息)...

解析线程栈信息时,我们关注以下几点:

  • 线程名称:在引号内的字符串,例如 "Thread-1"、"Thread-2" 和 "main"。
  • 线程状态:位于 java.lang.Thread.State 行后面的状态信息,如 "TIMED_WAITING (sleeping)"。
  • 方法调用路径:从 "at" 开始,指示方法调用路径。例如,"at Main.lambda$main$0(Main.java:8)" 表示在 Main 类的第8行的 lambda 表达式中调用。

通过分析线程栈信息,我们可以了解各个线程的状态和当前所处的方法调用路径。这对于定位死锁、长时间阻塞、性能问题等非常有帮助,并可以指导后续的问题排查与优化工作。

4.2.2 编写示例程序

java脚本模拟服务器内存溢出&服务器部署java项目_java模拟内存溢出_做测试的喵酱的博客-CSDN博客

1、方案

1、打包成war包,可以直接将war包部署在tomcat容器里

2、spring boot,打包成jar包。打的jar包,内置了tomcat,所以在服务器上,直接启jar包就行,没有必要放在tomcat容器里部署,在启动jar包时,可以配置线程池等。

这里用 spring boot,打包成jar。

2、新建 项目

java 选择8

勾选spring web

修改pom.xml文件,把Spring boot 版本,改成2.4.4

(啊,为啥用2.4.4 呢,因为我用习惯了) 

3 编写项目

package com.example.analyzestack.controller;


import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyTest {

    Object obj1 = new Object();
    Object obj2 = new Object();

    public void fun1() {
        synchronized (obj1) {
            fun2();
        }
    }
    public void fun2() {
        synchronized (obj2) {
            while (true) { //为了打印堆栈,该函数堆栈分析不退出
                System.out.print(">>>>>>>>>>>");
            }
        }
    }

    @RequestMapping("/OOM")
    public String ping(){
        MyTest aa = new MyTest();
        aa.fun1();
        return "stack分析";
    }

}

配置端口号,8084

4 打jar包

(如果之前,大过,要先cleam清理一下)

jar包在target目录下

5 部署服务在服务器启动该jar包

1、先将jar包上传到服务器。

rz

2、启动jar包

nohup java -jar service.jar > server.log 2>&1 &

格式:

nohup java -jar -Dspring.profiles.active=prod  包名.jar --server.port=80 &

如:

nohup java -jar -Dspring.profiles.active=prod testOOM-0.0.1-SNAPSHOT.jar &

我们这里指定这个jvm的堆内存,最大最小内存都是5m,这样容易压到内存溢出。

nohup java -jar -Dspring.profiles.active=prod -Xms256m -Xmx256m testOOM-0.0.1-SNAPSHOT.jar &

使用浏览器访问:

http://124.70.87.136:8083/OOM

部署项目:

nohup java -jar -Dspring.profiles.active=prod -Xms256m -Xmx256m AnalyzeStack-0.0.1-SNAPSHOT.jar &

 14044 就是进程号

使用top命令,查看服务器状态

 cpu us 被打满了。14044 是我们刚刚启动的java进程,消耗了少量cpu

kswapd0  消耗了大量的cpu

kswapd0 是一个Linux操作系统中的内核线程。它是负责虚拟内存交换(swap)子系统的一部分。当系统的内存不足以容纳所有正在执行的进程时,kswapd0 将被激活来管理交换空间。

kswapd0 的主要任务是将不再活跃或很少使用的内存页面移出物理内存,并将其写入交换空间(通常是硬盘上的交换文件)。这样可以释放物理内存以供其他活跃的进程使用。

kswapd0 在后台运行,并且具有自动管理交换空间的功能。它监视系统的内存使用情况,并在需要时调整交换内存的使用,以确保系统的正常运行。通过这种方式,kswapd0 帮助操作系统维持合理的内存使用和系统性能。

请注意,kswapd0 是一个内核线程,它在内核级别运行,并不直接对用户可见。在一般情况下,您不需要主动干预或操作 kswapd0 程序。

4.2.2 分析线程栈 

使用jmeter压测接口

jstack 14044 >stack.log

上一章节:

JVM-简介&垃圾回收&内存泄漏分析_傲娇的喵酱的博客-CSDN博客

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

做测试的喵酱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值