Java虚拟机理解-内存管理 运行时数据区域 HotSpot虚拟机对象 垃圾收集器与内存分配策略 HotSpot算法实现 垃圾收集器 Java工具 JDK可视化工具 调优案例分析与实战

jdk 1.8之前与之后的内存模型有差异,方法区有变化(https://cloud.tencent.com/developer/article/1470519)。
Java虚拟机理解-内存管理
运行时数据区域
HotSpot虚拟机对象
垃圾收集器与内存分配策略
HotSpot算法实现
垃圾收集器
Java工具
JDK可视化工具
调优案例分析与实战

java的内存数据区域划分:

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

程序计数器(Program Counter Register)

理解为当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常、线程恢复等基础功能依赖于此。
每个线程独立存储,互不影响,生命周期与线程相同
java方法的计数器内容时正在执行的虚拟机字节码的指令地址,Native方法则是空(Undefined),此区域无OOM。

虚拟机栈(Java Virtual Machine Stacks)

线程私有,生命周期与线程相同。
描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
常说的Java内存区分为堆(Heap)栈(Stack)两块比较粗糙,实际的内存划分复杂很多。常用的对象内存分配关系最密切的内存区域是这两块,其中的栈在这里就是指虚拟机栈的局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference类型,不等同于对象本身,可能是指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令地址)
64位的long和double占用2个局部变量空间(Slot),其他占用一个。
这个区域有两种异常:
如果线程请求的栈深度大于虚拟机允许的深度,将抛出*Error。
如果虚拟机可以动态扩展内存,当扩展时无法申请到足够内存是,会抛出OutOfMemoryError。

本地方法栈(Native Method Stack)

与虚拟机栈的作用相似,虚拟机栈执行的是java方法,而本地方法栈执行的是Native方法。 Sun HotSpot将本地方法栈与虚拟机栈合二为一。
抛出的异常也相同

Java堆(Java Heap)

被所有线程共享,在虚拟机启动时创建。唯一的目的就是存放对象,几乎所有的对象实例都在这里分配内存(栈上分配,标量替换等优化技术会有影响)(Java虚拟机规范原文 The heap is the runtime data area from which memory for all class instances and arrays is allocated)。可以通过(-Xmx -Xms控制堆的扩展)
堆是GC管理的主要区域,现在的收集器基本都采用分待收集算法,所以堆还可以分为:新生代和老年代;再细一点分为Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)

方法区(Method Area)

被所有线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译期编译后的代码等数据。Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它别名是Non-Heap(非堆)。
很多人将方法区称为永久代(Permanent Generation),本质上两者并不等价,仅仅因为HotSpot设计团队将GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。这样HotSpot的GC可以像管理Java堆一样管理这部分内存,省去写专门代码。其他虚拟机(JRockit,J9)是不存在永久代的概念。
但是这样容易出现OOM,存在(-XX:MaxPermSize上限,其他的VM只要没有触及进程可用内存上限就不会有问题),因此有极少数方法可能会出现问题(String.intern()),这个区域的内存回收主要针对常量池的回收和对类型的卸载。

运行时常量池(Runtime Constant Pool)

是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
JVM对于Class文件每一个字节用于存储哪种数据都必须符合规范,对于运行时常量池没有任何细节要求。运行时常量池具有动态性,常量并非编译时产生,运行时也可以产生新的常量,如String.intern()

直接内存(Direct Memory)

并非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。
JDK1.4中加入了NIO(New Input/Output),引入了基于通道(Channel)与缓冲区(Buffer)的IO方式,使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这部分是不受到Java堆大小的限制。

HotSpot虚拟机对象

对象的创建

