news 2026/9/20 9:56:10

进程与线程的本质区别:从Linux内核到JVM的全链路解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
进程与线程的本质区别:从Linux内核到JVM的全链路解析

1. 为什么“进程和线程的区别”这个问题,十年来每次面试都还在问?

你打开任何一份Java、Python或C++的初级到中级岗位JD,几乎必然看到“熟悉多线程编程”“理解JVM内存模型”“能分析高CPU/内存占用问题”这几条。不是HR在凑字数,而是真实生产环境里,90%以上的性能卡顿、服务假死、资源泄漏、偶发超时,根源都藏在这两个最基础却最容易被轻视的概念底下——进程线程

我带过三届校招新人,第一周必做一件事:让他们用top -H(Linux)或任务管理器(Windows)打开一个正在跑Spring Boot应用的机器,找一找那个叫java的进程下面到底挂了多少个名字像http-nio-8080-exec-1AsyncResolver-bootstrap-1Reference Handler的“小尾巴”。很多人盯着屏幕愣住:“这……算一个程序?还是好几个?”——这就是问题的起点。他们写的代码里调了new Thread(),加了@Async,配了ThreadPoolTaskExecutor,但没人告诉他们:你启动的不是“线程”,而是一组共享地址空间的执行单元;你杀掉的不是“进程”,而是整个隔离沙盒的生命周期

更现实的场景是:某天凌晨三点告警,线上服务响应延迟飙升到2秒,监控显示CPU打满但QPS没涨,jstack一 dump,满屏WAITING on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject;或者运维同事甩来一张截图:“这个sangforpwex.exe占着30% CPU不放,杀不掉,重启也不行”;又或者你用colcon build编译ROS2项目,明明开了-j8,但实际只跑了3个编译线程,htop里看着CPU利用率上不去……这些问题背后,没有一个是靠背“进程是资源分配单位、线程是调度单位”这种教科书定义就能解决的。

真正卡住人的,从来不是概念本身,而是概念在真实系统中的映射关系

  • 当你在Java里写Thread t = new Thread(() -> { System.out.println("hello"); }); t.start();,操作系统层面到底发生了什么?
  • JVM里的java.lang.Thread对象,和Linux内核里的task_struct结构体,是怎么一一对应的?
  • 为什么wechatappex.exe会开几十个线程,而你的Spring Boot应用默认只开10个http-nio线程?
  • mate-indicators进程能不能关?关了桌面图标没了算bug还是设计?
  • u盘无法弹出提示“请先结束占用进程”,你lsof +D /media/xxx查出来的到底是进程ID还是线程ID?

这篇文章不讲PPT式定义,不列对比表格糊弄人。我会带你从一个Java Web服务启动开始,一层层剥开:用户态代码 → JVM运行时 → 操作系统内核 → 硬件CPU调度器,看“进程”和“线程”这两个词,在每一层究竟长什么样、干了什么事、踩了哪些坑。所有结论都来自我亲手调试过的27个生产事故现场,包括一次因hashmap非线程安全导致支付订单重复扣款的线上故障,和一次因qt曲线刷新放在GUI线程引发界面冻结3分钟被客户投诉的嵌入式项目。你不需要记住所有术语,但读完后,再看到jstack输出里的"main" #1 prio=5 os_prio=0 tid=0x00007f8b4c00a000 nid=0x1e6a runnable [0x00007f8b54dfe000],你能立刻说出nid=0x1e6a是线程ID还是进程ID,os_prio=0意味着什么,runnable状态是否真在跑,以及为什么它卡在[0x00007f8b54dfe000]这个栈地址上——这才是“区别”的终极意义:不是用来答题的,是用来定位问题的

2. 进程与线程的本质:从硬件寄存器到JVM堆内存的全链路拆解

2.1 操作系统视角:进程是“租下整栋楼”,线程是“楼里分租的室友”

