banner
NEWS LETTER

Scroll down

JUC

ThreadLocal什么时候会出现内存泄漏?

如果仅仅使用手动创建线程的方式,ThreadLocal会随着线程被回收而被回收,但如果使用的是线程池,ThreadLocal中存放的数据就不会被释放了,所以需要调用remove方法手动释放

如何中断线程?

Java的中断是一种协作机制,也就是通过中断并不能直接中断另外一个线程,而需要被中断的线程自己处理中断

  • volatile标志位:通过设置标志位,使线程正常退出,能够确保线程根据业务逻辑安全停止
  • Thread.stop()方法:已废弃,不推荐使用,会导致线程安全问题和资源泄漏
  • Thread.interrupt()方法:推荐使用,通过设置中断标志来提示线程停止,能够优雅地停止线程

1. 使用volatile标志位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class WorkerThread extends Thread {
private volatile boolean running = true;

@Override
public void run() {
int i = 1;
while (running) {
System.out.println("线程运行中:" + i++);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}

System.out.println("线程已优雅退出");
}

public void cancel() {
running = false;
}
public static void main(String[] args) {
WorkerThread ti = new WorkerThread();
ti.start();
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ti.cancel();
}
}

2. interrupt()协作式中断

在Java的中断模型中,每个线程有一个中断标识,该中断请求可以来自所有线程,包括被中断的线程本身。当线程t1想要中断线程t2时,将t2的中断标识设置为true即可,线程t2可以选择在合适的时候处理该中断请求,或者不理会。

![image-20250326133246307](/Users/effy/Library/Application Support/typora-user-images/image-20250326133246307.png)

主要有三个方法:

  • interrupt()是一个动词,表示中断线程。

  • Interrupted是一个形容词,用于检查线程的中断位,并且重置中断位为false

  • 而isInterrupted()方法只是简单的检查,interrupted()处理的更加复杂。

方法名 介绍 代码
void interrupt() 中断线程,设置线程的中断位为true ![image-20250326134541129](/Users/effy/Library/Application Support/typora-user-images/image-20250326134541129.png)
boolean isInterrupted() 检查线程的中断标记位,true-中断状态, false-非中断状态 ![image-20250326133834933](/Users/effy/Library/Application Support/typora-user-images/image-20250326133834933.png)
static boolean interrupted() 静态方法,返回当前线程的中断标记位,同时清除中断标记,改为false。比如当前线程已中断,调用interrupted(),返回true, 同时将当前线程的中断标记位改为false, 再次调用interrupted(),会发现返回false ![image-20250326135035649](/Users/effy/Library/Application Support/typora-user-images/image-20250326135035649.png)

以下是线程处于sleep状态时被中断的处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ThreadInterrupt extends Thread{
@Override
public void run() {
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程已运行:" + i++);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
System.out.println("线程已中断");
// 当线程处于wait、sleep、join等状态被中断时,中断标志会被清除为false,并抛出InterruptException异常
// 所以需要再次设置中断标志,否则线程将不会退出循环
Thread.currentThread().interrupt();
}
}

System.out.println("线程已优雅停止");
}

public static void main(String[] args) throws InterruptedException {
ThreadInterrupt test = new ThreadInterrupt();
test.start();

Thread.sleep(5000L);

test.interrupt();
System.out.println("主线程请求线程中断");
}
}
  • 中断sleep、wait和join等方法时,这些方法会响应中断,抛出InterruptedException,同时清除中断标记位为false
  • 中断LockSupport.park()时,阻塞方法park响应中断, 不会抛出异常,同时不会清除中断标记位,还是为true
  • 中断正常运行的线程, 不会清空中断状态,同时线程结束后,重置中断状态位

https://mp.weixin.qq.com/s/pWbFDI3h5BJTttu6aTch5g

https://juejin.cn/post/7054934081733132301#heading-3

JVM

1. 内存泄漏排查

