跳至主要內容

JVM

观风大约 26 分钟内存模型GC类加载机制内存分配策略JVM监控

JVM

JVM内存结构

内存空间分配

Java 虚拟机的内存空间分为 5 个部分:

  • 程序计数器
  • Java 虚拟机栈
  • 本地方法栈
  • 方法区

jvm-memory-structure

程序计数器

程序计数器的作用

  • 程序计数器来依次读取指令,从而实现代码的流程控制。
  • 在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了。

程序计数器的特点

  • 线程私有,每条线程都有自己的程序计数器。
  • 生命周期:随着线程的创建而创建,随着线程的结束而销毁。
  • 是唯一一个不会出现 OutOfMemoryError 的内存区域。

Java 虚拟机栈(Java 栈)

Java 虚拟机栈的定义

Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口信息

jvm-stack

压栈出栈过程

当在这个栈帧中调用另一个方法,与之对应的栈帧又会被创建,新创建的栈帧压入栈顶,变为当前的活动栈帧。

方法结束后,当前栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。

局部变量表

主要用于存储方法参数、定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及 return address 类型。

比如int a = 1;存储的就是a这个基本数据类型

操作数栈

存储操作数的值。

比如int a= 1;存储的就是1;

Java 虚拟机栈的特点

  • 运行速度特别快,仅仅次于 PC 寄存器。
  • 局部变量表随着栈帧的创建而创建,它的大小在编译时确定。
  • Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
    • StackOverFlowError 若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常。
    • OutOfMemoryError 若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 异常。
  • Java 虚拟机栈也是线程私有,随着线程创建而创建,随着线程的结束而销毁。
  • 出现 StackOverFlowError 时,内存空间可能还有很多。

本地方法栈(C 栈)

本地方法栈的定义

本地方法栈是为 JVM 运行 Native 方法准备的空间。它与 Java 虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。

栈帧变化过程

本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。

方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

堆的定义

堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。

jvm-memory

为什么有的对象不存放在堆中?

  1. 线程栈上分配(Stack Allocation):某些小型、短寿命的对象可以在线程的栈上分配,而不是堆上。这种对象在方法执行期间创建,并在方法返回时销毁,因此不需要堆内存中的垃圾回收。这种优化称为逃逸分析,JVM会分析对象的生命周期并决定是否将其分配在堆上或栈上。
  2. 方法区(Metaspace):类的元数据和静态字段通常存储在方法区(在Java 8之前是永久代,从Java 8开始是元空间)。虽然这不是对象本身,但它们也是内存中的数据,不存储在堆内存中。
  3. 直接内存(Direct Memory):有时候,Java应用程序使用java.nio包中的ByteBuffer来进行I/O操作或与本地库进行交互。这些ByteBuffer可能使用allocateDirect()方法分配,它们的数据存储在堆外(native memory)中,而不是堆内存中。
  4. 对象池(Object Pooling):在某些情况下,为了提高性能和减少垃圾回收的负担,开发人员可以使用对象池,将一组对象预先创建并重复使用,而不是频繁地创建和销毁对象。这些对象通常存储在池中而不是堆中。

堆的特点

  • 线程共享,整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个。
  • 在虚拟机启动时创建。
  • 是垃圾回收的主要场所。
  • 堆可分为新生代(Eden 区:From SurviorTo Survivor)、老年代。
  • Java 虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
  • 关于 Survivor s0,s1 区: 复制之后有交换,谁空谁是 to。

新生代与老年代

  • 老年代比新生代生命周期长。
  • 新生代与老年代空间默认比例 1:2:JVM 调参数,XX:NewRatio=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3。
  • HotSpot 中,Eden 空间和另外两个 Survivor 空间缺省所占的比例是:8:1:1
  • 几乎所有的 Java 对象都是在 Eden 区被 new 出来的,Eden 放不了的大对象,就直接进入老年代了。

对象分配过程

  • new 的对象先放在 Eden 区,大小有限制
  • 如果创建新对象时,Eden 空间填满了,就会触发 Minor GC,将 Eden 不再被其他对象引用的对象进行销毁,再加载新的对象放到 Eden 区,特别注意的是 Survivor 区满了是不会触发 Minor GC 的,而是 Eden 空间填满了,Minor GC 才顺便清理 Survivor 区
  • 将 Eden 中剩余的对象移到 Survivor0 区
  • 再次触发垃圾回收,此时上次 Survivor 下来的,放在 Survivor0 区的,如果没有回收,就会放到 Survivor1 区
  • 再次经历垃圾回收,又会将幸存者重新放回 Survivor0 区,依次类推
  • 默认是 15 次的循环,超过 15 次,则会将幸存者区幸存下来的转去老年区 jvm 参数设置次数 : -XX:MaxTenuringThreshold=N 进行设置
  • 频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集