普通对象的创建(不包括数组和Class对象),当虚拟机遇到new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过。如果没有会先执行相应的类加载过程。
类加载检查通过后,VM为新生对象分配内存。对象所需的内存大小在加载后可以完全确定。若java分配和空闲内存区域是绝对规整的,那分配过程仅仅是将指针向空闲空间移动一个与对象大小相等的距离,这个称之为“指针碰撞(Bump the Pointer)”。如果不是规整的,那VM要维护一个列表记录可用内存,分配时找到足够大的内存空间分配同时更新列表,这个称之为“空闲列表(Free List)”。
Java堆是否规整是由GC是否带有压缩整理功能决定,所以Serial,ParNew等带Compact过程的收集器使用的是指针碰撞,CMS这种基于Mark-Sweep算法的收集器采用空闲列表。
并发情况下的线程安全考虑有两种方案,
一种是堆内存分配空间的动作进行同步处理,虚拟机会采用CAS(compare and swap)配上失败重试的方式保证更新操作的原子性。【CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)】
一种是把内存分配的动作按照线程划分在不同的空间之中进行,就是每个线程都事先分配一小块内存区域称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只要在要分配新的TLAB才需要同步锁定,VM是否使用TLAB,可以使用-XX:+/-UseTLAB参数设定
内存分配完成后,虚拟机会将分配到的内存空间都初始化为零值,保证了字段再java代码中可以不赋值直接使用。
接下来虚拟机设置对象头(Object Header),如对象是哪个类的实例,如何找到类的元数据信息,对象的哈希值,对象GC分代年龄等信息。

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储布局分为3块区域:对象头(Header),实例数据(Instance Data),对齐填充(Padding)。
对象头包含两部分信息,第一部分用于存储对象自身的运行时数据,如HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,官方称之为“Mark Word”。
对象投的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象时哪个类的实例。数组还会额外存储一块记录数组长度的数据。
接下来的实例数据部分是对象真正存储的有效信息,各种字段类型内容。无论是父类还是子类,这部分的存储属性会收到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
第三部分对齐填充并不是必然存在的,也没有特别含义,仅起着占位符的作用。HotSpot VM自动内存管理系统要求对象起始地址必须是8字节的整数倍,因此对象实例部分没有对齐时需要用填充来补齐。

对象的访问定位

Java程序要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问对象的方式分为使用句柄和直接指针两种。

句柄访问
Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据(存储在堆中)和类型数据(存储在方法区中)各自的具体地址信息。
好处是稳定,对象移动(如GC时移动)只会改变句柄中的实例数据指针。
直接指针访问
在Java堆对象的布局中放置访问类型数据的相关信息。reference中存储的直接是对象地址。速度快,因为节省了一次指针定位的时间开销。

Sun HotSpot使用的是直接指针访问的方式。

垃圾收集器与内存分配策略

程序计数器,虚拟机栈,本地方法栈3个区域是随线程生命周期,栈中的栈帧随着方法的进入退出执行出入栈。这几个区域的内存分配和回收都具备确定性。
主要是Java堆和方法区是动态的。

对象存活算法

引用计数法(Reference Counting),添加引用计数器,引用加一失效减一,主流Java虚拟机没有使用,因为比较难解决对象相互循环引用。
可达性分析算法(Reachability Analysis),从“GC Roots”对象作为起始点向下搜索,搜索路径称为“引用链(Reference Chain)”,不可达则证明对象不可用,就可以回收。
GC Roots对象包括如下:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。

JDK1.2之后,Java对引用的概念进行了扩充,分为

  • 强引用(Strong Reference)
    在程序代码中普遍存在 Object o=new Object(),强引用存在永远不会回收对象
  • 软引用(Soft Reference)
    有用但并非必需的对象,在内存溢出异常前,会把这些对象列进回收范围进行第二次回收。这次之后如果没有足够内存才会抛出内存溢出。JDK1.2之后提供了SoftReference类实现软引用
  • 弱引用(Weak Reference)
    强度比软引用更弱,无论当前内存是否足够都会回收。JDK1.2之后提供了WeakReference类实现软引用
  • 虚引用(Phantom Reference)
    虚引用不会影响生存时间,也无法通过虚引用取得对象实例,仅用于在GC时收到一个系统通知。JDK1.2之后提供了PhantomReference类实现软引用

可达性分析算法中不可达的对象,要经历2次标记之后才会真正死亡。可达性算法计算后第一次标记并且进行一次筛选,条件是次对象是否有必要执行finalize()方法。如果没有该方法,或者虚拟机调用过该方法,则视为无需执行。
如果被判定有必要执行,这个对象会被放在F-Queue队列中,并且稍后在一个由虚拟机自动建立的,低优先级的Finalizer线程去执行。执行意味着触发但并不会等待。对象如果与引用链上任何一个对象建立关联,则第二次标记时将移出“即将回收”的集合;否则就回收。

方法区回收

Java虚拟机规范中说过可以不要求虚拟机在方法区实现垃圾回收,因为性价比比较低,堆中的新生代垃圾回收一次一般可以回收70%-95%的空间,永久代远低于此。
永久代垃圾回收主要包含两部分:废弃常量和无用的类。
如常量“abc”,当前系统中没有任何String对象或者其他地方引用,则会被清理出常量池。
无用的类条件则苛刻很多:

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

在大量使用反射、动态代理、CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,防止永久代内存溢出。