先抛开Java、Python这些语言层抽象,回到Linux内核最原始的视角。当你在终端敲下java -jar app.jar,内核做的第一件事,不是加载.class文件,而是创建一个进程控制块(PCB),在内核内存里分配一块固定大小的结构体——task_struct。这个结构体有多大?在Linux 5.10 x86_64上,实测是8192字节(8KB)。它存了什么?不是代码,不是数据,而是描述“这个程序该怎么活”的全部元信息

  • mm_struct *mm:指向该进程的内存管理结构,记录它拥有哪些虚拟内存段(代码段、堆、栈、共享库)、页表基址(CR3寄存器值)、是否允许写时复制(COW);
  • files_struct *files:打开的文件描述符表,/proc/<pid>/fd/目录下的每个数字链接,都对应这里的一个struct file *指针;
  • signal_struct *signal:信号处理函数表,kill -9 <pid>之所以能强制终止,是因为内核直接修改了这里的sigpending位图;
  • struct list_head thread_group:关键!这是所有属于该进程的线程组成的双向链表头,进程本质就是线程组的容器

提示:ps -eLf命令输出的LWP(Light Weight Process)列,显示的就是线程ID(TID),而PID列是线程组ID(TGID)。当LWP == PID时,这个线程就是主线程,也是进程的“法定代表”。

那么线程呢?在Linux里,线程就是共享同一task_structmmfilessignal等字段的多个独立调度实体。内核用clone()系统调用创建线程时,传入CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND标志,意思是:“给我一个新的task_struct,但把内存、文件、信号这些‘家当’都跟父进程共用”。所以,一个Java进程启动后,jps -l看到的23456 com.example.App,对应内核里一个task_struct;而jstack 23456里列出的20个线程,对应内核里20个task_struct,但其中19个的mm字段和主线程完全一致——它们住在同一栋楼(虚拟地址空间),共用同一个厨房(文件句柄)、同一个信箱(信号队列),只是各自有独立的卧室(栈空间)和身份证(线程ID)。

这就解释了为什么u盘无法弹出lsof +D /media/usb返回的PID,其实是某个进程的TGID,而真正持有USB设备文件句柄的,可能是它下面某个工作线程(比如rsync的IO线程)。你kill -9 <PID>只能杀死主线程,但其他线程可能还拿着句柄不放,直到整个线程组退出才释放。

2.2 JVM视角:Java线程是OS线程的“代理壳”,不是凭空变出来的

很多Java程序员误以为new Thread()是在JVM里“造”了一个线程。真相残酷:JVM不做线程调度,它只是OS线程的包装器和协调员。当你调用Thread.start(),HotSpot JVM底层调用的是pthread_create()(POSIX线程库),最终触发Linux的clone()系统调用。我们用strace抓一下:

strace -e trace=clone,execve,mmap -p $(pgrep -f "java.*app.jar") 2>&1 | grep clone # 输出类似: clone(child_stack=NULL, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f8b4c00a000, tls=0x7f8b4c00a700, child_tidptr=0x7f8b4c00a9d0) = 23457

注意flags里的CLONE_THREAD——这是Linux线程模型的关键标志。它让新创建的task_struct加入原进程的线程组(thread_group链表),并让/proc/<pid>/status里的Threads:字段+1。而JVM做的,只是:

  1. 在Java堆里创建java.lang.Thread对象,存入ThreadGroup引用;
  2. 调用pthread_create(),传入JVM封装的start_thread函数指针作为入口;
  3. 将OS线程ID(TID)存入Thread对象的tid字段(通过getThreadId()可获取);
  4. 维护一个java.lang.Thread.State枚举,但这只是JVM对OS线程状态的快照映射,不是真实状态。

注意:Thread.getState()返回的RUNNABLE,不等于OS的TASK_RUNNING。JVM可能把处于pthread_mutex_lock()等待中的线程标记为RUNNABLE,因为从Java语义看,它“随时可以被调度”,但OS内核里它其实在TASK_INTERRUPTIBLE状态睡觉。这就是为什么jstack里看到一堆RUNNABLE线程,top里CPU却不高——它们全卡在锁上。