img

Full GC /Major GC 触发条件

  1. 老年代空间不足: 当老年代空间被对象占满,没有足够的空间来分配新的对象时,会触发Full GC。
  2. 永久代或元空间不足: 元空间不足时,会触发Full GC。
  3. 手动触发: 开发人员可以通过Java代码或JVM参数来手动触发Full GC。例如,可以使用System.gc()方法,或在启动JVM时使用-XX:+FullGC等参数来触发Full GC。
  4. CMS收集器: 使用CMS垃圾回收器的情况下,当老年代空间不足时,CMS会尝试在尽量减少停顿时间的情况下执行垃圾回收。但如果CMS回收无法释放足够的内存空间,就会触发Full GC。

逃逸分析

标量替换
  • 标量不可在分解的量,java 的基本数据类型就是标量,标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在 JAVA 中对象就是可以被进一步分解的聚合量
  • 替换过程,通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM 不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。

线程本地分配缓存区(TLAB)

  • TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区,是属于 Eden 区的,这是一个线程专用的内存分配区域,线程私有,默认开启的(当然也不是绝对的,也要看哪种类型的虚拟机)
  • 堆是全局共享的,在同一时间,可能会有多个线程在堆上申请空间,但每次的对象分配需要同步的进行(虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性)但是效率却有点下降
  • 所以用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自己的 TLAB,这样可以使得线程同步,提高了对象分配的效率
  • 当然并不是所有的对象都可以在 TLAB 中分配内存成功,如果失败了就会使用加锁的机制来保持操作的原子性
  • -XX:+UseTLAB 使用 TLAB,-XX:+TLABSize 设置 TLAB 大小

方法区

方法区的定义

Java 虚拟机规范中定义方法区是堆的一个逻辑部分。方法区存放以下信息:

  • 已经被虚拟机加载的类信息
  • 常量
  • 静态变量
  • 即时编译器编译后的代码

方法区的特点

  • 线程共享。
  • 内存回收效率低。
  • Java 虚拟机规范对方法区的要求比较宽松。 和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收。

运行时常量池

jvm-runtime-constant-pool

HotSpot 虚拟机对象探秘

对象的内存布局

在 HotSpot 虚拟机中,对象的内存布局分为以下 3 块区域:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

object-memory-layout.png

对象头

对象头记录了对象在运行过程中所需要使用的一些数据:

  • 哈希码
  • GC 分代年龄
  • 锁状态标志
  • 线程持有的锁
  • 偏向线程 ID
  • 偏向时间戳

对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个数组,那么对象头还会包括数组长度。

实例数据

实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。

对齐填充

用于确保对象的总长度为 8 字节的整数倍。

对象的创建过程

new-instruction

类加载检查

虚拟机在解析.class文件时,若遇到一条 new 指令,首先它会去检查常量池中是否有这个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类加载过程。

为新生对象分配内存

对象所需内存的大小在类加载完成后便可完全确定,接下来从堆中划分一块对应大小的内存空间给新的对象。分配堆中内存有两种方式:

  • 指针碰撞 如果 Java 堆中内存绝对规整(说明采用的是“复制算法”或“标记整理法”),空闲内存和已使用内存中间放着一个指针作为分界点指示器,那么分配内存时只需要把指针向空闲内存挪动一段与对象大小一样的距离,这种分配方式称为“指针碰撞”。
  • 空闲列表 如果 Java 堆中内存并不规整,已使用的内存和空闲内存交错(说明采用的是标记-清除法,有碎片),此时没法简单进行指针碰撞, VM 必须维护一个列表,记录其中哪些内存块空闲可用。分配之时从空闲列表中找到一块足够大的内存空间划分给对象实例。这种方式称为“空闲列表”。

初始化

分配完内存后,为对象中的成员变量赋上初始值,设置对象头信息,调用对象的构造函数方法进行初始化。

至此,整个对象的创建过程就完成了。

垃圾收集策略与算法

判定对象是否存活

引用计数法

在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。

引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是主流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因为它很难解决对象之间循环引用的问题。

