news 2026/9/9 9:14:20

JVM内存结构详解:从启动失败到性能调优一网打尽

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM内存结构详解:从启动失败到性能调优一网打尽

一个跑了好几年的 Java 服务,某天突然起不来了,控制台里只有一行 "error invoking method. failed to launch jvm",连堆栈都没有。用户急,运维也急,大家只能一遍遍试启动参数。说实话,JVM 相关的知识,大学课本里讲过,各种面试题里也见过,但真正到了线上故障那一刻,你才会发现:内存结构、性能调优、JVM 与 JRE 的关系、启动失败的排查,这些看似零散的知识点其实是一条完整的线索。这篇文章我想用一篇的篇幅,把 JVM 的内存结构讲透,再把基于这份地图的启动排障与性能调优方法串起来,顺带回答几个热度一直很高的高频问题。适合正在学 JVM 的 Java 开发者,也适合已经工作几年、想系统补全这块知识体系的人。

1. 运行时数据区:先把 JVM 的"地盘"划分搞清楚

1.1 线程私有的三个区域:程序计数器、虚拟机栈、本地方法栈

JVM 在执行 Java 字节码的时候,并不是把所有东西都扔到一个大池子里,而是把内存按职责划分成了几个区域。规范里给这些区域起了一个总称:运行时数据区(Runtime Data Area)。其中程序计数器、虚拟机栈、本地方法栈是线程私有的——每个线程都有自己的那一份,互不干扰;堆和方法区是进程级共享的,所有线程都在里面"抢地盘"。

程序计数器(Program Counter Register)是这里面最小的一个区域,作用是记录当前线程正在执行的字节码指令地址。别小看它,线程切换、异常处理、循环跳转都靠它恢复"刚才看到哪一行了"。这个区域是唯一一个不会抛 OutOfMemoryError 的区域,因为虚拟机规范根本没有给它分配内存的规定,它的天然体积也非常小。

虚拟机栈(JVM Stack)就重要多了,每个方法从调用到执行完毕,对应一个栈帧(Stack Frame)入栈和出栈的过程。栈帧里有四样东西:局部变量表(存放基本类型、引用类型和 returnAddress)、操作数栈(像计算机里那个运算栈)、动态链接(指向运行时常量池的引用,用于解析方法和字段)、方法出口(异常处理表和正常返回地址)。方法一旦调得太深,栈空间就会耗尽,抛出 StackOverflowError;如果是栈动态扩展时申请不到内存,则是 OutOfMemoryError。栈的大小用 -Xss 控制,默认在大多数平台上是 1MB 左右,这个值不是越大越好,调大了会占用更多总内存,因为每个线程都有一份独立栈。

本地方法栈(Native Method Stack)和虚拟机栈结构类似,只是它服务于 native 方法。HotSpot 虚拟机做了一个很实际的合并:把本地方法栈和虚拟机栈合二为一,所以你在排查问题时常常会觉得"这两个不是一回事吗"——在 HotSpot 里它们确实就是同一个栈。

1.2 线程共享的堆和方法区:GC 的主战场

堆(Java Heap)是 JVM 管理内存中最大的一块,也是垃圾收集器的"主战场"。几乎所有 new 出来的对象实例都在这里分配(栈上分配、标量替换等优化手段除外)。堆是所有线程共享的,所以这里最热闹,也最容易出问题。堆的大小通过 -Xms(初始)和 -Xmx(最大)控制,线上经验一般是把两者设为相同值,避免运行期频繁扩容收缩。

方法区(Method Area)在逻辑上属于堆的一部分,但很多人喜欢把它单独拿出来记。它存储的是已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存。JDK 8 之前,HotSpot 用"永久代"(PermGen)来实现方法区,JDK 8 开始彻底改成了"元空间"(Metaspace),直接使用本地内存(Native Memory),不再受堆大小的限制,而是受操作系统内存限制。这一个改动直接解决了一类经典 OOM——PermGen Space 溢出。

方法区里还包含运行时常量池(Runtime Constant Pool),存放编译期生成的各种字面量与符号引用。类加载之后,常量池里的信息会被放进来,同时运行期也可以往里添加新内容(比如 String.intern() 方法)。

