内存模型

# 内存模型

# Java 内存模型

参考文章 (opens new window)

JVM 包含两个子系统和两个组件,两个子系统分为 Class loader(类装载)、Execution Engine(执行引擎);两个组件为 Runtime data area(运行时数据区)、Native Interface(本地接口)。

  • Class loader(类装载):根据给定的全限定名类名来装载 class 文件到 Runtime data area
  • Execution Engine(执行引擎):执行 classes 中的指令。
  • Native Interface(本地接口):与 native libraries 交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区):就是我们常说的 JVM 内存。

作用:首先通过编译器把 Java 代码转换成字节码,类加载器再把字节码加载到内存中,将其放在运行时数据区的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程需要调用其他语言的本地库接口来实现程序的功能。

# 堆和栈的区别

  • 物理地址:堆的物理地址分配对对象是不连续的,因此性能慢些。在 GC 的时候也要考虑不连续的分配,所有有各种算法。比如,标记清除、复制、标记压缩和分代。而栈使用的数据结构中的栈,后进先出的原则,物理地址分配是连续的,所以性能快。

  • 内存分别:堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定,一般堆大小远远大于栈。栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

  • 存放内容:堆存放的是对象的实例和数组,因此该区更关注的数据的存储。栈中存放局部变量、操作数栈、返回结果,该区更关注的是程序方法的执行。

  • 程序可见度:堆对于整个应用程序是共享可见的。栈只对线程是可见的,线程私有,和线程生命周期相同。


# 类加载

# 类的生命周期

Class 文件需加载虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件的呢?

系统加载 Class 类型的文件主要是三步:加载、连接和初始化。连接分为验证、准备和解析。

  • 加载:根据查找路径找到相应的 class 文件然后导入;
  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。
  • 卸载:该类的 Class对象被 GC

# Class.forName 和 ClassLoader 的区别

都可以对类进行加载,但是 Class.forName 将 .class 文件加载到了 jvm ,还会对类进行解释,执行静态代码块,执行给静态变量赋值的静态方法。

而 ClassLoader 只有在 newInstance() 的时候才会执行构造方法。


# 双亲委派机制

参考文章 (opens new window)

# 为什么需要双亲委派机制?

对于任意一个类,都需要加载它的类加载器和这个类本身来一同确立其在 Java 虚拟机中的唯一性。

# 双亲委派机制

如果一个类加载器收到了某个类的加载请求,则该类加载器并不会直接去加载该类,而是把这个请求委派给父类加载器,每个层次的类加载器都是如此。因此所有的类加载请求最终都会被传送到顶端的启动类加载器。只有当父类加载器在搜索范围内查找到无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试自己加载。

# JVM 提供的三种类加载器

  • 启动类加载器(BootStrap ClassLoader):C++ 实现,在 Java 内无法获取,负责加载 <JAVA_HOME>/lib/目录中被虚拟机识别的类库。
  • 扩展类加载器(Extension ClassLoader):Java 实现,可以在 Java 里获取,负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
  • 系统类加载器/应用程序类加载器(Application ClassLoader):是我们接触最多的类加载器,我们写的代码默认就是由它加载的。

# 破坏双亲委派机制

为什么要破坏双亲委派机制呢?

因为在某些情况下父类加载器需要委托子类加载器去加载 class 文件。受到加载范围的限制,父类加载器无法加载到需要的文件。以 Driver 接口为例,由于 Driver 接口定义在 JDK 中,而其实现是由各个数据库的服务商类提供,比如 MySQL 的就写了 MySQL Connector ,那么问题就来了,DriverManager(也由 JDK 提供)要加载各个实现了 Driver 接口的实现类,然后进行管理,但是 DriverManager 由启动类加载器加载,只能加载JAVA_HOMElib下文件,而其实现是由服务商提供的额,由系统类加载器加载,这个时候就需要启动类加载器类委托子类来加载 Driver 实现,从而破坏双亲委派。