可达性分析法

所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。

GC Roots 是指:

  • Java 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

回收堆中无效对象

两色标记法

在两色标记法中,对象被标记为两种颜色,通常是白色和黑色。初始状态下,所有对象都是白色,然后从根集开始,通过可达性分析,将可达对象标记为黑色。然后,未标记的对象就可以被回收了。这个算法的关键在于确保标记阶段与垃圾回收阶段是并发执行的,以降低停顿时间。

三色标记法

三色标记法是两色标记法的一种改进,引入了一个额外的灰色(或灯灰色)状态。在初始状态下,所有对象都是白色。然后,从根集开始,可达对象标记为灰色,然后逐一处理灰色对象,并将其标记为黑色或白色。灰色对象通常会被放入一个队列中,以便在后续阶段继续处理。未标记的白色对象可以被回收,而黑色对象是保留的。这个算法允许更好的并发性和更低的停顿时间。

回收方法区内存

判定废弃常量

只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。比如,一个字符串 "bingo" 进入了常量池,但是当前系统没有任何一个 String 对象引用常量池中的 "bingo" 常量,也没有其它地方引用这个字面量,必要的话,"bingo"常量会被清理出常量池。

判定无用的类

判定一个类是否是“无用的类”,条件较为苛刻。

  • 该类的所有对象都已经被清除
  • 加载该类的 ClassLoader 已经被回收
  • 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记-清除算法

img

复制算法(新生代)

img

标记-整理算法(老年代)

img

分代收集算法

根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。

  • 新生代:复制算法
  • 老年代:标记-清除算法、标记-整理算法

HotSpot 垃圾收集器

image-20230913205857114

新生代垃圾收集器

Serial 垃圾收集器(单线程)

使用复制算法。

只开启一条 GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程,即 Stop The World。

一般客户端应用所需内存较小,不会创建太多对象,而且堆内存不大,因此垃圾收集器回收时间短,即使在这段时间停止一切用户线程,也不会感觉明显卡顿。因此 Serial 垃圾收集器适合客户端使用。

Serial

ParNew 垃圾收集器(多线程)

使用标记-整理算法

ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。

ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。

ParNew

老年代垃圾收集器

Serial Old 垃圾收集器(单线程)

标记整理算法

Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程,都适合客户端应用。

Parallel Old 垃圾收集器(多线程)

标记整理算法

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,适合在多CPU环境下使用。

CMS 垃圾收集器

CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

  • 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
  • 并发标记:使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
  • 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
  • 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。

并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

CMS

G1 通用垃圾收集器

分区算法

G1 是一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。

从整体上看, G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。

G1回收是否要扫描整堆?

并不!每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历。

如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:

  • 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
  • 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
  • 最终标记:Stop The World,使用多条标记线程并发执行。
  • 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。

内存分配与回收策略

对象优先在 Eden 分配

大对象直接进入老年代

虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。

长期存活的对象将进入老年代

使用 -XXMaxTenuringThreshold 设置新生代的最大年龄,只要超过该参数的新生代对象都会被转移到老年代中去。

默认是15

动态对象年龄判定

如果当前新生代的 Survivor 中,相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄 >= 该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

JVM 性能调优

操作系统:2~3G

堆:线程栈、元空间 3~1

堆内 old:minor 3:1 minor内 Eden:surviror 3:1

以8G内存为例:

操作系统+直接内存3G

堆3G (old:2G,Eden:600M,s0:200M,s1:200M)

栈+元空间2G

类加载的时机

类的生命周期

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段:

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

验证、准备、解析 3 个阶段统称为连接。

Load Class

类加载过程中“初始化”开始的时机

Java 虚拟机规范没有强制约束类加载过程的第一阶段(即:加载)什么时候开始,但对于“初始化”阶段,有着严格的规定。有且仅有 5 种情况必须立即对类进行“初始化”:

  • 在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发其初始化。
  • 对类进行反射调用时,如果类还没有初始化,则需要先触发其初始化。
  • 初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
  • 虚拟机启动时,用于需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类。
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发其初始化。

这 5 种场景中的行为称为对一个类进行主动引用,除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用

加载

在加载阶段,虚拟机需要完成 3 件事:

  • 通过类的全限定名获取该类的二进制字节流。
  • 将二进制字节流所代表的静态结构转化为方法区的运行时数据结构。
  • 在内存中创建一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

验证

验证阶段确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备

