文章目录
- 1、JVM垃圾回收机制
- 1.1 针对的内存区域
- 1.2 怎么判断对象是否可以被回收?
- 1.3 垃圾收集算法
- 1.3.1 **标记-清除(Mark-Sweep)**
- 1.3.2 复制(Copying)
- 1.3.3 标记-整理(Mark-Compact)
- 1.3.4 分代(Generation-based)
- 1.3.5 三色标记法
- 2、垃圾收集器
- 2.1 Serial(串行)垃圾回收器:(复制)
- 2.2 ParNew垃圾回收器:(复制)
- 2.3 **Parallel Scavenge垃圾回收器:**(复制)
- 2.4 Serial Old垃圾回收器:(标记-压缩)
- **Serial + Serial Old:针对几兆-几十兆的堆内存**
- 2.5 Parallel Old垃圾回收器:(标记-压缩)
- jdk1.8默认垃圾回收器:**Parallel Scavenge + Parallel Old:(ps+po):针对几十兆-上百兆1G的堆内存**
- 2.6 CMS(Concurrent Mark Sweep)垃圾回收器:
- **ParNew+CMS:(1.5,1.6,1.7)**针对几十G的堆内存
- 2.7 G1(Garbage-First)垃圾回收器:
- **(1.7诞生但不成熟,1.8可使用)**
- 2.8 ZGC(Z Garbage Collector)
- 2.9 Shenandoah:
- 开源领域贡献**(jdk12引入)**
- 3、JVM调优
- 3.1 基础命令
- 3.1.1 jps 查看所有进程
- 3.1.2 jinfo (进程号) 查看对应线程的所有线程
- 3.1.3 jstat 查看统计信息(数据跟踪信息)
- 3.1.4 jstack (进程号) 跟踪该进程下所有线程
- 3.1.5 jmap **查看堆内存中哪些对象占用情况**
- 3.1.6 jmap jmap -dump:format=b,file=20240129.hprof (进程号) **产生堆内存存储文件**
- 3.1.7 top 查看当前系统中每个进程占用cpu和内存的信息
- 3.2 CPU飚高生产环境解决思路:
- 3.3 图形界面工具 arthas 阿里开源
- 3.3.1 :启动 arthas
- 3.3.2 help :查看arthas所有命令
- 3.3.3 查看 dashboard
- 3.3.4 thread 列出当前进程所有线程占用CPU和内存情况
- 3.3.5 jvm 查看该进程的各项参数 (类比 jinfo)
- 3.3.6 通过 jad 来反编译 UserController Class
- 3.3.8 `monitor` 监控方法的执行情况
- 3.3.9 `watch`:检测函数返回值
- 3.3.10 `trace`:根据路径追踪,并记录消耗时间
- 3.3.11 `tt`:时间隧道,记录多个请求
- 3.3.12 redefine 定义class
- 3.3.13 退出 arthas
- 3.4 项目中应用
1、JVM垃圾回收机制
概念:JVM垃圾回收机制是Java虚拟机管理内存的重要部分,其主要任务是自动检测并释放不再使用的对象所占用的内存空间。
1.1 针对的内存区域
-
堆内存(Heap)
:这是GC的主要工作区域,所有由new创建的对象都在这里分配内存。堆内存进一步划分为年轻代(Young Generation)和老年代(Old Generation)。
- 年轻代包括 Eden、Survivor0 和 Survivor1 区域(某些实现可能有变化),主要存储新创建的对象和短生命周期的对象。
- 老年代则存放经过多次新生代GC后仍存活下来的对象。
1.2 怎么判断对象是否可以被回收?
-
引用计数器法:为每个对象创建一个引用计数,有对象引用时+1,引用被释放时-1,当计数器为0时,就可以回收了。
在使用引用计数器法的系统中,每个Java对象实例都会有一个关联的引用计数器。当一个对象被创建时,其引用计数器初始化为1;每当有新的引用指向该对象时,引用计数器加1;而当某个引用离开作用域或赋值为null,即不再引用该对象时,引用计数器减1。
- 优缺点:
-
优点:
- 实现简单,实时性好,一旦计数器为0就能立即回收对象,无需等到特定时刻进行垃圾回收。
-
缺点:
-
无法处理循环引用问题。例如,如果两个对象互相引用对方但除此之外没有其他对象引用它们,引用计数器永远不会为0,导致这两个对象无法被正确回收,从而造成内存泄漏。
-
因为需要频繁更新计数器,在多线程环境下可能会引入同步开销。
-
对于大量琐碎的小对象,维护引用计数器的成本可能较高。
实际应用: 现代Java虚拟机并没有采用引用计数器算法作为主要的垃圾回收机制,而是采用了更复杂的如可达性分析(根搜索算法)、分代收集、并发标记清除以及压缩等技术来提高垃圾回收效率和准确性,并避免了引用计数器方法存在的问题。尽管如此,某些编程语言和环境中仍然使用引用计数器作为内存管理的一种策略。
早期版本的Python、苹果公司的Objective-C和Swift语言等使用或曾经使用引用计数器,但大多数现代高级语言的内存管理系统往往结合多种垃圾回收算法以解决引用计数器法无法处理循环引用等问题,确保更准确、高效的内存管理。
-
-
- 优缺点:
-
可达性分析算法:JVM的可达性分析算法是一种用于确定对象是否可被回收的垃圾收集机制。该算法基于“对象引用”的概念,通过一系列称为GC Roots的对象作为起始点,来判断一个对象在程序执行期间是否还可能被访问。
工作原理:
- GC Roots集合:
- GC Roots是指在Java虚拟机栈中的本地变量表中引用的对象(如正在执行的方法中的局部变量、参数等)。
- 方法区中静态变量引用的对象。
- 常量池中的引用。
- JNI(Java Native Interface)引用的对象。
- 所有线程对象持有的对象引用。
- 引用链(Reference Chain): 从这些GC Roots开始向下搜索,跟随对象之间的引用关系,形成一条条引用链。如果一个对象能够通过引用链直接或间接地从任意一个GC Roots节点到达,则认为这个对象是可达的,即它仍有可能被应用程序使用。
- 不可达对象判定: 如果一个对象无法通过任何引用链与任何GC Roots建立关联,则认为它是不可达的,即当前不再有任何活动线程可以通过正常途径访问到这个对象,因此可以被垃圾收集器标记为可回收。
- 回收过程: 在确定了哪些对象是不可达之后,垃圾收集器会在合适的时机对这些不可达对象占用的内存空间进行回收。
**可达性分析算法结合了分代收集策略和其他优化手段,在实际应用中极大地提高了JVM管理内存的效率和准确性。**随着技术的发展,JVM中的垃圾收集器也在不断演进,如HotSpot JVM就采用了多种不同的垃圾收集器实现,并且在可达性分析的基础上引入了如并发标记、压缩整理等更复杂的技术,以进一步降低停顿时间和提高吞吐量。
- GC Roots集合:
1.3 垃圾收集算法
1.3.1 标记-清除(Mark-Sweep)
最基础的垃圾回收算法,主要应用于Java虚拟机(JVM)中。该算法包括两个阶段:标记和清除。
垃圾回收器:
- 标记阶段(Mark) 在这个阶段,垃圾收集器会从一组称为GC Roots的对象开始,遍历整个对象图,所有能够从GC Roots直接或间接引用到的对象都会被标记为“存活”。GC Roots通常包括栈中的局部变量、静态变量、常量池引用等。
- 清除阶段(Sweep)在标记阶段结束后,垃圾收集器会遍历堆内存中的所有对象,将未被标记为“存活”的对象视为垃圾,并释放它们占用的内存空间。
优缺点分析:
优点:
- 实现相对简单,易于理解。
- 可以有效地回收不再使用的内存。
缺点:
- 清理后容易产生内存碎片,因为已清理的内存区域可能分散在堆内存的不同位置,这可能导致大对象无法找到连续的内存空间而不得不提前触发下一次垃圾回收。
- 效率问题,标记和清除过程都需要遍历整个堆内存,如果堆内存很大或者存活对象很多,可能会导致STW(Stop-The-World)停顿时间过长。
1.3.2 复制(Copying)
一种垃圾回收策略,主要用于解决内存碎片问题。
垃圾回收器:通常应用于年轻代的垃圾回收,如垃圾回收器Serial、ParNew和Parallel Scavenge。
工作原理: 复制算法将堆内存划分为两个或多个相同大小的区域,一般称为From空间和To空间(或者Eden区与Survivor区)。当对象创建时,首先分配到From空间。
- 标记阶段: 从一组GC Roots出发,遍历整个对象图,标记所有可达的对象为“存活”。
- 复制阶段: 将所有标记为“存活”的对象复制到另一个空白的内存区域(To空间),并按照特定顺序紧凑排列,这样可以确保To空间中的内存是连续的,没有碎片。
- 清除阶段: 清空已扫描完的From空间,此时From空间变为空白,等待下一次分配新对象。
- 交换角色: 在下一次垃圾回收时,From和To空间的角色互换,即上次作为To空间的那个区域成为新的From空间,用于存储新创建的对象,而原来的From空间则变为新的To空间。
优缺点分析:
优点:
- 避免了内存碎片的问题,因为每次只清理并重用一个完整的内存区域。
- 复制过程中可以进行内存整理,使对象在内存中连续存放,有利于提高内存访问速度。
缺点:
- 内存利用率相对较低,至少需要预留一半的内存空间供复制操作使用。
- 对于大对象或者生命周期较长的对象,频繁复制可能影响效率。
为了减少复制带来的额外开销,一些JVM实现会采用 Survivor 空间的设计,在多次新生代GC后仍存活的对象会被晋升到老年代,从而降低频繁复制的成本。同时,老年代通常采用其他的垃圾回收算法,例如标记-压缩、分代收集等。
1.3.3 标记-整理(Mark-Compact)
一种垃圾回收算法,主要用于解决标记-清除算法遗留的内存碎片问题。该算法结合了“标记”和“整理”两个步骤。
垃圾回收器:通常应用于老年代的垃圾回收,如垃圾回收器Parallel Old、CMS和G1。
1. 标记阶段(Mark) 与标记-清除算法类似,首先从一组称为GC Roots的对象开始遍历整个对象图,标记所有可达的对象为“存活”。未被标记的对象即被视为垃圾。
2. 整理阶段(Compact) 不同于标记-清除直接删除未被标记的对象,标记-整理算法在标记完成后,会将所有存活的对象向一端移动,并对不连续的空白区域进行合并。这样可以使得存活对象紧凑地排列在一起,而不再使用中的内存空间则聚集到一起,形成一个或多个大的连续空闲区域。
优缺点分析:
优点:
- 有效解决了内存碎片问题,使得分配大对象时更容易找到连续的内存空间。
- 避免了由于内存碎片导致的频繁GC。
缺点:
- 执行整理操作需要额外的时间成本,因此可能导致垃圾回收过程中的停顿时间更长。
- 对于堆内存较大的情况,移动大量对象可能影响性能。
在Java虚拟机中,如CMS收集器的老年代部分采用的就是基于标记-清除算法,而G1垃圾收集器虽然不是严格意义上的标记-整理算法,但在其混合回收阶段也会进行类似于整理的操作,以达到消除内存碎片的目的。此外,ZGC和Shenandoah等新一代垃圾收集器也采用了不同的手段来避免内存碎片并减少STW(Stop-The-World)停顿时间。
1.3.4 分代(Generation-based)
分代(Generation-based)垃圾回收策略是Java虚拟机中广泛采用的一种内存管理方式,其核心思想是根据对象的生命周期特性将堆内存划分为不同的区域或“代”进行管理。通常JVM将堆内存分为年轻代(Young Generation)、老年代(Old Generation)和永久代/元空间(PermGen/Metaspace)。
1. 年轻代(Young Generation)
- 包括Eden区、Survivor 0区和Survivor 1区(某些实现可能有所不同)。
- 新创建的对象首先会被分配到年轻代的Eden区。
- 当Eden区满了或者达到一定年龄阈值后,会触发年轻代的垃圾回收,即Minor GC,主要使用复制算法(Copying)。
2. 老年代(Old Generation)
- 存放经过多次Minor GC仍然存活下来的对象。
- 老年代的空间一般较大,且对象生命周期较长,因此垃圾回收频率较低,通常采用标记-清除(Mark-Sweep)或标记-压缩(Mark-Compact)等算法。
3. 永久代/元空间(PermGen/Metaspace)
- 在Java 8之前,用于存储类信息、常量池、方法数据等非实例部分的数据,被称为永久代(PermGen)。
- 自Java 8开始,永久代被移除,这些数据被转移到了元空间(Metaspace),并且元空间的大小并不固定,可以动态扩展至物理内存大小限制。
Region-based收集器(如G1)
- G1垃圾收集器对堆内存的划分更为精细,它将整个堆内存划分为多个相同大小的区域(Regions)。
- 每个区域都可以作为年轻代或老年代的一部分,可以根据需要动态调整角色。
- G1在执行垃圾回收时,并不严格区分年轻代与老年代的回收,而是优先回收收益最大的区域,以降低停顿时间和提高整体效率。
总的来说,分代垃圾回收策略利用了大多数对象生命周期较短的特点,通过针对性地在不同代之间执行垃圾回收,优化了内存管理和垃圾回收性能。而Region-based垃圾回收策略在此基础上进一步细化了内存管理单元,提供了更灵活、高效的垃圾回收机制。
1.3.5 三色标记法
在Java虚拟机(JVM)的垃圾回收(Garbage Collection, GC)过程中,三色标记法是一种经典的垃圾检测算法,用于识别哪些对象是可达的(即活跃的),哪些对象是不可达的(即垃圾)。以下是三色标记法在JVM垃圾回收中的基本使用流程:
- 初始化阶段:
- 所有对象初始时均视为白色,表示对象是否可达未知。
- 根节点扫描:
- 从一组被称为GC Roots的对象开始,这些对象包括但不限于:Java栈帧中的本地变量、静态变量、JNI引用等。
- 将所有GC Roots引用的对象标记为灰色。
- 并发标记阶段:
- 从灰色对象开始,遍历它们的所有引用字段,将所引用的对象从白色标记为灰色。
- 被标记为灰色的对象进入灰色队列,等待进一步的处理。
- 递归标记:
- 从灰色队列中取出对象,将其本身标记为黑色,表示这个对象及其从根可达的所有子对象都已经标记完毕。
- 然后继续遍历该对象的所有引用,将新发现的白色对象标记为灰色,并放入灰色队列。
- 终止条件:
- 当灰色队列为空时,所有可达对象已经被标记为黑色,而白色对象则被认为是不可达的垃圾对象。
- 并发清除或重定位阶段:
- 在某些GC实现中,如G1、ZGC、Shenandoah,可能会在标记过程完成后,通过并发的方式清除或移动白色的垃圾对象,释放对应的内存空间。
- 安全点与同步:
- 在并发标记的过程中,为了避免对象引用关系发生变化导致漏标或误标,垃圾回收器会在适当的“安全点”暂停应用线程,进行所谓的“冻结”或“快照”,确保标记的正确性。
- 浮动垃圾与增量更新:
- 由于并发标记期间可能有新的垃圾对象产生,这些在标记过程中产生的垃圾称为“浮动垃圾”。通常情况下,这些垃圾将在下一次垃圾回收周期中被处理。
三色标记法能有效避免对象遗漏和重复标记的问题,通过精确地追踪对象的可达性状态,使得JVM能够高效地回收内存,保持程序运行时有足够的可用内存空间。在具体的垃圾回收器实现中,如CMS、G1等,会对三色标记法进行变种或扩展,以适应各自的特点和优化目标。
2、垃圾收集器
Java垃圾回收器(Garbage Collector, GC)是Java虚拟机(JVM)的重要组件,用于自动管理内存资源,减轻程序员手动管理内存的负担。它主要用于回收堆内存中不再被任何引用变量所指向的对象所占用的空间,以便释放这部分空间供后续对象分配使用。
在Java的发展历程中,随着JVM技术的演进,出现了多种不同的垃圾回收器,以适应不同的应用场景和性能需求。以下是一些Java中的关键垃圾回收器:
2.1 Serial(串行)垃圾回收器:(复制)
- 适用场景:单CPU环境或客户端应用,堆内存较小。
- 特性:它是JVM中最基本的垃圾回收器,只使用一个线程进行垃圾回收,采用“Stop-the-World”模型,在进行垃圾回收时会暂停所有用户线程。新生代采用复制算法,将内存划分为 Eden、Survivor 区域。
- 新生代 Serial:
- 初始标记(Initial Mark):STW,仅标记出直接可达的对象。
- 复制(Copying):Eden区存活对象复制到幸存区,清空Eden和已用过的幸存区。
- 持续标记(Applying Remark):并非所有回收器都有此阶段,但Serial在必要时会进一步标记。
- 并发清除(Concurrent Reset):准备下一个年轻代回收周期。
新生代:老年代 = 1:2
新生代使用复制-算法,本身也分三个区,Eden、To Survivor、From Survivor。默认8:1:1
- Eden+From Survivor存活的对象放入To Survivor
- 清空Eden+From Survivor分区
- From Survivor和To Survivor分区交换
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄都+1,当年龄达到15(默认的)时,升级为老年代,大的对象直接放入老年代。
老年代这边,当空间占用达到某一个阈值之后,触发Full GC,此时一般使用标记整理算法。
对象优先分配到新生代的Eden区,Eden区空间不够时,进行Minor GC,还不够,则分配到老年代。
Minor GC非常频繁,回收速度也快。大对象(需要大量连续内存空间的对象)直接进入老年代。
2.2 ParNew垃圾回收器:(复制)
- 适用场景:多CPU服务器环境,与CMS配合使用。
- 特性:ParNew是Serial GC的多线程版本,同样用于新生代的垃圾回收。它可以利用多核优势,减小STW的时间,但它仍然是一个Stop-the-World的收集器,并且默认与CMS配合使用。
2.3 Parallel Scavenge垃圾回收器:(复制)
- 适用场景:关注吞吐量的应用。
- 特性:同样是新生代并行收集器,但其主要目标是最大化应用的吞吐量(程序运行时间/(程序运行时间+垃圾收集时间))。提供了自适应调整策略,允许根据运行时情况动态调整堆大小和其他参数。
2.4 Serial Old垃圾回收器:(标记-压缩)
-
适用场景:作为CMS或G1的备用老年代收集器,或者用于Client模式下的JVM。
-
特性:Serial Old是Serial GC的老年代版本,采用标记-压缩算法,同样采用单线程进行垃圾回收。
-
Serial + Serial Old:针对几兆-几十兆的堆内存
2.5 Parallel Old垃圾回收器:(标记-压缩)
-
适用场景:关注吞吐量并且内存较大的应用。
-
特性:Parallel Old是Parallel Scavenge的老年代版本,使用多线程进行老年代的垃圾回收,采用标记-压缩算法,目的是提高大堆内存下系统的吞吐量。
-
jdk1.8默认垃圾回收器:Parallel Scavenge + Parallel Old:(ps+po):针对几十兆-上百兆1G的堆内存
2.6 CMS(Concurrent Mark Sweep)垃圾回收器:
-
适用场景:实时性要求高,对响应时间敏感的服务端应用。
-
特性:CMS是一种低延迟、并发的垃圾回收器,主要用于老年代的回收。它分为四个阶段:初始标记、并发标记、重新标记和并发清除,尝试在应用不完全停止的情况下完成大部分垃圾回收工作。
-
**ParNew+CMS:(1.5,1.6,1.7)**针对几十G的堆内存
-
CMS 工作流程
- 初始标记(Initial Mark):STW,标记从根集合直接可达的老年代对象。
- 并发标记(Concurrent Mark):非STW,标记整个堆中的存活对象。
- 重新标记(Remark):STW,修正并发标记阶段由于应用线程继续运行而导致的变动。
- 并发清除(Concurrent Sweep):非STW,清除未标记的对象。
- 并发重置(Concurrent Reset):准备下一次GC。
2.7 G1(Garbage-First)垃圾回收器:
-
适用场景:大规模应用,大堆内存,对停顿时间有一定要求。
-
特性:G1是一种整体化的垃圾回收器,将整个堆划分为多个大小固定的区域。针对大型堆设计的一种分代收集器,能够进行并行的全局并发标记和局部并发清理。它实现了预测性停顿时间和部分并发收集,目标是简化管理和提供一致的性能表现。
-
(1.7诞生但不成熟,1.8可使用)
-
G1 工作流程
- 初始标记(Initial Mark):STW,标记所有GC Roots直接可达的对象。
- 并发标记(Concurrent Marking):非STW,全局标记阶段,构建记忆集并标记所有可达对象。
- 最终标记(Final Mark or Remark):STW,处理并发标记阶段结束后剩余的引用变化。
- 筛选回收(Cleanup/Copy):STW,基于优先级回收一部分Region(内存分区),包含混合的年轻代和老年代对象。
2.8 ZGC(Z Garbage Collector)
- 适用场景:超大堆内存(TB级别),对极低延迟有极高要求的场景。
- 特性:ZGC的设计目标是在任何堆大小下都能保证GC暂停时间不超过10毫秒。它使用了颜色指针、读屏障、并发标记、并发整理等一系列创新技术,大幅度减少了GC开销。
- 甲骨文官方支持**(jdk11引入)**
- 在JDK 19中,经过长期的测试和完善,ZGC已经被设定为默认垃圾回收器。
2.9 Shenandoah:
-
适用场景:类似ZGC,追求低延迟,适用于大型应用和云环境。
-
特性:Shenandoah也致力于降低GC停顿时间,引入了并发的“跨代引用处理”和“并发压缩”技术,可以在不显著增加处理器负载的情况下,实现实质上的并发清理和压缩,从而极大地缩短了STW时间。
-
开源领域贡献**(jdk12引入)**
3、JVM调优
3.1 基础命令
3.1.1 jps 查看所有进程
Win10 系统
Linux 系统:
3.1.2 jinfo (进程号) 查看对应线程的所有线程
这里我们查看一下21448这个进程的信息:jinfo 21448
在VM Flags中我们可以看到JVM的配置信息
使用 jinfo -flags 可以仅查看JVM配置信息
这里我们可以看到 JDK17 默认使用的垃圾回收器是G1
而 JDK8 默认使用的垃圾回收器是Parallel Scavenge+Parallel Old
Linux:
jinfo -flags [进程号] 查看JVM参数
常用参数
- -Xms2g:初始化推大小为 2g;
- -Xmx2g:堆最大内存为 2g;
- -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
- -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
- –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
- -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
- -XX:+PrintGC:开启打印 gc 信息;
- -XX:+PrintGCDetails:打印 gc 详细信息。
3.1.3 jstat 查看统计信息(数据跟踪信息)
- 我们这里专门查看21448这个进程的GC信息:jstat -gc 21448
- 还可以增加一个时间参数(毫秒),查看这个时间参数的频率下的GC信息:jstat -gc 21448 500
Linux:jstat -gc 3076
3.1.4 jstack (进程号) 跟踪该进程下所有线程
jstack 21448
3.1.5 jmap 查看堆内存中哪些对象占用情况
-
jmap -histo (进程号)
-
jmap -histo 21448 | head -20 :20表示取前20行
仅Linux系统中可使用head命令,在Windows 10系统中,由于没有内置的
head
命令,可以通过PowerShell或组合使用其他Windows命令来模拟查看文件头几行的功能。先将
jmap
的输出重定向到临时文件,然后使用more
或find
命令查看前20行:jmap -histo:live 21448 > temp.txt more +1 temp.txt
在
more
命令中,+1
是为了跳过标题行(如果有的话),然后按回车键逐页查看,直到看到前20行。注意:JMAP命令不能再生产环境直接执行,会STW拿出当前进程所有对象信息
Linux:
3.1.6 jmap jmap -dump:format=b,file=20240129.hprof (进程号) 产生堆内存存储文件
导出文件之后,可以使用jdk自带的文件分析工具
图形界面工具jvisualvm
- 可以直接读取jmap导出的文件分析,也可以远程连接LInux服务器分析排查问题
- 图形界面中分析出问题
- 压测环境中观察出问题
- 机器做了负载均衡,摘出其中一台,导出其堆存储文件
- TCP down复制一份到生产环境,另一份到测试环境
3.1.7 top 查看当前系统中每个进程占用cpu和内存的信息
top -Hp 进程号 查看当前进程中所有线程的CPU和内存占用情况
3.2 CPU飚高生产环境解决思路:
- 初步排查:
- 使用
top
或htop
命令查看当前系统中CPU占用最高的进程(PID)。 - 通过
ps
、pgrep
或者直接在top
结果中找到与Java相关的进程。
- 使用
- 定位高CPU消耗线程:
- 获取到Java进程的PID后,可以使用
top -H -p PID
命令来查看该进程中各个线程的资源占用情况。 - 或者使用
pidstat
命令监视指定进程及其线程的CPU使用情况。
- 获取到Java进程的PID后,可以使用
- 堆栈信息分析:
- 使用
jstack
命令获取Java进程的堆栈跟踪信息:jstack PID > thread_dump.txt
,这会输出所有线程的状态和调用堆栈。 - 分析堆栈信息,找出那些处于RUNNABLE状态且没有阻塞点的线程,它们可能正在执行大量计算或陷入死循环。
- 使用
- 日志分析:
- 查看对应时间段内的应用程序日志,根据线程ID或者相关上下文信息,查找是否有异常行为或者频繁执行的操作。
- 性能工具辅助:
- 可以利用JMX(Java Management Extensions)监控Java虚拟机内部状态,包括线程池、内存使用等。
- 使用
VisualVM
、JProfiler
或其他Java性能分析工具进一步深入分析CPU热点和瓶颈。
- 临时缓解措施:
- 如果能够确定是某个模块的问题,且存在快速修复的可能性,则可尝试重启服务或者调整配置暂时降低负载。
- 在不影响业务的情况下,可以考虑使用操作系统层面的限制手段,如设置cgroups限制Java进程的CPU使用配额。
- 长期解决方案:
- 根据分析结果优化代码逻辑,例如消除不必要的循环、减少同步开销、优化数据库查询等。
- 调整架构设计,增加缓存、异步处理、负载均衡等策略以分散压力。
- 持续监控与预防:
- 建立健全的监控体系,确保能够实时感知此类问题的发生,并及时预警。
- 对于关键应用,定期进行性能测试和压测,提前发现潜在性能瓶颈。
3.3 图形界面工具 arthas 阿里开源
下载地址:https://arthas.aliyun.com/
简介:Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
3.3.1 :启动 arthas
直接通过java -jar 启动arthas的jar包文件
选择应用 java 进程:jvm-test 进程是第 1 个,则输入 1,再输入回车/enter
。Arthas 会 attach 到目标进程上,并输出日志:
3.3.2 help :查看arthas所有命令
- JVM 相关:
- dashboard - 当前系统的实时数据面板
- getstatic - 查看类的静态属性
- heapdump - dump java heap, 类似 jmap 命令的 heap dump 功能
- jvm - 查看当前 JVM 的信息
- logger - 查看和修改 logger
- mbean - 查看 Mbean 的信息
- memory - 查看 JVM 的内存信息
- ognl - 执行 ognl 表达式
- perfcounter - 查看当前 JVM 的 Perf Counter 信息
- sysenv - 查看 JVM 的环境变量
- sysprop - 查看和修改 JVM 的系统属性
- thread - 查看当前 JVM 的线程堆栈信息
- vmoption - 查看和修改 JVM 里诊断相关的 option
- vmtool - 从 jvm 里查询对象,执行 forceGc
- class/classloader 相关:
- classloader - 查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource
- dump - dump 已加载类的 byte code 到特定目录
- jad - 反编译指定已加载类的源码
- mc - 内存编译器,内存编译
.java
文件为.class
文件 - redefine - 加载外部的
.class
文件,redefine 到 JVM 里 - retransform - 加载外部的
.class
文件,retransform 到 JVM 里 - sc - 查看 JVM 已加载的类信息
- sm - 查看已加载类的方法信息
- monitor/watch/trace 相关:
- monitor - 方法执行监控
- stack - 输出当前方法被调用的调用路径
- trace - 方法内部调用路径,并输出方法路径上的每个节点上耗时
- tt - 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
- watch - 方法执行数据观测
- 基础命令
- base64 - base64 编码转换,和 linux 里的 base64 命令类似
- cat - 打印文件内容,和 linux 里的 cat 命令类似
- cls - 清空当前屏幕区域
- echo - 打印参数,和 linux 里的 echo 命令类似
- grep - 匹配查找,和 linux 里的 grep 命令类似
- help - 查看命令帮助信息
- history - 打印命令历史
- keymap - Arthas 快捷键列表及自定义快捷键
- pwd - 返回当前的工作目录,和 linux 命令类似
- quit - 退出当前 Arthas 客户端,其他 Arthas 客户端不受影响
- reset - 重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类
- session - 查看当前会话的信息
- stop - 关闭 Arthas 服务端,所有 Arthas 客户端全部退出
- tee - 复制标准输入到标准输出和指定的文件,和 linux 里的 tee 命令类似
- version - 输出当前目标 Java 进程所加载的 Arthas 版本号
3.3.3 查看 dashboard
输入 dashboard,按回车/enter
,会展示当前进程的信息,按ctrl+c
可以中断执行。
可以看到进程里面有哪些线程,每个线程的状态、吃CPU的情况等。
在Memory中我们可以看到内存的占用情况:
- 新生代:eden_space、survivor_space
- 老年代:tenured_space
- 非堆内存:nonheap
3.3.4 thread 列出当前进程所有线程占用CPU和内存情况
thread pid
会打印线程 ID pid 的栈,通常pid 1是 main 函数的线程。
thread -b 寻找死锁
3.3.5 jvm 查看该进程的各项参数 (类比 jinfo)
我们在garbage collectors(GC)里面可以看到这里垃圾回收的统计情况
- Copy(复制算法)用了49次,耗时506
- MarkSweepCompact(标记整理算法)用了3次,耗时795
3.3.6 通过 jad 来反编译 UserController Class
3.3.8 monitor
监控方法的执行情况
监控com.example.jvm.controller.TestController
类的 “getStr”方法 ,并且每5S更新一次状态。
monitor com.example.jvm.controller.TestController getStr -c 5
监控的维度说明
监控项 | 说明 |
---|---|
timestamp | 时间戳 |
class | Java类 |
method | 方法(构造方法、普通方法) |
total | 调用次数 |
success | 成功次数 |
fail | 失败次数 |
rt | 平均耗时 |
fail-rate | 失败率 |
3.3.9 watch
:检测函数返回值
方法执行数据观测,让你能方便的观察到指定方法的调用情况。
能观察到的范围为:返回值
、抛出异常
、入参
,通过编写OGNL 表达式进行对应变量的查看。
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
express | 观察表达式 |
condition-express | 条件表达式 |
[b] | 在方法调用之前观察before |
[e] | 在方法异常之后观察 exception |
[s] | 在方法返回之后观察 success |
[f] | 在方法结束之后(正常返回和异常返回)观察 finish |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[x:] | 指定输出结果的属性遍历深度,默认为 1 |
这里重点要说明的是观察表达式,观察表达式的构成主要由ognl 表达式组成,所以你可以这样写"{params,returnObj}",只要是一个合法的 ognl 表达式,都能被正常支持。
特别说明
- watch 命令定义了4个观察事件点,即 -b 方法调用前,-e 方法异常后,-s 方法返回后,-f 方法结束后
- 4个观察事件点 -b、-e、-s 默认关闭,-f 默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出
- 这里要注意方法入参和方法出参的区别,有可能在中间被修改导致前后不一致,除了 -b 事件点 params 代表方法入参外,其余事件都代表方法出参
- 当使用 -b 时,由于观察事件点是在方法调用前,此时返回值或异常均不存在
通过watch命令可以查看函数的参数/返回值/异常信息。
- 查看方法执行的返回值
watch com.example.jvm.controller.UserController list returnObj
3.3.10 trace
:根据路径追踪,并记录消耗时间
对方法内部调用路径进行追踪,并输出方法路径上的每个节点上耗时。
简介:
trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
观察表达式的构成主要由ognl 表达式组成,所以你可以这样写"{params,returnObj}",只要是一个合法的 ognl 表达式,都能被正常支持。
很多时候我们只想看到某个方法的rt大于某个时间之后的trace结果,现在Arthas可以按照方法执行的耗时来进行过滤了,例如trace *StringUtils isBlank '#cost>100’表示当执行时间超过100ms的时候,才会输出trace的结果。
watch/stack/trace这个三个命令都支持#cost耗时条件过滤。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式,使用OGNL表达式 |
[E] | 开启正则表达式匹配,默认是通配符匹配 |
[n:] | 设置命令执行次数 |
#cost | 方法执行耗时,单位是毫秒 |
案例:
# trace函数指定类的指定方法
trace com.example.jvm.controller.UserController list
# 在浏览器上进行登录操作,检查最耗时的方法
trace *.DispatcherServlet *
3.3.11 tt
:时间隧道,记录多个请求
time-tunnel 时间隧道。
记录下指定方法每次调用的入参和返回信息,并能对这些不同时间下调用的信息进行观测
参数解析:
tt的参数 | 说明 |
---|---|
-t | 记录某个方法在一个时间段中的调用 |
-l | 显示所有已经记录的列表 |
-n 次数 | 只记录多少次 |
-s 表达式 | 搜索表达式 |
-i 索引号 | 查看指定索引号的详细调用信息 |
-p | 重新调用:指定的索引号时间碎片 |
案例:
# 最基本的使用来说,就是记录下当前方法的每次调用环境现场。
tt -t com.example.jvm.controller.UserController list
模拟报错:
@Operation(summary = "业务接口模拟测试")
@Parameters({
@Parameter(name = "str",description = "字符串参数",in = ParameterIn.QUERY),
})
@GetMapping("work")
public ResponseEntity<String> work(@RequestParam("str") String str){
if (str.equals("1")){
throw new RuntimeException("异常");
}
testService.work1();
testService.work2();
testService.work3();
return ResponseEntity.ok().body("success");
}
public void work1() {
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
log.info("work1");
}
public void work2() {
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
log.info("work2");
}
public void work3() {
try {
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
log.info("work3");
}
# 对现有记录进行检索
tt -l
# 需要筛选出 `primeFactors` 方法的调用信息
tt -s 'method.name=="getStr"'
# 查看某条记录详细信息
tt -i 1007
3.3.12 redefine 定义class
可以在不停止项目的情况下,修改java文件,通过javac 类名.java编译 再通过redefine 定义class上传到远程
我在Linux上放了一个小程序,输出zyw.
# 编辑T.java文件
vim T.java
# 编译T.java生成T.class文件
javac T.java
# 启动arthas 绑定TestMain进程
java -jar arthas-boot.jar
# 重新定义T.class 文件
redefine T.class
3.3.13 退出 arthas
如果只是退出当前的连接,可以用quit
或者exit
命令。Attach 到目标进程上的 arthas 还会继续运行,端口会保持开放,下次连接时可以直接连接上。
如果想完全退出 arthas,可以执行stop
命令。