违反双亲委派机制的例子还有 JDNI、热部署、Tomcat、JDBC 等。


# 垃圾回收

# YGC的执行过程

YGC采用的复制算法,主要分成以下两个步骤:

1、查找GC Roots,将其引用的对象拷贝到S1区

2、递归遍历第1步的对象,拷贝其引用的对象到S1区或者晋升到Old区

上述整个过程都是需要暂停业务线程的(STW),不过ParNew等新生代回收器可以多线程并行执行,提高处理效率。

YGC通过可达性分析算法,从GC Root(可达对象的起点)开始向下搜索,标记出当前存活的对象,那么剩下未被标记的对象就是需要回收的对象。

图片

可作为YGC时GC Root的对象包括以下几种:

1、虚拟机栈中引用的对象

2、方法区中静态属性、常量引用的对象

3、本地方法栈中引用的对象

4、被Synchronized锁持有的对象

5、记录当前被加载类的SystemDictionary

6、记录字符串常量引用的StringTable

7、存在跨代引用的对象

8、和GC Root处于同一CardTable的对象

其中1-3是大家容易想到的,而4-8很容易被忽视,却极有可能是分析YGC问题时的线索入口。

另外需要注意的是,针对下图中跨代引用的情况,老年代的对象A也必须作为GC Root的一部分,但是如果每次YGC时都去扫描老年代,肯定存在效率问题。在HotSpot JVM,引入卡表(Card Table)来对跨代引用的标记进行加速。

图片

Card Table,简单理解是一种空间换时间的思路,因为存在跨代引用的对象大概占比不到1%,因此可将堆空间划分成大小为512字节的卡页,如果卡页中有一个对象存在跨代引用,则可以用1个字节来标识该卡页是dirty状态,卡页状态进一步通过写屏障技术进行维护。

遍历完GC Roots后,便能够找出第一批存活的对象,然后将其拷贝到S1区。接下来,就是一个递归查找和拷贝存活对象的过程。

S1区为了方便维护内存区域,引入了两个指针变量:_saved_mark_word和_top,其中_saved_mark_word表示当前遍历对象的位置,_top表示当前可分配内存的位置,很显然,_saved_mark_word到_top之间的对象都是已拷贝但未扫描的对象。

图片

如上图所示,每次扫描完一个对象,_saved_mark_word会往前移动,期间如果有新对象也会拷贝到S1区,_top也会往前移动,直到_saved_mark_word追上_top,说明S1区所有对象都已经遍历完成。

有一个细节点需要注意的是:拷贝对象的目标空间不一定是S1区,也可能是老年代。如果一个对象的年龄(经历的YGC次数)满足动态年龄判定条件便直接晋升到老年代中。对象的年龄保存在Java对象头的mark word数据结构中(如果大家对Java并发锁熟悉,肯定了解这个数据结构,不熟悉的建议查阅资料了解下,这里不做展开)。


# YGC的触发时机

当Eden区空间不足时,就会触发YGC。结合新生代对象的内存分配看下详细过程:

1、新对象会先尝试在栈上分配,如果不行则尝试在TLAB分配,否则再看是否满足大对象条件要在老年代分配,最后才考虑在Eden区申请空间。

2、如果Eden区没有合适的空间,则触发YGC。

3、YGC时,对Eden区和From Survivor区的存活对象进行处理,如果满足动态年龄判断的条件或者To Survivor区空间不够则直接进入老年代,如果老年代空间也不够了,则会发生promotion failed,触发老年代的回收。否则将存活对象复制到To Survivor区。

4、此时Eden区和From Survivor区的剩余对象均为垃圾对象,可直接抹掉回收。

此外,老年代如果采用的是CMS回收器,为了减少CMS Remark阶段的耗时,也有可能会触发一次YGC,这里不作展开。


# FGC是触发时机