垃圾回收算法

标记-清除算法(Mark-Sweep)分为标记和清除两个阶段。这是最基础的收集算法,因为后续收集算法都是基于这种思路并且对其不足进行改进而来。
其主要不足有两个:

  • 效率问题:标记和清除两个过程效率都不高
  • 空间问题:标记清除后会产生大量不连续的内存碎片,可能会导致后续在内存分配较大对象时无法找到连续内存而提前触发另一次垃圾收集动作。

复制算法(Copying),它将内存划分为大小相等的两块,每次只用其中的一块。当这块内存用完就将存活的对象复制到另外一块上,然后把这块内存直接清理掉。代价就是内存缩小为原来的一半。
现在的商业虚拟机都采用本算法来收集新生代。IBM公司研究表明新生代中98%对象生命周期短暂,不需要划分一半的内存,而是划分为一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和一块Survivor,回收时将Eden和Survivor中存活对象一次性复制到另外一块Survivor。HotSpot默认Eden和Survivor的比例是8:1.当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)。
复制算法在对象存活率较高时需要进行较多的复制操作,效率变低。而且如果不想浪费50%的空间,就需要额外的空间进行分配担保(100%对象存货),老年代一般不能直接用这种算法

标记-整理算法(Mark-Compact),与标记清除相似,但是第二步不是直接清理,而是让所有存活的对象都向一段移动,然后直接清理掉边界意外的内存。

分代收集算法(Generational Collection),根据对象存活周期的不同将内存划分为几块,一般是将Java堆分为新生代和老年代,这样可以根据各个年代采用适当的收集算法。

有关年轻代的JVM参数
1)-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
2)-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
3)-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

HotSpot算法实现

枚举根节点

可以作为GC ROOT的节点主要在全局性的引用(如常量或类静态属性)与执行上下文(栈帧中的本地变量表)中。可达性分析堆执行时间的敏感还体现在GC停顿上,分析工作必须在一个能确保一致性的快照中进行。一致性是指整个分析期间执行系统像是被冻结在某个时间点上,不可以发生任何变化。GC进行时必须停顿所有java执行线程(Sun称之为 Stop The World)的其中一个重要原因,CMS收集器中,枚举根节点时也是必须要停顿的。
目前的主流Java虚拟机都是准确式GC,使用一组称为OopMap(Ordinary Object Pointer)的数据结构存储哪些地方存放着对象引用。在JIT编译过程中,会在特定的位置记录下栈和寄存器中是哪些位置是引用,GC扫描时可以直接得知这些信息。

安全点(safepoint)

如果为每一条指令生成OopMap,这些可能会需要大量的额外空间,成本较高。所以HotSpot是在特定位置记录这些信息,这些位置称为安全点。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”,例如方法调用、循环跳转、异常跳转等。
如何在GC发生时让所有的线程都在最近的安全点上停顿。这里有两种方案选择:

  • 抢先式中断(Preemptive Suspension)
    GC发生时首先中断全部线程,发现有线程中断的点不在安全点上就恢复让其运行到安全点。现在几乎没有虚拟机使用类似的方式。
  • 主动式中断(Voluntary Suspension)
    GC发生时设置标志,各个线程执行时主动去轮询这个标志,为真时中断挂起。轮询标志与安全点所在的地点重合。

SafePoint一般出现在以下位置:

  • 循环体的结尾
  • 方法返回前
  • 调用方法的call之后
  • 抛出异常的位置

安全区域(Safe Region)

当线程处于Sleep或者Block时,线程无法响应JVM的中断请求,这时候就需要安全区域。安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。当线程进入SR中的代码时,首先标志自己进入了SR,当这段时间JVM发起GC时,不用管SR状态的线程。线程要离开SR时,会检查系统是否完成根节点枚举(或者整个GC过程),完成就继续执行否则就等待信号。

垃圾收集器

Java虚拟机理解-内存管理
运行时数据区域
HotSpot虚拟机对象
垃圾收集器与内存分配策略
HotSpot算法实现
垃圾收集器
Java工具
JDK可视化工具
调优案例分析与实战

上图展示了7种不同分代的收集器,如果中间有线,表示他们可以搭配使用。

Serial收集器

最基本,发展历史最悠久的收集器。在JDK1.3.1之前时虚拟机新生代手机的唯一选择。单线程收集器,Stop The World的执行者。虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效。在桌面场景中,新生代内存一般是几十或者一两百兆,停顿时间控制在几十毫秒最多一百多毫秒。