再看JVM内存模型。jmap -heap <pid>输出的Heap Usage,是所有线程共享的堆内存;而每个线程的java.lang.Thread对象本身,连同它的Java栈帧,都存在各自的线程私有栈里。这个栈空间由-Xss参数控制(默认1MB),但注意:-Xss1m不是给每个Java线程分配1MB物理内存,而是分配1MB虚拟地址空间,实际物理页按需分配。这也是为什么开1000个线程,free -h看不到内存暴涨——虚拟内存没动,物理内存只在真正压栈时才增长。

2.3 硬件视角:CPU核心不认“进程”或“线程”,只认“上下文切换”

最后落到CPU层面。现代x86-64处理器有几百个寄存器,但调度器真正关心的只有几个关键上下文:

  • 指令指针(RIP):下一条要执行的指令地址;
  • 栈指针(RSP):当前栈顶位置;
  • 通用寄存器(RAX, RBX...):保存计算中间值;
  • 页表基址(CR3):决定虚拟地址翻译成哪个物理地址。

当OS调度器决定切换线程时,它做的唯一动作就是:保存当前线程的RIP/RSP/寄存器到其内核栈,再从下一个线程的内核栈恢复这些值,并更新CR3(如果跨进程)。重点来了:如果两个线程属于同一进程,CR3值不变,省去TLB(Translation Lookaside Buffer)刷新开销;如果属于不同进程,CR3必须换,TLB全失效,下次内存访问要重新查页表,慢100倍以上

这就是进程和线程性能差异的物理根源。colcon build -j8之所以能高效并行,是因为8个编译进程(每个gcc是一个独立进程)共享CPU核心,但每次切换都要刷TLB;而Java线程池里8个Worker线程,切换时CR3不变,上下文切换成本低得多。但代价是:一个线程崩溃(如空指针),整个进程地址空间可能被污染(虽然JVM有保护机制,但native code崩溃仍可能拖垮整个JVM)。

3. 实操验证:用5个命令,亲手揪出进程与线程的“真身”

光说概念太虚。下面带你用真实命令,在自己机器上验证刚才说的每一层关系。我用一台Ubuntu 22.04 + OpenJDK 17的机器演示,你跟着敲,结果会一模一样。

3.1 第一步:启动一个“透明”Java进程,让它永远活着

写一个最简Java类,避免Spring等框架干扰:

// Alive.java public class Alive { public static void main(String[] args) throws InterruptedException { System.out.println("PID: " + java.lang.ProcessHandle.current().pid()); // 开3个后台线程,模拟真实业务 for (int i = 0; i < 3; i++) { new Thread(() -> { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) {} System.out.println("Thread-" + Thread.currentThread().getId() + " running"); } }, "Worker-" + i).start(); } // 主线程阻塞,保持进程存活 Thread.currentThread().join(); } }

编译运行:

javac Alive.java java Alive & # 记下输出的PID,比如 12345

3.2 第二步:用ps/proc确认“进程即线程组”

# 查看进程基本信息 ps -o pid,tid,ppid,comm -p 12345 # 输出: # PID TID PPID COMMAND # 12345 12345 12344 java <-- 主线程,TID==PID # 12345 12346 12345 java <-- Worker-0线程 # 12345 12347 12345 java <-- Worker-1线程 # 12345 12348 12345 java <-- Worker-2线程 # 进入/proc查看线程组详情 ls /proc/12345/task/ # 列出所有线程TID,应该有4个目录(12345,12346,12347,12348) cat /proc/12345/status | grep -E "Tgid|Pid|Threads" # 输出: # Tgid: 12345 <-- 线程组ID(即进程ID) # Pid: 12345 <-- 当前线程ID(主线程) # Threads: 4 <-- 总线程数

关键发现:Tgid恒等于进程启动时的PID,而Pid/proc/<tid>/status里会变化。这证明内核眼里,进程是线程组的逻辑标识,不是独立实体。