下面4种情况,对象会进入到老年代中:

  • YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。
  • 经过多次YGC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
  • 动态年龄判定规则,不是看相同年龄,而是年龄从小到大累加,当加入某个年龄段后,累加和超过survivor区域TargetSurvivorRatio的时候,就从这个年龄段(包括)往上的年龄的对象进行晋升。其中:TargetSurvivorRatio也是个JVM参数,可配置,默认50%
  • 大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代, 直接在老年代中分配。

当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FGC(Major GC),FGC处理的区域同时包括新生代和老年代。除此之外,还有以下4种情况也会触发FGC:

  • 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发FGC。
  • 空间分配担保:在YGC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果小于,说明YGC是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发Full GC;如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC。
  • Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC。
  • System.gc() 或者Runtime.gc() 被显式调用时,触发FGC。

# GC会对程序产生影响的情况总结

不管YGC还是FGC,都会造成一定程度的程序卡顿(即Stop The World问题:GC线程开始工作,其他工作线程被挂起),即使采用ParNew、CMS或者G1这些更先进的垃圾回收算法,也只是在减少卡顿时间,而并不能完全消除卡顿。

那到底什么情况下,GC会对程序产生影响呢?根据严重程度从高到底,我认为包括以下4种情况:

  • FGC过于频繁:FGC通常是比较慢的,少则几百毫秒,多则几秒,正常情况FGC每隔几个小时甚至几天才执行一次,对系统的影响还能接受。但是,一旦出现FGC频繁(比如几十分钟就会执行一次),这种肯定是存在问题的,它会导致工作线程频繁被停止,让系统看起来一直有卡顿现象,也会使得程序的整体性能变差。
  • YGC耗时过长:一般来说,YGC的总耗时在几十或者上百毫秒是比较正常的,虽然会引起系统卡顿几毫秒或者几十毫秒,这种情况几乎对用户无感知,对程序的影响可以忽略不计。但是如果YGC耗时达到了1秒甚至几秒(都快赶上FGC的耗时了),那卡顿时间就会增大,加上YGC本身比较频繁,就会导致比较多的服务超时问题。
  • FGC耗时过长:FGC耗时增加,卡顿时间也会随之增加,尤其对于高并发服务,可能导致FGC期间比较多的超时问题,可用性降低,这种也需要关注。
  • YGC过于频繁:即使YGC不会引起服务超时,但是YGC过于频繁也会降低服务的整体性能,对于高并发服务也是需要关注的。

其中,「FGC过于频繁」和「YGC耗时过长」,这两种情况属于比较典型的GC问题,大概率会对程序的服务质量产生影响。剩余两种情况的严重程度低一些,但是对于高并发或者高可用的程序也需要关注。


# 垃圾回收相关问题

为什么会有新生代?

如果不分代,所有对象全部在一个区域,每次GC都需要对全堆进行扫描,存在效率问题。分代后,可分别控制回收频率,并采用不同的回收算法,确保GC性能全局最优。

为什么新生代会采用复制算法?

新生代的对象朝生夕死,大约90%的新建对象可以被很快回收,复制算法成本低,同时还能保证空间没有碎片。虽然标记整理算法也可以保证没有碎片,但是由于新生代要清理的对象数量很大,将存活的对象整理到待清理对象之前,需要大量的移动操作,时间复杂度比复制算法高。

为什么新生代需要两个Survivor区?

为了节省空间考虑,如果采用传统的复制算法,只有一个Survivor区,则Survivor区大小需要等于Eden区大小,此时空间消耗是8 * 2,而两块Survivor可以保持新对象始终在Eden区创建,存活对象在Survivor之间转移即可,空间消耗是8+1+1,明显后者的空间利用率更高。

新生代的实际可用空间是多少?

YGC后,总有一块Survivor区是空闲的,因此新生代的可用内存空间是90%。在YGC的log中或者通过 jmap -heap pid 命令查看新生代的空间时,如果发现capacity只有90%,不要觉得奇怪。