ParNew收集器

ParNew就是Serial的多线程版本,它是Server模式下的虚拟机中首选的新生代收集器,因为除了Serial,只有它能和CMS(Concurrent Mark Sweep)收集器配合工作。因为JDK1.5用CMS收集老年代时,新生代只能选择Serial或者ParNew中的一个。

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
  • 并发(Concurrent):用户线程与垃圾收集器同时执行(不一定是并行,可能交替执行)

Parallel Scavenge收集器

新生代收集器,复制算法,并行多线程收集器。
CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,PS收集器目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间端适合与用户交互的程序,高吞吐量可以高效利用CPU时间,尽快完成程序运算任务,适合后台运算而不需要太多交互的任务。
Parallel Scavenge提供了两个参数用于精确控制吞吐量,最大垃圾收集停顿时间 -XX:MaxGCPauseMillis,直接设置吞吐量大小-XX:GCTimeRatio。
还有一个参数-XX:+UseAdaptiveSizePolicy,打开之后,新生代大小、Eden和Survivor区比例,晋升老年代对象年龄等细节参数不需要设置。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称之为GC自适应的调节策略(GC Ergonomics)

Serial Old收集器

这是Serial收集器的老年代版本,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。在Server模式下有两大用途:一种是用在JDK1.5以及之前的版本与Parallel Scavenge收集器搭配使用,另一种就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

这是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。是在JDK1.6提供。注重吞吐量以及CPU资源敏感的场合,可以考虑Parallel Scavenge加Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以捉去最短回收停顿时间为目标的收集器。运作过程分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)
    其中初始标记、重新标记两个步骤仍然需要“Stop The World”,初始标记仅标记GC Roots能直接关联的对象,速度很快。并发标记就是进行GC Roots Tracing的过程,重新标记阶段则是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记,这个阶段停顿时间一般比初始标记阶段稍长一些,但是比并发标记的时间短。
    CMS的优点:并发收集、低停顿
    3个明显缺点:
  1. CMS收集器对CPU资源非常敏感。其实面向并发设计的程序对CPU资源都比较敏感,在并发阶段虽然不会暂停用户线程,但是会因为占用了一部分线程(CPU)资源导致应用程序变慢,总吞吐量会降低。CMS默认启动回收线程是(CPU数量+3)/4,也就是CPU在4个以上时会占用不少于25%的CPU资源并且随着CPU数量增加而下降,当CPU不足4个是,影响可能就很大。
  2. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败导致另一次Full GC的产生。CMS并发清理阶段用户线程还在运行,还会由新的垃圾产生。这部分在标记过程后,CMS无法在当次收集中处理掉他们,要等到下一次GC时再清理,这些垃圾称为“浮动垃圾”。CMS在GC过程中用户线程还要继续运行,因此需要预留一部分内存供用户使用,因此老年代不能等到完全填满。
  3. 因为Mark-Sweep算法本身可能会引发的空间碎片。

G1收集器

G1(Garbage-First)收集器是最前沿的成果之一。目标是替换CMS,有如下特点:

  • 并行与并发:G1能充分利用多CPU、多环境下的硬件优势,缩短StopTheWorld停顿时间。
  • 分代收集:不需要其他收集器配合能够独立管理整个GC堆
  • 空间整合:整体看来是基于“标记-整理”实现,局部(2个Reginon之间)上来看是基于“复制”算法实现。这意味这G1运作期间不会产生内存空间碎片,收集后可以提供规整的可用内存。
  • 可预测的停顿:能够建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不能超过N毫秒,这个几乎是实时java(RTSJ,Real-Time Java Specification)的垃圾收集器特征。

使用G1收集器时,Java堆内存被划分为多个大小相等的独立区域(Region),虽然保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,他们是一部分Region(不需要连续)的集合。
G1能够建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,根据允许的收集时间,有限回收价值最大的Region。
G1算法实现从2004年Sun实验室发表第一篇G1论文,直到jdk7u4才移除了“Experimental”标识达到商用程度。

JDK9之后G1称为默认收集器,
JDK11开始引入ZGC,是一种可扩展的低延迟垃圾收集器,旨在实现以下目标:

  • 暂停时间不超过10毫秒
  • 暂停时间不会随堆或实时设置大小而增加
  • 处理堆范围从几百M到几T字节大小

JDK12开始引入Shenandoah GC,Shenandoah是一款concurrent及parallel的垃圾收集器;跟ZGC一样也是面向low-pause-time的垃圾收集器,不过ZGC是基于colored pointers来实现,而Shenandoah GC是基于*s pointers来实现。