把这块内容用个类比来记:堆像公司的公共仓库,所有线程都往里面放东西、取东西;虚拟机栈是每个员工面前的工作台,人走台空;程序计数器就是员工手里的记事本;方法区是公司的规章制度档案室,装的是类结构和版本信息这类"制度性"内容。

1.3 方法区在 JDK 8 之后的最大变化:永久代彻底退出

这里要专门强调一下永久代和元空间的区别,因为面试和实际排查中都特别容易踩。永久代在 JDK 7 及以前是堆的一部分,受 -XX:MaxPermSize 控制;JDK 8 之后元空间用本地内存实现,默认情况下大小几乎不受限,只受系统物理内存约束,你可以用 -XX:MaxMetaspaceSize 给它设一个上限防止失控。

这个变化带来的实际影响是:以前常见的 "java.lang.OutOfMemoryError: PermGen space" 在 JDK 8 之后基本消失了,取而代之的是 "Metaspace" 相关的溢出。但注意,元空间使用本地内存也是一把双刃剑——如果类加载太频繁且卸载不掉(典型场景:热部署、动态代理生成大量代理类),元空间会持续膨胀,最终把机器内存吃光,表现出来就是机器突然卡死或者进程被系统的 OOM Killer 干掉,而不是 JVM 自己抛异常。所以生产环境建议把 -XX:MaxMetaspaceSize 显式配起来。

2. 堆内存的分代设计与对象的一生:GC 判断垃圾靠的是这条路

2.1 新生代里的 Eden 区与 Survivor 区怎么配合

堆虽然是一大块,但 JVM 不会傻乎乎地在整块堆里做统一扫描。商业虚拟机的经验法则是"绝大多数对象朝生夕灭",于是堆被划分为新生代(Young Generation)和老年代(Old Generation),新生代内部又分成一块 Eden 区和两块 Survivor 区(S0 和 S1),默认比例是 8:1:1,可用 -XX:SurvivorRatio 调整。

新对象一般先在 Eden 区分配。当 Eden 区满的时候,触发 Minor GC,整个过程用复制算法:把存活对象复制到某一块 Survivor 区,然后一次性清空 Eden 和另一块 Survivor。下次 Minor GC 时,把 Eden 和这块存活区的对象再复制到另一块 Survivor 区,如此反复。每经历一次 Minor GC 存活下来的对象年龄 +1,默认到 15 岁(-XX:MaxTenuringThreshold 可调)就晋升到老年代。

为什么用两块 Survivor?因为复制算法有个前提——大量对象需要被回收时,复制存活对象很划算。如果只有一块 Survivor,就没法实现"一块收、一块空"的交替复制,也没办法及时整理碎片。这是分代设计里最容易考的点,面试官问"为什么是两个 Survivor 而不是一个",答案就是这个。

2.2 对象什么时候"晋升"到老年代

除了年龄到达阈值会晋升,还有几种情况会直接进入老年代。一是大对象:-XX:PretenureSizeThreshold 设置了阈值,超过该大小的对象直接在老年代分配,避免在新生代和 Survivor 之间来回复制浪费性能。二是空间分配担保:Minor GC 前,JVM 会检查老年代最大可用连续空间是否大于新生代所有对象总大小,如果不满足,就可能直接触发一次 Full GC 来腾空间。

还有一点容易被忽略:动态年龄判定。虚拟机并不一定等对象到 15 岁才晋升,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就直接进入老年代。所以你在线上看到的对象年龄可能只有三四岁就晋升了,这是正常现象。

Minor GC 和 Full GC 的区别也要分清:Minor GC 只回收新生代,频率高、速度快;Full GC 回收整个堆和方法区(元空间),通常伴随老年代的标记-整理或标记-清除过程,停顿时间长,是调优的重点关注对象。如果 Full GC 频繁,要么是老年代空间不足,要么是晋升速度过快,要么是元空间膨胀,要么是代码里有人在显式调用 System.gc()。

2.3 判断对象是否可回收:可达性分析与四种引用