Eden区是如何加速内存分配的?

HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread Local Allocation Buffers)。

由于Eden区是连续的,因此bump-the-pointer在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而加快内存分配速度。

TLAB技术是对于多线程而言的,在Eden中为每个线程分配一块区域,减少内存分配时的锁冲突,加快内存分配速度,提升吞吐量。


# 垃圾收集器

图片

  • Serial(串行) 收集器

  • CMS (Concurrent Mark Sweep)收集器:CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。标记-清除算法。

  • Parallel:其实就是 Serial 收集器的多线程版本,新生代采用复制算法,老年代采用标记-整理算法。Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。

  • G1 (Garbage-First) 收集器:

    • 分代收集:虽然 G1 可以不需要其他收集器配合而独立管理整个 GC 堆,但是它能够采用不同的收集算法去处理新创建的对象和已经存活一段时间,熬过多次 GC 的对象。
    • 空间整合:G1 整体是基于标记-整理算法。所以运行期间不会产生内存碎片,收集后能提供规整的可用内存。在分配大对象时不会因为无法找到连续内存而触发下一次 GC。
    • 对堆的划分:G1 之前的收集器对内存的收集都是新生代或者老年代,而 G1 收集器中 Java 堆的内存布局就和其他收集器有很大区别,他们将整个堆划分为多个大小相等的区域(Region),虽然还有概念上的新生代和老年代,但是他们之间已经不存在物理隔离了,他们都是一部分 Region(不需要连续)的集合。
    • 可预测停顿:G1 除了追求低停顿以外,还能建立可预测时间模型,能让使用者明确指定在一个长度为M 毫秒的时间片段内,消耗在垃圾收集上的时间不超过 N 毫秒。G1 跟踪每个 Region 里面的垃圾的价值(回收的内存大小和时间的比值)大小,在后台维护一个优先列表,每次优先回收价值最大的 Region,这也是可预测停顿的实现原理。

SerialGC(串行回收器),最古老的一种,单线程执行,适合单CPU场景。

ParNew(并行回收器),将串行回收器多线程化,适合多CPU场景,需要搭配老年代CMS回收器一起使用。

ParallelGC(并行回收器),和ParNew不同点在于它关注吞吐量,可设置期望的停顿时间,它在工作时会自动调整堆大小和其他参数。

G1(Garage-First回收器),JDK 9及以后版本的默认回收器,兼顾新生代和老年代,将堆拆成一系列Region,不要求内存块连续,新生代仍然是并行收集。

上述回收器均采用复制算法,都是独占式的,执行期间都会Stop The World.


# 垃圾回收算法

参考文章 (opens new window)

  • 标记清除算法: 该算法分为“标记”和“清除”阶段:首先比较出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。 逻辑简单,但是标记和清除都比较低效,容易导致内存中产生大量不连续的小的内存区域。

  • 复制算法:为解决标记清除算法的缺陷。将整个内存区域划分为两个大小相同的区域,同时期只在一个区域存储数据。执行垃圾回收时将不能被回收的对象拷贝到另一块区域,之前的区域全部清除。该算法逻辑简单,执行高效但是浪费空间。商用 JVM 中一般使用复制算法回收【新生代】的内存空间。Eden80% + Survivor(from)10% + Survivor(to)10%

  • 标记整理算法:复制算法对于对象存活率低的情况很高效,但是当对象存活率很高比如【老年代】,这种算法就不能使用复制算法了。标记整理算法的第一个阶段也是标记可以被回收的对象,但是第二个阶段是将活着的对象整理到成连续的区域。

商业 JVM 中一般采用【分代收集算法】,将内存区域进行细分,采用不同的算法进行垃圾回收。


# 调优工具

# JDK工具