内存分配与回收策略

大部分情况下,对象在新生代Eden区分配,当Eden没有足够空间,虚拟机发起一次Minor GC。之后仍然不足进行Full GC。
大对象直接进入老年代,所以代码中出现大对象是要很慎重,特别是一堆生命周期很短的大对象。
长期存活的对象进入老年代。每熬过一次Minor GC,年龄增加1岁,默认15岁就会晋升到老年代。
动态对象年龄判定,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等待MaxTenuringThreshold中要求的年龄。
空间分配担保,Minor GC之前虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立可以确保Minor GC安全。如果不成立,检查老年代最大连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试进行Minor GC。如果虚拟机设置值不允许冒险,或者小于平均大小,则改为一次Full GC。担保失败(Handle Promotion Failure)后,会发起一次Full GC。

Java工具

  • jps(JVM Process Status Tool)
    可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(MainClass,main()函数所在的类)以及本地虚拟机唯一ID(Local Virtual Machine Identifier, LVMID)
    [jira@iz2ze6589vyznj8lm7cbk7z ~]$ jps -lv
    6610 org.apache.catalina.startup.Bootstrap -Djava.util.logging.config.file=/home/jira/atlassian-jira-software-7.5.2-standalone/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms1384m -Xmx3768m -Djava.awt.headless=true -Datlassian.standalone=JIRA -Dorg.apache.jasper.runtime.BodyContentImpl.LIMIT_BUFFER=true -Dmail.mime.decodeparameters=true -Dorg.dom4j.factory=com.atlassian.core.xml.InterningDocumentFactory -XX:-OmitStackTraceInFastThrow -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Xloggc:/home/jira/atlassian-jira-software-7.5.2-standalone/logs/atlassian-jira-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -Dcatalina.base=/home/jira/atlassian-jira-software-7.5.2-standalone -Dcatalina.home=/home/jira/atlassian-jira-software-7.5.2-standalone -Djava.io.tmpdir=/home/jira/atlassian-jira-software-7.5.2-standalone/temp
    25652 synchrony.core -Xss2048k -Xmx1g
    25431 org.apache.catalina.startup.Bootstrap -Djava.util.logging.config.file=/home/jira/atlassian-confluence-6.3.1/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms1256m -Xmx2048m -XX:PermSize=128m -XX:MaxPermSize=512m -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dconfluence.context.path= -Dorg.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE=32768 -Dsynchrony.enable.xhr.fallback=true -Xms1024m -Xmx2024m -XX:+UseG1GC -Datlassian.plugins.enable.wait=300 -Djava.awt.headless=true -XX:G1ReservePercent=20 -Xloggc:/home/jira/atlassian-confluence-6.3.1/logs/gc-2019-10-27_01-10-04.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=2M -XX:-PrintGCDetails -XX:+PrintGCDateStamps -XX:-PrintTenuringDistribution -Xms1256m -Xmx2048m -XX:PermSize=128m -XX:MaxPermSize=512m -Djava.endorsed.dirs=/home/jira/atlassian-confluence-6.3.1/endorsed -Dcatalina.base=/home/jira/atlassian-confluence-6.3.1 -Dcatalina.home=/home/jira/atlassian-confluence-6.3.1 -Djava.io.tm
    5372 sun.tools.jps.Jps -Dapplication.home=/usr/java/jdk1.8.0_181-amd64 -Xms8m

  • jstat(JVM Statistics Monitoring Tool)虚拟机统计信息监视工具
    它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾回收、JIT编译等运行数据。
    [jira@iz2ze6589vyznj8lm7cbk7z ~]$ jstat -gcutil 6610
    S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
    69.10 0.00 52.43 92.57 90.47 83.80 5613 460.532 12 13.624 474.156
    这个是我们Jira服务器GC情况,E表示Eden区使用了52.43%,两个Survivor区(S0(69.10%),S1(0%)),老年代O(表示Old)占用了92.57%,jdk1.8之前永生代是P(Permanent)之后是M(Metaspace)90.47%,CCS(Compressed Class Space)83.8%。YGC(Young GC)5613次,YGCT总耗时460秒,FGC(Full GC)12次,FGCT耗时13秒,GCT总GC耗时474秒。

  • jinfo(Java infomation)
    java配置信息工具,可以实时地查看和调整虚拟机各项参数,jps -v可以查看显式指定参数,jinfo -flag可以进行默认值查询,jinfo -sysprops可以把虚拟机进程中System.getProperties()内容打印出来。

  • jmap(Memory Map for Java)
    用于生成堆转储快照(一般称为heapdump或者dump文件),还可以查询finalize执行队列,Java堆和永久代的详细信息,如空间使用率,使用的收集器等。jmap -dump:format=b,file=文件名 [pid] 会生成一个bin文件

  • jhat(JVM Heap Analysis Tool)
    虚拟机堆转储快照分析工具,与jmap搭配使用,来分析jmap生成的堆转储快照。内置了一个http服务器,可以再完成之后启动网站访问。命令就是直接 jhat xxx.bin

  • jstack(Stack Trace for Java)
    Java堆栈跟踪工具,用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件),线程快照就是虚拟机内每一条线程当前执行的方法堆栈信息集合。jstatck -l pid。后续的JDK1.5中,java.lang.Thread类新增了一个getAllStackTraces()方法,可以写个页面直接查询。

  • HSDIS(JIT生成代码反汇编)
    Sun官方推荐的HotSpot虚拟机JIT编译代码的反汇编插件。
    在主流商用虚拟机中,HotSpot和J9可以采用混合模式(解释器与编译器配搭使用),而JRockit内部没有解释器,采用纯编译模式。
    Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行的特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成为本地平台相关的机器码,并进行优化,而完成这个任务的编译器称为及时编译器(Just In Time Compiler,简称JIT)。
    解释器与编译器
    解释器优势:当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行。同时解释器还可作为编译器激进优化时的一个“逃生门”,当激进优化假设不成立时可退回到解释状态继续执行。
    编译器优势:编译之后得到优化后的本地代码,执行效率高,但优化耗时较长,且占用内存资源较大。