确定一个对象该不该死,主流 JVM 用的是可达性分析(Reachability Analysis),而不是简单的引用计数。思路是从一组称为 GC Roots 的根对象出发,沿着引用链向下搜索,能到达的对象就算"活的",不可达的对象就视为垃圾。GC Roots 包括:虚拟机栈帧中的局部变量所引用的对象、方法区中的静态变量和常量引用的对象、本地方法栈中 JNI 引用的对象、被 synchronized 持有的对象,以及活跃线程等。

引用计数法为什么不行?因为它解决不了循环引用:A 引用 B,B 引用 A,两个对象互相指着但外部已经没人用它们了,引用计数永远不为 0,内存就泄漏了。这也是一个高频面试点。

Java 还把引用分成了四种强度:强引用(Strong)是普通的 new 出来的引用,GC 绝对不会动它;软引用(Soft)在内存即将溢出时才会被回收,适合做缓存;弱引用(Weak)在下次 GC 时必然被回收,典型应用是 ThreadLocal 的 key;虚引用(Phantom)最弱,无法通过它获取对象,只用于在对象被回收时收到一个系统通知,比如 NIO 里 DirectByteBuffer 的回收就依赖虚引用。

3. 一次真实的启动失败现场:"error invoking method. failed to launch jvm" 排查全过程

3.1 这个报错到底是谁抛出来的

先别急着改参数,搞清楚报错的来源很重要。"error invoking method. Failed to launch JVM" 这句话并不是某个 Java 库抛出的异常,而是启动器(Launcher)给出的提示。很多商业软件、集成安装包(用 InstallAnywhere 之类的工具打的包)、IDE 插件、甚至一些老的 WebLogic 安装脚本,它们的启动器本身是一个原生程序,启动时通过配置文件去定位 JVM 并执行启动方法。如果这一步失败,启动器就用这个笼统的提示来搪塞你。

知道了来源,排查思路就清晰了:你不是在排查一个 Java 异常,而是在排查"一个原生程序为什么没能成功拉起 JVM 进程"。这决定了下面的排查顺序。

3.2 排查链路:从内存参数到架构匹配

我这里有一条实际可复用的排查链路,按优先级从高到低:

第一,看启动器的 VM 配置。InstallAnywhere 打的包通常在同目录下有一个 .lax 文件(比如 xxx.lax),里面写着 vm 参数,常见的有 -Xmx、-Xms、-D 选项。把 -Xmx 先降下来试,比如从 1024m 改成 512m。很多小机器上这个报错就是因为 -Xmx 开得太大,系统剩余物理内存不够,或者 32 位 JVM 分配不到足够大的连续地址空间。

第二,确认 JVM 本身可用。命令行直接执行 java -version,如果这一步都报错,说明你的 JAVA_HOME 或者 PATH 环境有问题,或者 PATH 里指向了一个损坏的 JDK。这里要特别提一下 32 位和 64 位的匹配问题:启动器是 64 位的,却找到了 32 位的 JVM,很容易出现 Failed to launch JVM 这类错误。在 Linux 上用 file $(which java) 或者 uname -m 看一下架构,Windows 上检查 Java 安装路径是 Program Files 还是 Program Files (x86)。

第三,检查环境变量。启动器在解析 JVM 路径时,通常优先用配置里的绝对路径,其次看 JAVA_HOME,最后才看 PATH。JAVA_HOME 必须指向 JDK 或 JRE 的根目录而不是 bin 目录,这个问题很经典,值得反复强调。

第四,看看是不是杀毒软件或安全策略把临时目录里的文件给拦了。安装类软件经常把 JVM 相关文件解压到临时目录再执行,如果被杀,表现就是这个启动直接失败。

第五,如果应用跑在容器里,还要看容器的内存限制。Java 8 之前的 JVM 不感知 cgroup 限制,你 -Xmx 设了 2G,但容器内存只有 1.5G,外部启动器去拉进程的时候被系统拒绝,报错形式多种多样,这类报错只是其中一种。JDK 8u191+ 支持 -XX:MaxRAMPercentage 来按容器配额自适应。