背诵版:主要可以分为发现问题和诊断原因两个步骤吧,前期在服务运行过程中,可以使用Prometheus、VisualVM或者Arthas这种工具来监控运行过程内存异常情况,比如Full GC后内存占用没有明显下降,并且内存占用在持续走高,说明服务中可能出现了内存泄漏。然后可以使用jmap来生成快照进行分析,也可以事先配置好JVM参数-XX:HeapDumpOnOutOfMemoryError并设置-XX:HeapDumpPath文件保存路径,在服务发生OOM时自动保存堆快照文件。然后把hprof文件导入到MAT中进行分析,根据MAT列出的可疑对象,通过线程Thread的methodHandler去定位OOM时执行的代码,找到深堆较大的对象,然后再去对应的业务代码中分析这些对象创建使用存在的内存泄漏问题,最后修复代码并验证测试。

  • 生存内存快照分析(离线)
    • 优点:通过完整的内存快照准确判断问题产生的原因
    • 缺点:内存较大时,生成内存快照较慢,这个过程会影响用户的使用;通过MAT分析内存快照时,需要至少准备1.5-2倍的内存空间
  • 在线定位问题
    • 优点:不需要生成内存快照,对用户影响较小
    • 缺点:不能看到详细的内存信息;需要通过arthas或btrace工具
    • 步骤
      1. 使用jmap -histo:live 进程ID > 文件名,可以生成内存中存活的对象
      2. 分析内存占用较多的对象,一般就是造成内存泄漏的原因
      3. 使用arthas的stack命令可以追踪对象创建的方法的被调用路径,追踪对象创建的根源

1. 发现问题

通过监控工具尽可能早的发现内存异常情况,如果触发Full GC但是只能回收很少的内存时,并且内存占用在持续走高,就说明程序出现了内存泄漏

  1. top:首先可以使用top命令对整体内存使用率进行初步排查,判断是哪个进程占用了较多的内存。比如top -n 5,指定只查看占用率前五的进程,top -o mem以内存降序排序

    ![image-20250313132207330](/Users/effy/Library/Application Support/typora-user-images/image-20250313132207330.png)

  2. VisualVM:可视化实时监控CPU、内存空间。本地连接或者远程连接

    ![image-20250313152344377](/Users/effy/Library/Application Support/typora-user-images/image-20250313152344377.png)

  3. 普罗米修斯

定位到溢出对象产生的根源,问题主要来说分为两类:

  1. 代码缺陷:比如对数据库连接没有自动释放,线程池中使用ThreadLocal现有手动remove
  2. 高并发请求:并发请求量大,请求数据多,方法执行慢。可以使用jemter测试接口,发现问题

2. 诊断原因

  • 在JVM启动时,添加-XX:HeapDumpOnOutOfMemoryError参数,表示在发生OOM的时候自动保存hprof内存快照文件
  • 使用MAT打开hprof文件,并进行内存泄漏检测,分析给出的怀疑对象,以及看对应的调用栈,MAT通过支配树和深堆浅堆寻找可疑泄漏对象

3. 修复问题

4. 测试验证

2. GC调优

背诵版:GC调优主要关注三个核心指标,垃圾回收吞吐量、延迟和内存使用量,要尽可能降低垃圾回收时间。主要步骤可以分为发现问题和诊断原因,前期要尽可能早的发现GC问题,比如可以通过jstat指令去初步判断GC是否存在问题,当确认出现问题还可以通过Prometheus、Grafana工具去监控更具体的信息,比如GC次数、GC触发时间、GC停顿时间等数据。也可以通过在jvm启动时添加-XX+PrintGCDetails参数打印详细的GC信息并保存到日志中,有了GC日志可以用GCeasy这样的日志分析工具去进行分析,他会给出比较详细的GC报告,比如吞吐量、延迟和GC信息,前期通过这些工具尽可能早的发现GC时间过长、频率过高的现象。

三个核心指标

  • 垃圾回收吞吐量:CPU用户执行用户代码的时间和总执行时间的比值,尽可能降低垃圾回收STW的时间
  • 延迟:用户发起请求到收到响应经历的时间
  • 内存使用量:在保证前两个指标的情况下,尽可能降低内存使用量

步骤:

  1. 发现问题:通过监控工具尽可能早的发现GC时间过长或者频率过高的现象
    • jstat -gc 进程ID 每次统计的间隔(毫秒)统计次数:操作简单,jdk自带,但不够精确,只能判断GC是否存在问题
    • ![image-20250314134302466](/Users/effy/Library/Application Support/typora-user-images/image-20250314134302466.png)
    • 使用VisualGC、Prometheus、Grafana等监控工具,可以具体看到GC次数、GC触发时间、GC停顿时间等数据
    • GC日志:使用-XX:+PrintGCDetails打印详细GC信息,并使用-Xloggc:文件名保存到文件中,使用GCeasy进行分析
  2. 解决问题
    1. 优化基础JVM参数
      1. 把-Xmx和-Xms设置为一样大,可以减少堆内存的扩容,扩容也是会有一定性能开销的。初始堆太小的话,java启动比较慢
      2. -XX:MaxMetaSpaceSize=值设置元空间最大值,避免元空间内存泄漏不停向操作系统申请内存。-XX:MetaSpaceSize=值,当元空间大小达到这个值时,触发一次Full GC,可以不设置
      3. -Xss256k,不同操作系统默认值不一样,如果用不到那么大可以设置小一点
      4. 不建议手动设置:-Xmn(新生代空间)、-XX:SurvivorRatio(Eden区和S0比值,默认为8)、-XX:MaxTenuringThresholod(最大晋升年龄)
      5. ![image-20250314151235693](/Users/effy/Library/Application Support/typora-user-images/image-20250314151235693.png)
    2. 减少对象产生:根据业务代码具体修改
    3. 更换垃圾回收器:jdk8默认ParallelScavenge+ ParallelOld、ParNew + CMS、G1
    4. 优化垃圾回收器参数