3.3 第三步:用jstackjmap看JVM如何映射OS线程

# 获取JVM线程快照 jstack 12345 > jstack.out # 打开jstack.out,搜索"Worker",找到类似: # "Worker-0" #13 prio=5 os_prio=0 tid=0x00007f8b4c00a000 nid=0x303a runnable [0x00007f8b54dfe000] # java.lang.Thread.State: RUNNABLE # at java.io.PrintStream.write(PrintStream.java:635) # - locked <0x000000071800a800> (a java.io.PrintStream) # 解析这行:tid=0x00007f8b4c00a000 是JVM内部线程ID(十六进制),nid=0x303a 是OS线程ID(十六进制转十进制:0x303a = 12346)! # 这和ps看到的TID完全一致。 # 再看内存分布 jmap -histo 12345 | head -20 # 查看堆里对象分布 jmap -clstats 12345 # 查看类加载器统计(验证ClassLoader是进程级单例)

3.4 第四步:用perf追踪一次真实的上下文切换

安装perf工具:

sudo apt install linux-tools-common linux-tools-generic sudo perf record -e sched:sched_switch -p 12345 sleep 5 sudo perf script | head -10

输出类似:

java 12345 [001] 12345.678901: sched:sched_switch: prev_comm=java prev_pid=12345 prev_prio=120 prev_state=R ==> next_comm=java next_pid=12346 next_prio=120 java 12346 [001] 12345.678902: sched:sched_switch: prev_comm=java prev_pid=12346 prev_prio=120 prev_state=R ==> next_comm=java next_pid=12347 next_prio=120

看到没?prev_pidnext_pid都是12345开头的TID,且prev_commnext_comm都是java,说明调度器在同一进程内的不同线程间切换,没有进程切换开销。

3.5 第五步:制造一个“线程死锁”,用jstack精准定位

修改Alive.java,加入死锁逻辑:

static Object lockA = new Object(); static Object lockB = new Object(); new Thread(() -> { synchronized (lockA) { System.out.println("Thread-1 got lockA"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lockB) { System.out.println("Thread-1 got lockB"); } } }, "Deadlock-1").start(); new Thread(() -> { synchronized (lockB) { System.out.println("Thread-2 got lockB"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lockA) { System.out.println("Thread-2 got lockA"); } } }, "Deadlock-2").start();

运行后,jstack 12345会输出:

Found one Java-level deadlock: ============================= "Deadlock-2": waiting to lock monitor 0x00007f8b4c00a000 (object 0x000000071800a800, a java.lang.Object), which is held by "Deadlock-1" "Deadlock-1": waiting to lock monitor 0x00007f8b4c00b000 (object 0x000000071800a810, a java.lang.Object), which is held by "Deadlock-2"

注意:jstack能检测死锁,是因为它遍历了所有java.lang.Thread对象的lockInfo字段,并检查锁的持有/等待关系——这完全是JVM在用户态做的分析,和OS内核无关。但底层支撑这个分析的,正是每个线程独立的栈帧和锁对象引用。

4. 高频场景深度解析:从面试题到线上故障的实战指南

4.1 场景一:Java线程等待都完成——为什么CountDownLatch.await()Thread.join()更可靠?

面试常问:“如何等待多个线程执行完毕?”很多人答for (Thread t : threads) t.join()。这在简单demo里可行,但在生产环境是危险操作。原因有三:

  1. 异常中断风险join()可能被interrupt()打断,抛出InterruptedException,若未正确处理,主线程提前退出,后续线程还在跑;
  2. 超时不可控join(5000)只能设总超时,无法为每个线程单独设超时;
  3. 资源泄漏隐患join()期间主线程阻塞,若被中断且未清理资源(如数据库连接),可能泄露。

CountDownLatch则完全不同。看它的底层实现:

// CountDownLatch.await()最终调用: private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquireShared(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); // 中断时抛异常,但已确保状态一致 } } finally { if (failed) cancelAcquire(node); } }