准备阶段是正式为类变量(或称“静态成员变量”)分配内存并设置初始值的阶段。这些变量(不包括实例变量)所使用的内存都在方法区中进行分配。

初始值“通常情况下”是数据类型的零值(0, null...)

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

类加载器

类与类加载器

判断类是否“相等”

任意一个类,都由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。

因此,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

这里的“相等”,包括代表类的 Class 对象的 equals() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

加载器种类

系统提供了 3 种类加载器:

  • 启动类加载器(Bootstrap ClassLoader): 负责将存放在 <JAVA_HOME>\lib 目录中的,并且能被虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。
  • 扩展类加载器(Extension ClassLoader): 负责加载 <JAVA_HOME>\lib\ext 目录中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader): 由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也称它为“系统类加载器”。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

ClassLoader

双亲委派模型

工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(找不到所需的类)时,子加载器才会尝试自己去加载。

在 java.lang.ClassLoader 中的 loadClass 方法中实现该过程。

为什么使用双亲委派模型

使用沙箱机制来保证代码的安全性

像 java.lang.Object 这些存放在 rt.jar 中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的 Object 类都是同一个。

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在 classpath 下,那么系统将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证。

重要的JVM参数

堆内存相关

显式指定堆内存

–Xms-Xmx

-Xms2G -Xmx5G

显式新生代内存(Young Generation)

通过-XX:NewSize-XX:MaxNewSize指定

-XX:NewSize=256m
-XX:MaxNewSize=1024m

显式指定永久代/元空间的大小

-XX:MetaspaceSize=N #设置 Metaspace 的初始大小(是一个常见的误区,后面会解释)
-XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小

垃圾收集相关

垃圾回收器

# 串行垃圾收集器
-XX:+UseSerialGC
# 并行垃圾收集器
-XX:+UseParallelGC
# CMS垃圾收集器
-XX:+UseParNewGC
# G1垃圾收集器
-XX:+UseG1GC

GC 日志记录

# 必选
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime

# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1

# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M

处理 OOM

# 遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中
-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit

JDK 命令行工具

jps (JVM Process Status): 类似 UNIX 的 ps 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;

jstat(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;

`jstat -class pid`:显示 ClassLoader 的相关信息;

`jstat -compiler pid`:显示 JIT 编译的相关信息;

`jstat -gc pid`:显示与 GC 相关的堆信息;

`jstat -gccapacity pid`:显示各个代的容量及使用情况;

`jstat -gcnew pid`:显示新生代信息;

`jstat -gcnewcapcacity pid`:显示新生代大小与使用情况;

`jstat -gcold pid`:显示老年代和永久代的行为统计,从 jdk1.8 开始,该选项仅表示老年代,因为永久代被移除了;

`jstat -gcoldcapacity pid`:显示老年代的大小;

`jstat -gcpermcapacity pid`:显示永久代大小,从 jdk1.8 开始,该选项不存在了,因为永久代被移除了;

`jstat -gcutil pid`:显示垃圾收集信息

jinfo (Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息;

jmap (Memory Map for Java) : 生成堆转储快照;

jmap -dump:format=b,file=C:\Users\SnailClimb\Desktop\heap.hprof 17340

jhat (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;

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

jstack 9256

Arthas

dashboard  # 大盘

thread				#	显示所有线程的信息
thread 1			#	显示1号线程的运行堆栈
thread -b			#	查看阻塞的线程信息(死锁)
thread -n 3			#	查看最忙的3个线程,并打印堆栈
thread -i 1000 -n 3	#	指定采样时间间隔,每过1000毫秒采样,显示最占时间的3个线程

jvm  # JVM相关信息

#	反编译MathGame方法
jad java.MathGame
#	反编绎时只显示源代码(排除ClassLoader信息)。
#	默认情况下,反编译结果里会带有ClassLoader信息,通过--source-only选项,可以只打印源代码。方便和mc/redefine命令结合使用。
jad --source-only java.MathGame
#	反编译到指定文件中
jad --source-only java.MathGame > Hello.java
#	只反编译mathGame类型中main方法
jad java.MathGame main


#	把String类的字节码文件保存到~/logs/arthas/classdump/目录下
dump java.lang.String
#	把java包下所有的类的字节码文件保存到~/logs/arthas/classdump/目录下
dump java.*


#	在浏览器上进行登录操作,检查最耗时的方法
trace *.DispatcherServlet *