我遇到过最典型的一次:一个安装包在 32 位 Windows 上跑得好好的,换到 64 位 Windows 后偶发这个报错,排查了很久才发现是 .lax 里写死了 -Xmx1024m,而 32 位 JVM 在 64 位系统上运行时,进程的地址空间分配经常出现碎片,导致无法分配连续内存。把 -Xmx 降到 768m 之后,问题再没出现过。

3.3 预防同类问题的三个习惯

这类启动问题,与其每次现查,不如提前堵住。我的经验是:

  • 把 JVM 相关参数统一放在一个配置文件里,写清楚每个参数的含义和单位,别让 -Xmx 散落在各种启动脚本中。
  • 启动脚本里加一段自检逻辑:先执行 java -version,比对架构、版本,再决定是否继续启动。看起来多花了几毫秒,但能省下大量排查时间。
  • 记录基线:每个环境(开发、测试、生产)的 JVM 参数、物理内存、JVM 版本都记录在案,出问题时有据可查。

4. JDK、JRE、JVM 之间的关系:很多事故的源头其实在这里

4.1 三者的职责边界

"jre和jvm之间的关系" 是搜索热度长期居高不下的问题,很多做了好几年 Java 的人也未必能一句话讲清楚。我的理解是:

JVM(Java Virtual Machine)是规范也是实现。它负责读取 class 字节码,解释或即时编译成机器指令执行。你有 HotSpot、OpenJ9、GraalVM 这些不同的实现,它们都遵循同样的 JVM 规范,所以同一个 class 文件可以在不同 JVM 上运行。

JRE(Java Runtime Environment)是运行 Java 程序的最小环境:JVM + Java 核心类库(java.、javax.这些)+ 一些支撑文件。如果你只需要运行别人写好的 Java 程序,装 JRE 就够了。

JDK(Java Development Kit)是开发工具全家桶:JRE + 开发工具(javac、jar、javap、jdb、javadoc)+ 基础类库源码。你要编译 Java 代码,必须装 JDK。另外 JDK 9 开始,官方不再单独发布 JRE 安装包,运行时镜像可以用 jlink 按需裁剪,这算是这个关系模型里最新的变化,但不影响 JVM 规范本身的划分。

一句话总结:JDK 包含 JRE,JRE 包含 JVM,JDK = JVM + 类库 + 工具,JRE = JVM + 类库。

4.2 只装 JRE 的机器上跑程序,常见的问题

很多生产服务器为了"精简"只装 JRE,结果跑应用时出各种莫名其妙的问题。最常见的三个:

一是启动脚本里调用了 JDK 专属工具。比如有些自动化部署脚本会执行 keytool 导入证书,或者用 javap 做类检查,甚至有些应用服务器会尝试调用 javac 做 JSP 预编译,这些工具在 JRE 里都不存在。

二是 JAVA_HOME 语义错误。如果 JAVA_HOME 指向 JRE 目录,某些脚本在 ${JAVA_HOME}/bin/java 能找到 java,但在 ${JAVA_HOME}/lib/tools.jar 或 ${JAVA_HOME}/bin/javadoc 等路径上就找不到文件了。像 JConsole、VisualVM 这类诊断工具,纯 JRE 环境根本起不来。

三是版本混淆。有的机器上 PATH 和 JAVA_HOME 指向两个不同版本的 JVM,启动脚本用 JAVA_HOME 的版本,而你自己手工敲 java -version 看到的又是另一个版本,排查时极容易误判。我的建议是:生产环境统一装 JDK 而不是 JRE。理由是现在 JDK 和 JRE 的体积差别已经不大,而且很多运行期诊断工具只有 JDK 才有,你不可能等线上出问题的时候再临时补装。

5. 性能调优从哪下手:GC 日志、参数与工具的配合

5.1 先学会看 GC 日志,再谈调优

性能调优的第一步永远不是改参数,而是观察。而观察 JVM 内部状态最直接的手段,就是 GC 日志。JDK 8 及之前常用 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -verbose:gc;JDK 9 之后日志系统统一成了 -Xlog:gc*,比如 -Xlog:gc*:file=gc.log:time,uptime,level,tags。