3. 慢接口如何排查

背诵版:主要的思路是生成GC报告,通过GCeasy等工具进行分析,判断是不是存在GC问题或者内存问题;如果是GC问题的话,比如频繁的Full GC,就需要去调整对应的JVM参数,如果存在内存问题,比如内存占用持续走高,并且FullGC后没有明显的下降,可以通过jmap或者arthas把堆内存快照保存下来;通过MAT等工具去分析内存问题产生的原因;最终定位到问题并修复,测试验证。如果是业务代码本身执行的慢,可以通过生成火焰图来判断具体是什么方法执行占比时间高,从而去进行优化。arthas的trace命令、watch命令

4. CPU占用高如何排查

  1. 使用top |grep ‘java’命令查看CPU占用较高的进程,判断是否是java进程
  2. 使用top -p 进程ID + H:查看该进程下所有线程的CPU占用率
  3. 使用jstack 进程ID > 文件名:把线程的栈信息保存下来
  4. 查看栈信息文件,找到对应线程在执行什么方法

网络

1. TCP当前有大量连接处于close_wait状态可能是什么原因

  1. 代码逻辑缺陷:被动关闭的一方收到FIN请求后,没有调用close()方法主动发送自己的FIN请求,比如没有在finally中关闭连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 错误示例:未在 finally 中关闭连接
    try {
    Socket socket = serverSocket.accept();
    // 处理数据...
    } catch (Exception e) {
    e.printStackTrace();
    }
    // 正确示例:确保关闭连接
    finally {
    if (socket != null) socket.close();
    }
  2. 资源泄漏:比如使用了数据库连接池、HTTP连接池,连接没有正确释放回池中

  3. 线程阻塞或死锁:业务逻辑中有死锁或长时间阻塞操作,导致无法执行到关闭连接的代码

  4. 高并发场景下的积压:服务端性能不足,处理速度跟不上客户端关闭连接的请求,导致未关闭的连接堆积

排查:

  1. 查看处于CLOSE_WAIT状态的数量

  2. netstat -ant | grep CLOSE_WAIT | wc -l
    

2. TCP四次挥手为什么要有time_wait状态

Time_wait状态是主动关闭连接的一方在接到被动关闭连接的一方的FIN请求后,发送自己的ACK响应,再等待两倍最大报文存活时间:

  1. 确保最后一个ACK可靠到达:若主动关闭方发送的最后一个ACK丢失,被动关闭方会重传FIN报文。TIME_WAIT状态的主动关闭方可以重新发送ACK,避免连接处于“半关闭”状态。
  2. 防止旧连接的残留报文干扰新连接:如果相同的四元组(源IP、源端口、目标IP、目标端口)被快速复用,旧连接的延迟报文可能被错误地传递给新连接,导致数据混乱。

带来的问题

Time_Wait的时间里,主动关闭连接的一方的端口一直被占用,造成连接不能在短时间内再次被建立。

解决方法:

  1. 可以缩减time_wait等待的时间
  2. so_reuseaddr重用处于time_wait状态的连接
  3. 使用长连接,减少关闭次数

https://www.cnblogs.com/lilpig/p/16490208.html#time_wait%E4%BC%9A%E5%B8%A6%E6%9D%A5%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98

框架

如何实现定时任务?

1. Spring Task

2. ScheduledExecutorService

3. Quartz框架

MySQL

MySQL最大可连接数?

旧版本最大是16384,默认为100,可以配置max_connections

现在最大是100000,默认为151

MySQL最大支持的qps?

单库单表大概是在2000左右

Redis

Redis集群的三种方式

  • 主从模式
  • 哨兵模式
  • 集群分片模式
Other Articles
cover
  • 25/03/25
  • 10:57
cover
code
  • 25/02/19
  • 15:02