这些命令在 JDK 安装目录下的 bin 目录下。

  • jps (JVM Process Status): 类似 UNIX 的 ps 命令。用户查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
  • jstat( JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;
  • jinfo (Configuration Info for Java) : Configuration Info forJava,显示虚拟机配置信息;
  • jmap (Memory Map for Java) :生成堆转储快照;
  • jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
  • jstack (Stack Trace for Java):生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。

jinfo -flag name vmid :输出对应名称的参数的具体值。比如输出 MaxHeapSize、查看当前 jvm 进程是否开启打印 GC 日志 ( -XX:PrintGCDetails :详细 GC 日志模式,这两个都是默认关闭的)。

C:\Users\itzhouq>jinfo  -flag MaxHeapSize 17340
-XX:MaxHeapSize=2124414976
C:\Users\itzhouq>jinfo  -flag PrintGC 17340
-XX:-PrintGCCopy to clipboardErrorCopied

使用 jinfo 可以在不重启虚拟机的情况下,可以动态的修改 jvm 的参数。尤其在线上的环境特别有用,请看下面的例子:jinfo -flag [+|-]name vmid 开启或者关闭对应名称的参数。

C:\Users\itzhouq>jinfo  -flag  PrintGC 12345
-XX:-PrintGC

C:\Users\itzhouq>jinfo  -flag  +PrintGC 12345

C:\Users\itzhouq>jinfo  -flag  PrintGC 12345
-XX:+PrintGC

jmap(Memory Map for Java)命令用于生成堆转储快照。

示例:将指定应用程序的堆快照输出到桌面。后面,可以通过 jhat、Visual VM 等工具分析该堆文件。

C:\Users\itzhouq>jmap -dump:format=b,file=C:\Users\itzhouq\Desktop\heap.hprof 17340
Dumping heap to C:\Users\itzhouq\Desktop\heap.hprof ...
Heap dump file created

jhat 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果。

jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合.

生成线程快照的目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者在等待些什么资源。

# 可视化工具

  • JConsole:Java 监视与管理控制台

JConsole 是基于 JMX 的可视化监视、管理工具。可以很方便的监视本地及远程服务器的 java 进程的内存使用情况。你可以在控制台输出console命令启动或者在 JDK 目录下的 bin 目录找到jconsole.exe然后双击启动。

JConsole 可以显示当前内存的详细信息。不仅包括堆内存/非堆内存的整体信息,还可以细化到 eden 区、survivor 区等的使用情况,如下图所示。

点击右边的“执行 GC(G)”按钮可以强制应用程序执行一个 Full GC。

  • Visual VM:多合一故障处理工具

VisualVM 提供在 Java 虚拟机 (Java Virutal Machine, JVM) 上运行的 Java 应用程序的详细信息。在 VisualVM 的图形用户界面中,您可以方便、快捷地查看多个 Java 应用程序的相关信息。Visual VM 官网:https://visualvm.github.io/ 。Visual VM 中文文档:https://visualvm.github.io/documentation.html。


# JVM问题实战

# YGC问题排查案例

参考文章 (opens new window)

  • 问题出现:版本上线 -- 收到告警 -- 查看监控 -- 超时量陡增 -- 每分钟上千次接口超时。
  • 生产处理:检查监控,发现 YongGC 时间过长,由于YongGC期间程序会 STW,而上游服务的超时时间在 1S 之内,是因为YGC耗时过长引发了服务大面积超时。按照GC问题的常规排查流程,我们立刻摘掉了一个节点,然后通过以下命令dump了堆内存文件用来保留现场。jmap -dump:format=b,file=heap pid。最后对线上服务做了回滚处理,回滚后服务立马恢复了正常,接下来就是排查和修复过程。
  • 确认 JVM 配置:用下面的命令,我们再次检查了JVM的参数 ps aux | grep "applicationName=adsearch"。可以看到堆内存,新生代和老年代内存,垃圾回收器种类。然后使用 jmap -heap pid 查到 Eden 区,S0(0.2G) 区,S1 区的大小。本次上线并未修改 JVM 相关参数,服务器请求量和平时差不多,猜测此次问题大概率跟上线代码相关。
  • 检查代码:一次 YGC 主要包括从 GC Root 扫描对象,对存活对象进行标注。将存活对象复制到 S1 区或者晋升到 Old 区。根据监控正常 S区使用量一直维持低水平(30M),上线后使用率波动,最高 0.2G占满。对比 YGC 耗时指标和 S 区使用率指标,发现正相关。猜测:应该是长生命周期的对象越来越多,导致标注和复制过程的耗时增加。程序的全局变量或者类静态变量是重点怀疑对象,但是查看本次上线代码没有发现引入此类对象。
  • 对 dump 的堆内存文件进行分析:使用 MAT 工具导入,通过 Dominator Tree 视图查看所有大对象。立马发现有个类占用空间很大,是为了兼容用于新旧类转换的,类中存在大量静态 HashMap。但是分析后发现这个类的所有静态变量全部在类加载的时候初始化了,虽然会占用 100M的内存,但是之后不会再新增数据,经过多轮 YGC 最终晋升到老年代中,并且这个类很早之前就上线了,排除了这个可疑点。
  • 分析 YGC 处理 Reference 的耗时:查询网上案例,原因集中在两类:1 是对存活对象标注时间过长,2 是长周期对象积累过多。对于第一个问题使用 -XX:+PrintReferenceGC可以看到不同类型的 reference 处理耗时都很短,因此又排除了此项因素。
  • 再回到长周期对象进行分析:再次分析 MAT 中的大对象,发现有个大对象 ConfigService 类中 ArrayList 变量中竟然包含了 270W 个对象,而且大部分都是相同元素。分析发现这个类在第三方中间件中,但是被架构进行二次改造,每次调用 getConfig 方法都会往 List 中添加元素,并且未做去重处理。所以 bug 原因是二次改造中间件导致,上线前发布到中央仓库,而且公司中间件是通过 super-pom 方式统一维护,业务无感知。
  • 解决方案:为了快速验证 YGC 耗时过长是因为此问题导致的,我们在一台服务器上直接用旧版的中间件包进行替换,然后重启服务,观察 20 分钟左右,YGC 恢复正常。而后通知修改架构的问题,重新发布 super-pom,彻底解决这个问题。

通过上面这个案例,可以看到YGC问题其实比较难排查。相比FGC或者OOM,YGC的日志很简单,只知道新生代内存的变化和耗时,同时dump出来的堆内存必须要仔细排查才行。


# FGC问题排查案例「待完善」

参考文章 (opens new window)


# CPU占满情况分析「待完善」

比如代码中的死循环。

通过 top 命令查看内存占用。得到内存占用过多的进程的 pid1。

通过执行 top -Hp <pid1> 查看 Java 线程情况。得到线程异常的 pid2。

通过执行printf '%x' pid2获取 16 进制线程 id,用于 dump信息查询,结果为 803a

最后执行jstack pid1 | grep -A 20 803a来查看详细的dump信息。

这里dump信息直接定位出了问题方法以及代码行,这就定位出了 CPU 占满的问题。


# 死锁解决「待完善」

死锁会导致耗尽线程资源,占用内存,表现就是内存占用升高,CPU 不一定会飙升(看场景决定),如果是直接 new 线程,会导致 JVM 内存被耗尽,报无法创建线程的错误,这也是体现了使用线程池的好处。

通过ps -ef|grep java命令找出 Java 进程 pid,执行jstack pid 即可出现 java 线程堆栈信息,这里发现了 5 个死锁,我们只列出其中一个,很明显线程pool-1-thread-2锁住了0x00000000f8387d88等待0x00000000f8387d98锁,线程pool-1-thread-1锁住了0x00000000f8387d98等待锁0x00000000f8387d88,这就产生了死锁。