拿到 GC 日志先看三件事:一次 Minor GC 的停顿时间有多长、一次 Full GC 的停顿时间有多长、两次 GC 的间隔是多久。比如下面这种典型输出:

[GC (Allocation Failure) [PSYoungGen: 65536K->8128K(76288K)] 65536K->12176K(251392K), 0.0245678 secs] [Full GC (Ergonomics) [PSYoungGen: 8128K->0K(76288K)] [ParOldGen: 40512K->48640K(175104K)] 48640K->48640K(251392K), 0.8734561 secs]

Minor GC 停顿在几十毫秒内、Full GC 在几百毫秒内且一天才几次,通常是健康状态;如果 Full GC 每秒好几次,或者单次停顿超过几秒,那就不是调参能简单解决的了,可能要怀疑代码层面有内存泄漏或超大对象。

配合 jstat 可以看实时情况:jstat -gcutil 1000 每秒打印一次各区域的实用率,连续观察几分钟,能看出 Eden 区是否频繁打满、老年代是否在稳步爬升。jmap -heap 可以看堆的分布,jmap -histo:live 可以看哪些类占内存最多,这是定位内存泄漏最常用的一招。

提示:JDK 9 之后 jmap 的部分功能被 jhsdb 取代,但基本定位思路没变。

5.2 核心调优参数逐个拆解

下面是我在生产环境实际用过、且推荐你优先掌握的一组参数。

参数作用我的建议
-Xms / -Xmx堆初始/最大大小线上设为相同值,避免动态扩容
-Xmn新生代大小优先于 -XX:NewRatio 直接控制
-XX:NewRatio老年代/新生代比值默认 2,即新生代占 1/3 堆
-XX:SurvivorRatioEden/Survivor 比值默认 8,基本不用动
-XX:MaxMetaspaceSize元空间上限必须显式设置,防止本地内存失控
-XX:MaxRAMPercentage容器内堆占配额百分比JDK 8u191+ 容器场景推荐
-XX:+HeapDumpOnOutOfMemoryErrorOOM 时自动导出堆转储强烈建议开启,配合 HeapDumpPath
-XX:MaxGCPauseMillisG1 的目标停顿时间从 200ms 起调,别设太激进
-XX:+UseG1GC / -XX:+UseParallelGC垃圾收集器选择G1 适合大堆和可控停顿场景,ParallelGC 追求吞吐量

-Xmx 设置多少合适?经验上,如果机器只有这一个 Java 进程,堆最大可以用到物理内存的 60%~75%,再留一部分给元空间、线程栈、直接内存和操作系统。盲目的"越大越好"很容易带来更长甚至更频繁的 Full GC,因为堆变大后单次 GC 扫描范围更大。

新生代大小对 GC 频率影响最大。新生代太小,Minor GC 频繁;太大,老年代空间被压缩,Full GC 风险升高。我一般从堆的 1/3 开始调,然后观察 Minor GC 频率和停顿时间做微调。

5.3 一套可复用的调优流程

我给团队定了这么一套流程,你也可以直接拿去用:

  1. 先量化指标:记录当前的应用吞吐量、接口响应时间、GC 停顿时间、JVM 各区域使用率。
  2. 分析瓶颈类型。用 jstat 看 GC 频率,用 top 看 CPU,用 jmap/jstack 看线程和堆。是 CPU 打满(可能是代码热点或死循环)、老年代爬升(可能是泄漏)、还是 Full GC 频繁(可能是参数或晋升问题)。
  3. 定一个假设,改一个参数。别一次改五六个参数,否则出了问题你根本不知道是谁引起的。
  4. 验证效果,回归测试。用压测工具或者真实流量观察至少一天。
  5. 把稳定的参数组合固化成基线,写进部署文档和启动脚本。

调优最忌讳的就是"玄学调参"——不看日志、不看监控,凭感觉把 -Xmx 翻一倍。我见过太多系统崩溃其实问题在代码层,参数改了只是把崩溃延后了几个小时而已。

6. 高频面试题背后的三个底层考点

6.1 内存结构类问题怎么答才不踩坑