关键点:CountDownLatch基于AQS(AbstractQueuedSynchronizer)实现,所有等待线程被放入一个FIFO队列,由unpark()唤醒。countDown()调用时,不是唤醒单个线程,而是唤醒所有等待者doReleaseShared())。这意味着:

  • 即使某个线程被中断,CountDownLatch的状态(state变量)已原子递减,不影响其他线程;
  • await(long timeout, TimeUnit unit)可精确控制总超时,超时后自动返回false,业务代码可统一处理;
  • 所有等待线程共享同一个state,无资源竞争。

实操建议:在Spring Boot中,用@Async方法配合CountDownLatch做批量任务聚合:

@Service public class BatchService { @Async public void processItem(int id, CountDownLatch latch) { try { // 模拟耗时操作 Thread.sleep(1000); System.out.println("Processed " + id); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); // 保证一定执行 } } public void batchProcess(List<Integer> ids) throws InterruptedException { CountDownLatch latch = new CountDownLatch(ids.size()); for (int id : ids) { processItem(id, latch); } if (!latch.await(30, TimeUnit.SECONDS)) { // 超时处理 throw new RuntimeException("Batch processing timeout"); } System.out.println("All done!"); } }

实测心得:在QPS 500+的支付对账服务中,用CountDownLatch替代join()后,超时失败率从0.3%降至0.001%,因为join()在GC停顿时可能被意外中断,而CountDownLatch的AQS队列能抵抗短时STW。

4.2 场景二:hashmap线程安全吗?为什么ConcurrentHashMap能扛住高并发?

HashMap不是线程安全的”是标准答案,但多数人不知道不安全的具体表现和修复原理。看HashMap.put()的临界区:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // ① resize()可能被多个线程同时触发 if ((p = tab[i = (n - 1) & hash]) == null) // ② 这里检查为空 tab[i] = newNode(hash, key, value, null); // ③ 这里写入,但②③非原子 else { // 处理冲突... if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // ④ 链表尾插,同样非原子 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))) break; p = e; } } } ++modCount; // ⑤ modCount自增,迭代器据此判断并发修改 ++size; afterNodeInsertion(evict); }

问题在②③和④:线程A检查tab[i] == null为真,正要执行tab[i] = newNode(...),此时线程B也检查为真,也执行赋值——结果只有一个节点被保留,另一个丢失。更严重的是resize()(①):多个线程同时扩容,可能造成链表环形化,get()时无限循环。

ConcurrentHashMap的解决方案是分段锁(JDK7)→ CAS + synchronized(JDK8)。JDK8核心是:

  • 数组桶(Node[])每个元素用volatile修饰,保证可见性;
  • put()时,先用U.compareAndSetObject(tab, i, null, newNode)尝试CAS插入;
  • 若失败(桶非空),则对桶首节点加synchronized锁(锁粒度是单个桶,非整个map);
  • size()通过CounterCell数组累加,避免全局锁。

实测数据:在4核CPU、1000并发线程下,HashMap吞吐量约12万ops/s且大量数据丢失;ConcurrentHashMap达85万ops/s,零丢失。但注意:ConcurrentHashMapsize()是弱一致性,高并发下可能不准,需用mappingCount()替代。

4.3 场景三:qt曲线刷新能放在另一个线程里面吗?——GUI线程的不可侵犯性

Qt的QWidgetQPainter等类不是线程安全的,官方文档明确警告:“所有GUI相关操作必须在主线程(GUI thread)执行”。原因在于:

  • Qt的事件循环(QEventLoop)绑定到主线程,所有鼠标、键盘、重绘事件都在此线程分发;
  • QPainter内部使用OpenGL或Direct2D上下文,这些API要求调用线程拥有窗口句柄所有权;
  • QWidgetrepaint()update()等方法会触发paintEvent(),而paintEvent()必须在GUI线程执行,否则QApplication会崩溃。

正确做法是:工作线程计算数据,通过信号槽通知GUI线程刷新。例如:

// WorkerThread.h class WorkerThread : public QThread { Q_OBJECT public: void run() override { while (running) { // 耗时计算 QVector<double> newData = calculateData(); // 发送信号,跨线程传递数据 emit dataReady(newData); msleep(100); } } signals: void dataReady(const QVector<double>& data); }; // MainWindow.cpp MainWindow::MainWindow() { worker = new WorkerThread(); connect(worker, &WorkerThread::dataReady, this, &MainWindow::onDataReady); worker->start(); } void MainWindow::onDataReady(const QVector<double>& data) { // 此时在GUI线程,安全调用 curve->setSamples(data); // QCustomPlot示例 plot->replot(); }

注意:connect()的第五个参数必须是Qt::QueuedConnection(默认),确保信号在GUI线程的事件队列中执行。若用Qt::DirectConnection,信号会在工作线程直接调用onDataReady,导致崩溃。

4.4 场景四:mate-indicators进程可关闭吗?——桌面环境进程的共生关系

mate-indicators是MATE桌面环境的系统托盘服务,负责显示网络、音量、电源等指示器。它不是一个孤立进程,而是marco(窗口管理器)、caja(文件管理器)深度耦合的守护进程。强行kill -9会导致:

  • 托盘图标消失,但相关服务(如NetworkManager)仍在运行,只是无法显示状态;
  • 某些应用(如plank启动器)依赖其D-Bus接口,关闭后可能无法响应快捷键;
  • 下次登录时,mate-session会自动重启它,因为它是/usr/share/mate-session/sessions/mate.session中定义的必需组件。

安全关闭方式只有两种:

  1. 通过桌面设置禁用:System > Preferences > Startup Applications,取消勾选Indicator Applet
  2. 临时停止:systemctl --user stop mate-indicators.service(需支持user session D-Bus)。

实操心得:曾有用户为“节省内存”关闭mate-indicators,结果导致微信桌面版无法弹出消息通知(因微信通过D-Bus向indicator注册通知),最后不得不重装MATE。记住:桌面进程不是服务器进程,它的存在价值是“用户体验连贯性”,而非“资源最小化”。

5. 常见问题与排查技巧实录:27个真实故障的避坑清单

5.1 CPU占用异常:是线程在跑,还是进程在堵?

现象:top显示某个Java进程CPU 99%,但jstack里所有线程都是TIMED_WAITING。这通常不是CPU真忙,而是JVM GC线程在疯狂工作,或JNI代码陷入死循环

排查步骤:

  1. jstat -gc <pid>查看GC频率:若YGCT(Young GC时间)或FGCT(Full GC时间)持续增长,说明内存泄漏;
  2. jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c | sort -nr统计线程状态分布:若RUNNABLE占比<10%,大概率是GC或锁竞争;
  3. jcmd <pid> VM.native_memory summary查看本地内存:若Internal项暴涨,可能是Netty的DirectByteBuffer未释放;
  4. 最终手段:sudo perf top -p <pid>,看热点函数——若出现__libc_mallocJVM_RedefineClasses,基本确定是内存问题。

避坑技巧:线上服务务必配置-XX:+PrintGCDetails -Xloggc:/var/log/gc.log,用gcviewer分析GC日志。我处理过一个案例:-Xmx4gjmap -histo显示char[]占堆80%,根源是Logback配置了<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>,JSON序列化产生大量临时字符串。

5.2 内存占用异常:ps auxjmap结果为何差10倍?

ps aux%MEM列显示进程物理内存占用,而jmap -heap显示JVM堆内存。两者差异巨大,原因有三:

差异源ps auxjmap -heap典型场景
Native Memory包含JVM自身(CodeCache、Metaspace)、JNI库、DirectByteBuffer仅Java堆(Eden/Survivor/Old)Netty服务-XX:MaxDirectMemorySize=2gps多出2GB
内存碎片物理内存页分配,包含未使用的预留页堆内碎片,jmap只统计已分配对象-Xms1g -Xmx4gps始终显示~4GB
共享库所有进程共享的.so库计入每个进程完全不计入libjvm.sops中重复计算

实测对比:一个Spring Boot应用,ps aux显示RSS 3.2g,jmap -heap显示堆使用1.1g,差额2.1g中:1.5g是-XX:MaxMetaspaceSize=512m+CodeCache+Compressed Class Space,0.6g是Netty的DirectByteBuffer

避坑技巧:用pmap -x <pid>看详细内存映射,重点关注anon(匿名映射,即堆/直接内存)和shared(共享库)列。若anon远大于jmap结果,检查-XX:MaxDirectMemorySize-XX:MaxMetaspaceSize

5.3 线程池配置:colcon build线程数和Java线程池的黄金法则

colcon build -j8-j参数指定并行进程数,不是线程数。每个gcc进程是独立进程,有自己的内存空间。而Java线程池的corePoolSize应遵循:

  • CPU密集型任务(如图像处理、加密):corePoolSize = CPU核心数 * 2(留1个核心给OS);
  • IO密集型任务(如HTTP请求、DB查询):corePoolSize = CPU核心数 / (1 - 阻塞系数),阻塞系数按经验取0.8~0.9,即corePoolSize ≈ CPU核心数 * 5
  • 混合型任务:用ThreadPoolTaskExecutorallowCoreThreadTimeOut=true,让空闲核心线程可回收。

Spring Boot默认ThreadPoolTaskExecutor配置:

spring: task: execution: pool: core-size: 8 # 默认8,适合8核服务器 max-size: 16 # 最大线程数 queue-capacity: 100 # 队列容量,超限触发拒绝策略
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 9:55:48

Atlas 300V推理卡部署YOLOv8全流程:从模型转换到AscendCL实战

搞推理部署这一块的时间长了&#xff0c;慢慢会发现一个很有意思的现象&#xff1a;很多人一提到AI加速卡&#xff0c;脑子里默认就是NVIDIA的GPU&#xff0c;什么A100、4090、V100&#xff0c;张口就来。但真到了项目落地阶段&#xff0c;尤其是边缘侧、视频分析、工业视觉这类…

作者头像 李华
网站建设 2026/9/20 9:54:38

Atlas 300V部署YOLO实战:从模型转换到多路推理优化

Atlas 300V 24G这个名字&#xff0c;我第一次拿到手的时候也以为是块“插上就能让YOLO飞起来”的加速卡。结果装上驱动、看着npu-smi正常识别之后&#xff0c;我拿训练好的YOLOv8s模型去找推理入口&#xff0c;才发现根本不是那么回事——模型格式不对、接口不认、环境变量没配…

作者头像 李华
网站建设 2026/9/20 9:53:17

AI管理工具如何沉淀团队经验,实现知识自动传承

大家有没有遇到过这种场景&#xff1a;团队里最有经验的那位核心工程师一旦休假或者离职&#xff0c;很多关键决策、历史背景、踩坑教训就跟着他一起消失了。新来的同事小心翼翼地来问&#xff0c;老同事只能凭记忆回答&#xff0c;而且越传越失真。我一直在想&#xff0c;有没…

作者头像 李华
网站建设 2026/9/20 9:52:37

彻底清除exe病毒与xmrig挖矿木马:从进程到持久化的完整操作指南

1. 先搞清楚你面对的是什么&#xff1a;exe病毒与xmrig挖矿木马的典型行为特征很多人一看到任务管理器里某个.exe进程 CPU 占用飙到 90% 以上&#xff0c;第一反应就是“中毒了”&#xff0c;然后直接结束进程、删掉文件&#xff0c;重启之后发现它又回来了。这种“杀不死”的体…

作者头像 李华
网站建设 2026/9/20 9:52:29

Roo Code 并发重试:TaoToken 下看 429 退避与 Token 重放

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 9:52:13

Kimi K2.7 Code 上了 LiveCodeBench:用 TaoToken 同一把 Key 跑同一题集

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华