JDK可视化工具

最强的两个可视化工具:JConsole和Visual VM(All-in-One Java Troubleshooting Tool),这个是正式的JDK成员。
Visual VMI可以做到:
显示虚拟机进程以及配置、环境信息(jps,jinfo)
监视应用程序的CPU、GC、堆、方法区以及线程信息(jstat,jstack)
dump以及分析堆转储快照(jmap,jhat)
方法及的程序运行性能分析,找出被调用最多、运行时间最长的方法。
离线程序快照:收集程序运行时配置、线程dump、内存dump等信息建立快照。

调优案例分析与实战

高性能硬件程序部署

4CPU,16G内存,64位JDK1.5,-Xmx和-Xms固定在12G,但是网站长时间失去响应。
排查后发现是GC停顿,Server模式默认使用吞吐量优先,回收12GB,一次FullGC停顿高达14秒,读取文档序列号产生大对象直接进入老年代。因此出现每个十几分钟出现十几秒的停顿。

堆外内存导致的溢出错误

普通PC机,内存2G,JVM分配了1.6G,GC正常,堆内存正常,频繁OOM。因为框架NIO操作使用到Direct Memory内存,这块是比较难直接回收的,只有在FullGC时顺便回收。

外部命令导致系统缓慢

每个用户请求的处 理都需要执行一个外部shell脚本来获得系统的一些信息。执行这个shell脚本是通过Java的 Runtime.getRuntime().exec()方法来调用的。这种调用方式可以达到目的,但是它在Java 虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时创建进程 的开销也非常可观。Java虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一 样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执 行这个操作,系统的消耗会很大,不仅是CPU,内存负担也很重。
用户根据建议去掉这个Shell脚本执行的语句,改为使用Java的API去获取这些信息后, 系统很快恢复了正常。

JVM进程崩溃

BS系统,正常运行一段时间之后JVM自动关闭,留下一个hs_err_pdi###.log文件。由于异步启用了Socket请求另外一个较慢的站点,超过虚拟机承受能力后导致崩溃。改为消息队列后正常。

不恰当的数据结构

在HashMap<Long,Long>结构中,只有Key和Value所存放 的两个长整型数据是有效数据,共16B(2×8B)。这两个长整型数据包装成java.lang.Long对 象之后,就分别具有8B的MarkWord、8B的Klass指针,在加8B存储数据的long值。在这两个 Long对象组成Map.Entry之后,又多了16B的对象头,然后一个8B的next字段和4B的int型的 hash字段,为了对齐,还必须添加4B的空白填充,最后还有HashMap中对这个Entry的8B的引 用,这样增加两个长整型数字,实际耗费的内存为 (Long(24B)×2)+Entry(32B)+HashMap Ref(8B)=88B,空间效率为16B/88B=18%, 实在太低了。