关于 JVM 内存,面试官几乎必问"JVM 运行时数据区有哪些"。这个题看似简单,但很多人的答案是背出来的,一问"JDK 8 之后方法区去哪儿了"就卡住。我的建议是:先答完整结构(程序计数器、虚拟机栈、本地方法栈、堆、方法区/元空间),然后立刻补充"JDK 8 之后永久代被元空间替代,使用本地内存",再主动提到堆内分代(新生代、老年代、Eden、Survivor)。这样就展示了你不仅知道静态结构,还知道版本演进。

另一个高频变种是"什么情况会抛出 StackOverflowError 和 OutOfMemoryError"。回答时必须区分区域:栈溢出通常是递归过深,堆溢出通常是对象太多或泄漏,元空间溢出通常是类加载过多。每个区域说一个典型场景,面试官基本就满意了。

6.2 GC 与类加载类问题的答题框架

"判断对象是否可回收"这类问题,答题框架是:先说结论(可达性分析),再说 GC Roots 包括哪几类,最后补一句"为什么不用引用计数"——因为有循环引用问题。这个框架里最容易被忽略的是 GC Roots 的具体内容,很多人只说"从根对象出发"就没了,一定要把栈帧局部变量、静态变量、常量引用、JNI 引用这四类说全。

类加载过程(加载、验证、准备、解析、初始化)和双亲委派模型也是高频题。双亲委派的关键词是"自下而上检查,自上而下尝试加载":一个类加载器收到加载请求,先交给父加载器,父加载器加载不了才自己尝试。它的好处是避免核心类被重复加载和伪造,保证 java.lang.String 这种核心类始终是同一个。

6.3 调优类问题的避雷姿势

"你做过 JVM 调优吗" 是高级岗位的高频问题。最容易好高骛远地回答"我把 -Xmx 调大到 8G"——这不算调优,只是改参数。正确的回答姿势是:先讲你的监控手段(GC 日志、jstat、jmap、Arthas 之类),再讲你发现的具体问题(比如 Full GC 频率高、老年代持续增长),然后给出你的分析过程(怀疑是缓存对象过多,用 jmap 看到某个类实例数异常),最后讲你的措施和验证结果。

哪怕你只处理过一次真实的线上问题,也比你背十个调优参数强。面试官考的是你有没有排查思路,而不是知不知道参数名。

最后说一点我自己的体会。JVM 这套东西,初看是知识点,再看是方法论,真正用的时候其实是"地图"。内存结构是地图的坐标,GC 是地图上的交通规则,启动报错和调优是你在路上遇到的各种状况。把地图画熟,遇到问题你就知道自己在哪个路口,该往哪个方向排查,而不是在群里发截图等别人给"玄学方案"。我建议每个 Java 开发者都亲手做一次这样的练习:搭一个容易内存泄漏的小程序,开 GC 日志,用 jmap 定位泄漏点,再调参数验证效果。这个过程走一遍,你对 JVM 的理解会比刷十篇面试题都深。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 9:14:18

IntelliJ IDEA 2026.1:从AI补全到智能体,万能插座式AI编程实践

1. 这次更新为什么值得关注 如果你每天都在用 IntelliJ IDEA 写代码,那你大概率已经注意到,过去一年几乎所有的 IDE 都在往里面塞 AI 功能。GitHub Copilot 是起步最早的,Cursor 是靠 AI 原生的交互体验杀出来的,而 IDEA 这边的动…

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

嵌入式硬件开发:原理图即制造起点

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

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

定位器怎么选?从原理到场景的选购避坑指南

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

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

基础模型时代Human-Centric AI:从看见人到行动如人的技术谱系

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

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

PostgreSQL查看表结构全攻略:从实例到列,系统表与psql元命令详解

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

作者头像 李华
网站建设 2026/9/9 9:11:45

Intel 06H处理器机器检查错误码深度解析:从MCi_Status到故障定位

1. 这不是教科书里的“概念复述”,而是芯片工程师现场调试时真正盯住的那串十六进制数字你有没有在服务器机房深夜值班时,突然收到一条告警:“Machine Check Exception triggered on CPU 3”?紧接着控制台刷出一长串类似MCi_Statu…

作者头像 李华