1、从编码到执行
- 解释执行和编译执行是可以混合的,执行次数多的代码,会进行 JIT 的编译,交由操作系统直接执行。
- JVM 和 JAVA 无关,只要可以编译为 CLASS,都可在 JVM 上运行。
- 常见 JVM:
- Hotspot - Oracle 官方,最常用的 JVM。
- Jrockit - BEA,曾经号称最快的 JVM。被 Oracle 收购,合并于 Hotspot。
- J9 - IBM。
- Microsoft VM。
- TaobaoVM - Hotspot 深度定制版。
- LiquidVM - 直接针对硬件。
- Azul Zing - 最新垃圾回收的夜间标杆。官网:https://www.azul.com/
JDK、JRE、JVM
- JVM:Java Virtual Machine
- JRE:JVM + Core lib
- JDK:JRE + Development kit
2、Class文件结构
class 文件:是二进制字节流。
查看 class 文件:
javap -v X.class
3、类加载过程
类加载器
-
JVM 是按需动态加载,采用双亲委派机制。
主要是基于安全考虑:如果自定义 class 都可以 load 到内存,客户可以创建 java.lang.String 类,通过 CustomClassLoader 覆盖掉 Bootstrap 内的类文件。
次要是防止资源浪费:如果已经加载过,查找使用即可,无需重复加载。
-
打破双亲委派:
- 重写 loadClass() 方法。
- ThreadContextClassLoader 可以实现基础类调用实现类代码,通过
thread.setContextClassLoader
指定。 - 场景 - 热启动,热部署:osgi tomcat 都有自己的模块(web application)指定 classloader,可以加载同一类库的不同版本。
-
查看 ClassLoader 加载路径:
// BootstrapClassLoader 加载路径 System.getProperty("sun.boot.class.path"); // ExtensionClassLoader 加载路径 System.getProperty("java.ext.dirs"); // AppClassLoader 加载路径 System.getProperty("java.class.path");
类加载过程
- Loading:class 文件 load 到内存。
- Linking:链接。
- Verification:校验 class 是否符合 JVM 规范。
- Preparation:静态成员变量赋默认值,不是初值。
- Resolution:将常量池中类、方法、属性等符号引用解析为指针、偏移量等内存地址的直接引用。
- Initializing:静态变量赋初始值,调用静态代码块,调用类初始化代码。
CompilerAPI
可以手动直接在内存中编译代码,无需生成到磁盘。
LazyLoading
严格讲应该叫做 LazyInitializing
。
JVM 规范并没有规定何时加载,但是严格规定了什么时候必须初始化。
-
new
、getstatic
、putstatic
、invokestatic
指令,访问final
变量除外。getstatic
:读取静态变量。putstatic
:设置静态变量。invokestatic
:执行静态方法 java.lang.reflect
对类进行反射调用时。初始化子类时,父类首先初始化。
-
虚拟机启动时,被执行的主类必须初始化。
包含
main
方法的类。 动态语言支持
java.lang.invoke.MethodHandler
解析的结果为REF_getstatic
、REF_putstatic
、REF_invokestatic
的方法句柄时,该类必须初始化。
Java代码执行模式
Java 默认是解释执行,jvm 发现某段代码执行频率很高,则将其编译为本地代码。
解释器 - bytecode intepreter
JIT - Just In-Time compiler
-
混合模式:混合使用解释器 + 热点代码编译。起始阶段采用 解释执行。
热点代码检测:
多次被调用的方法(方法计数器:检测方法执行频率)
多次被调用的循环(循环计数器:检测循环执行频率)
可以通过参数指定运行模式:
-
-Xmixed
默认为混合模式,开始解释执行,启动速度较快,对热点代码实行检测和编译。 -
-Xint
使用解释模式,启动很快,执行稍慢。 -
-Xcomp
使用纯编译模式,执行很快,启动很慢。 -
-XX:CompileThreshold=10000
检测热点代码阈值。
4、JMM
- Java Memory Model:Java 内存模型
硬件层数据一致性
现代 CPU 数据一致性实现通过缓存锁及总线锁实现。
CPU 缓存一致性协议:MSI、MESI、MOSI、Synapse、Firefly、Dragon。Intel CPU 使用的是 MESI 协议。
MESI 是缓存锁实现方式之一,有些无法被缓存的数据,或者跨越多个缓存行的数据,依然必须使用总线锁。
读取缓存以 cache line 为基本单位,目前 64bytes。
CPU 每个 cache line 标记四种状态(额外2位):
- Modified:对缓存数据进行了更改。
- Exclusive:对缓存数据独享。
- Shared:对缓存数据读共享。
- Invalid:缓存数据被其他 CPU 进行了更改。
伪共享
位于同一缓存航的两个不同数据,被两个不同 CPU 锁定,产生互相影响。
解决方案:缓存行对齐,能够提高效率,但会浪费一些空间。
// disruptor 多线程对指针游标使用特别频繁。
public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
private volatile long cursor = INITIAL_CURSOR_VALUE;
public long p8, p9, p10, p11, p12, p13, p14; // cache line padding
CPU 乱序执行
CPU 乱序执行根源:为了提高指令执行效率,读等待同时指令执行。读指令等待的同时,可以同时执行不影响其他指令。而写的同时可以进行合并写。这样 CPU 的执行就是乱序的。
必须使用 Memory Barrier 来做好指令排序。volatile
的底层就是这样实现的(Windows 是 lock
指令)。防止乱序执行,可以使用内存屏障。
内存屏障
Intel CPU 级别内存屏障(不同 CPU,内存屏障实现不同):
-
sfence
:在sfence
指令前的写操作当必须在sfence
指令后的写操作前完成。 -
lfence
:在lfence
指令前的读操作当必须在lfence
指令后的读操作前完成。 -
mfence
:在mfence
指令前的读写操作当必须在mfence
指令后的读写操作前完成。
JVM 级别内存屏障规范(JSR33):
- LoadLoad 屏障:对于这样的语句
Loadl:LoadLoad:Load2
,在Load2
及后读读取操作要读取的数据被访问前,保证Load1
要读取的数据被读取完毕。 - StoreStore 屏障:对于这样的语句
Storel:StoreStore:Store2
,在Store2
及后续写入操怍执行前,保证Store1
的写入操怍对其它处理器可见。 - LoddStore 屏障:对于这样的语句
Loadl:LoadStore:Store2
,在Store2
及后续写入操作被刷出前,保证Load1
要读取的数据被读取完毕。 - StoreLoad 屏障:对于这样的语句
Store1:StoreLoad:Load2
,茌Load2
及后续所有读取操作执行前,保证Storel
的写入对所有处理器可见。
Volatile 实现细节:
字节码层面:class 文件增加了 ACC_VOLITILE 标识。
-
JVM 层面:volatile 内存区的读写,都加屏障。
StoreStoreBarrier volatile 写操作 StoreLoadBarrier LoadLoadBarrier volatile 写操作 LoadStoreBarrier
OS 和硬件层面:HSDIS - HotSpot Dis Assembler;Windows - lock 指令实现。
Synchronized 实现细节:
- 字节码层面:ACC_SYNCHRONIZED、monitorenter、monitorexit。
- JVM层面:调用了操作系统提供的同步机制。
- OS 和硬件层面:X86 - lock comxchg / xxxx。
Hanppens-Before原则:]VM规定重排序必须遵守的规则
- 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
- 管程锁定规则:一个
unlock
操作先行发生于后面(时间上)对同一个锁的lock
操作。 -
volatile
变量规则:对一个volatile
变量的写操作先行发生于后面(时间上)对这个变量的读操作。 - 线程启动规则:
Thread
的start()
方法先行发生于这个线程的每一个操作。 - 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过
Thread.join()
方法结束、Thread.isAlive()
的返回值等手段检测线程的终止。 - 线程中断规则:对线程
interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.isInterrupted()
方法检测线程是否中断。 - 对象终结规则:一个对象的初始化完成先行于发生它的
finalize()
方法的开始。 - 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。
AS IF SERIAL
不管如何重排序,单线程执行结果不会改变。
对象的内存布局
观察虚拟机配置:java -XX:+PrintCommandLineFlags -version
。
普通对象:
-
对象头:markword
- 32位 - 4字节
- 64位 - 8字节
-
ClassPointer 指针:开启
-XX:+UseCompressedClassPointers
为4字节,不开启为8字节。默认开启。可以使用
-XX:-UseCompressedClassPointers
关闭。 -
实例数据:普通对象指针。开启
-XX:+UseCompressedOops
为4字节,不开启为8字节。默认开启。Oops:Ordinary Object Pointers。
Padding 对齐:8的倍数。
数组对象:在普通对象基础上,多了一个数组长度,4字节。
HashCode 和 偏向锁
- 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
- 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
- 重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。
请一定要注意,这里讨论的hash code都只针对identity hash code。用户自定义的hashCode()方法所返回的值跟这里讨论的不是一回事。
Identity hash code是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。
对象定位
- 句柄池
- 直接指针:Hotspot 使用的是直接指针。
GC 回收时,定位方式有影响,三色标记算法,对句柄池算法效率比较高,对直接指针算法效率比较低。
对象分配过程
栈上分配:线程私有小对象,无逃逸(对象仅在方法内有引用),支持标量替换。默认开启。
线程本地分配:TLAB(Thread Local Allocation Buffer)。小对象,占用 Eden 区,默认 1%。多线程情况下不用竞争 Eden 就可以申请空间,提高效率。
老年代:大对象。
栈上分配 和 线程本地分配一般不需要调整参数。
-
相关 JVM 启动参数:
-XX:-EscapeAnalysis
:关闭逃逸分析。-XX:-EliminateAllocations
:关闭标量替换。-XX:-UseTLAB
:关闭TLAB。
5、运行时数据区
Run-time Data Areas
Program Counter:程序计数器 - 存放指令位置。每个线程有自己的 PC。
JVM stacks:每个线程有独有的栈,线程栈内装载的是栈帧,每个方法调用对应一个栈帧。
native method stacks:native 方法栈。
Direct Memory:JVM 直接访问内核空间内存(OS 管理的内存),省略了内存拷贝的过程。
NIO,提交效率,实现 zero copy。-
Method area:方法区。装载 class、常量池。方法区在所有 JVM 线程间共享。方法区是逻辑概念,PermSpace 和 MetaSpace 是具体实现。
1.8 版本前:Perm Space,FGC 不回收。字符串常量位于 PermSpace。启动时指定,不可变。
1.8 版本后:Meta Space,字符串常量位于堆,会触发 FGC 清理。如果不设定,最大是物理内存。
Heap:在所有 JVM 线程间共享。堆是运行时数据区,为所有类实例和数组分配内存。
run-time constant pool:常量池数据,装载在运行时常量池内。
堆内逻辑分区
内存模型:除 Epsilon、ZGC、shenandoah 之外,都是使用逻辑分代,其中 G1 是逻辑分代,物理不分代,除此之外,不仅逻辑分代,物理也分代。内存分区不适用不分代垃圾收集器,例如 ZGC(jdk11)、Shenandoah(jdk12)。
内存布局:新生代、老年代、方法区(MethodArea)
- 方法区是逻辑概念,1.7版本永久代(Perm Generation)实现,1.8版本元数据区(Metaspace)实现。
- 永久代需要启动时指定大小,且不可更改。元数据区可以设置,也可以不设置,受限于物理内存。
- 永久代/元数据区 存放:Class 元信息、方法编译后信息、代码编译后信息、JIT 编译信息、字节码等。
- 字符串常量:1.7 在永久代,1.8 在堆。
- 1.8 版本 新生代/老年代 内存比例默认 1 : 2。可通过参数
-XX:NewRatio=2
调整。
- MinorGC / YGC:新生代空间耗尽时触发。
- MajorGC / FullGC:老年代满了,无法继续分配空间时触发,新生代老年代同事进行回收。
- 新生代大量死去,少量存活,采用复制算法。
- 老年代存活率高,回收较少,采用标记清除或标记压缩算法。
Eden 区经过回收之后,进入 Survivor 区。在 Survivor 区达到 -XX:MaxTenuringThreshold
参数阈值后(最大15),进入 Tunured 区。
动态年龄:S0 向 S1 复制时,超过 50%,把年龄最大的放入老年代。
空间担保/分配担保:YGC 期间,Survivor 区空间不够了,直接进入老年代。
栈帧
框架用于存储数据和中间结果,以及执行动态链接、方法返回值和异常。
Local Variables Table:局部变量表。
Operand Stacks:操作数栈。
-
Dynamic Linking:指向运行时常量池的符号链接。
A() -> B(),B 方法要到常量池找,B 方法调用在栈帧上就是一个 Dynamic Linking。
Return Address:A() -> B(),B 方法的返回值应存放的位置。可以理解为方法出口。
指令集
指令集设计有两种类型:
- 基于栈的指令集:JVM 基于栈的指令集设计。
- 基于寄存器的指令集。
Hotspot 的 Local variable table 类似于寄存器。
6、GC基本算法
垃圾定义:没有任何引用指向的对象。
查找算法
Reference Count:引用计数。不能解决循环引用问题。
-
Root Searching:根可达算法。可作为 root 的对象包括:
- JVM stack:线程栈变量
- native method stack:JNI 指针
- run-time constant pool:常量池
- static references in method area:静态变量 - 方法区内部静态引用
- class
回收算法
- Mark-Sweep:标记清除 - 存活对象比较多的情况下,效率比较高,不适合 Eden 区。需要两次扫描,第一次标记,第二次清除,效率偏低。容易产生碎片。
- Copying:拷贝 - 适用于存活对象较少的情况,例如 Eden 区。只需要扫描一次,效率提高,没有碎片。缺点是空间浪费,需要移动和复制对象,指向对象的引用需要调整。
- Mark-Compact:标记压缩 - 空间连续,没有碎片,不会产生内存浪费。需要扫描两次,并移动数据,效率偏低。
7、垃圾回收器
常见垃圾回收器:1.8 版本默认 PS + ParallelOld。
Serial:年轻代,串行回收,单线程设计。单 CPU 效率最高。
SerialOld:老年代,串行回收,单线程设计,使用 mark-sweep-compact 算法。
ParallelScavenge:年轻代,串行回收,多线程设计。
ParallelOld:老年代,串行回收,多线程设计,使用 mark-compact 算法。
ParNew:年轻代,配合 CMS 的并行回收。基于 PS 做了增强,例如 CMS 某些特定阶段,ParNew 会同时运行。
-
CMS:ConcurrentMarkSweep,老年代,并发的。垃圾回收和应用程序同时运行,降低 STW 时间(200ms)。
CMS 使用标记清除,一定会产生碎片,碎片达到一定程度,使用 SerialOld 进行老年代回收。
1.4 版本开始支持,CMS 问题较多,目前没有任何版本默认支持,需要手动开启。
G1:算法 - f + SATB。
ZGC:算法 - ColloredPointers + 写屏障。
Shenandoah:算法 - ColloredPointers + 读屏障。
Eplison:一般调试时使用。
GC 和内存大小的关系:
- Serial:100Mb 以内
- PS:100Mb - 几个Gb
- CMS:20Gb
- G1:上百Gb
- ZGC:4Tb
CMS
-
运行阶段(实际6个,另外两个不重要)
- 初始标记:单线程。直接找到最根上的对象,会产生 STW,但运行很快。
- 并发标记:最浪费时间的阶段,和应用线程同时运行。
- 重新标记:多线程。会产生 STW,多数垃圾在并发标记过程中,标记完成之后产生的新垃圾,进行重新标记。新垃圾不多,停顿时间很短。
- 并发清理:清理过程中会产生浮动垃圾,需要等待下次 CMS 运行,再进行清理。
-
缺点:
- Memory Fragmentation:内存碎片化。
-XX:+UseCMSCampactAtFullCollection
-XX:CMSFullGCsBeforeCompaction
可优化此问题。 - Floating Garbage:浮动垃圾。解决方案:降低触发 CMS 的阈值。
-XX:CMSInitiatingOccupancyFraction 92%
,可以降低阈值,保持老年代有足够空间。 - CMS 的设计并不是应对大内存,由于内存碎片化,无法分配大对象,会启动 SerialOld。
- Memory Fragmentation:内存碎片化。
-
日志分析
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8511K(13696K)] 9866K(19840K), 0.0040321 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] //8511 (13696) : 老年代使用(最大) //9866 (19840) : 整个堆使用(最大) [CMS-concurrent-mark-start] [CMS-concurrent-mark: 0.018/0.018 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] //这里的时间意义不大,因为是并发执行 [CMS-concurrent-preclean-start] [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //标记Card为Dirty,也称为Card Marking [GC (CMS Final Remark) [YG occupancy: 1597 K (6144 K)][Rescan (parallel) , 0.0008396 secs][weak refs processing, 0.0000138 secs][class unloading, 0.0005404 secs][scrub symbol table, 0.0006169 secs][scrub string table, 0.0004903 secs][1 CMS-remark: 8511K(13696K)] 10108K(19840K), 0.0039567 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //STW阶段,YG occupancy:年轻代占用及容量 //[Rescan (parallel):STW下的存活对象标记 //weak refs processing: 弱引用处理 //class unloading: 卸载用不到的class //scrub symbol(string) table: //cleaning up symbol and string tables which hold class-level metadata and //internalized string respectively //CMS-remark: 8511K(13696K): 阶段过后的老年代占用及容量 //10108K(19840K): 阶段过后的堆占用及容量 [CMS-concurrent-sweep-start] [CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] //标记已经完成,进行并发清理 [CMS-concurrent-reset-start] [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //重置内部结构,为下次GC做准备
G1
-
G1 GC:Garbage First Garbage Collector - 主要运行在server端的,目标是在多核、大内存服务器上,通过并发和并行的手段,达到暂停时间比较短,维持不错的吞吐量。当开始 GC 过程时,优先收集垃圾最多的 Regions。G1 还是一种带压缩的收集器,在回收老年代分区时,将存活对象从一个分区,拷贝到另一个分区,实现了局部压缩。
G1 新生代、老年代比例是动态的,一般不要手工指定,因为这是 G1 预测停顿时间的基准。
-
特点:
- 并发收集 - 并发标记、并发回收。
- 压缩空闲空间不会延长 GC 的暂停时间。
- 更易预测的 GC 暂停时间。
使用不需要实现很高的吞吐量的场景。
-
触发
YGC:Eden 空间不足,多线程并行执行。
MixedGC:相当于 CMS,YGC之后 ,堆内存空间超过阈值,就会启动。阈值通过
-XX:InitiatingHeapOccupancyPercent=45
设置,默认 45%。-
FullGC:Old 空间不足,或手动调用
System.GC()
。优化方案:扩内存;提高CPU性能(产生对象的速度固定,GC 越快,内存空间越大);降低 MixedGC 触发的阈值,让 MixedGC 提早发生(默认45%)。
JDK10 之前使用的是 Serial,串行回收,调优目标是尽量不要产生 FGC。
-
MixedGC 过程
- 初始标记:STW
- 并发标记
- 最终标记:STW(重新标记)
- 筛选回收:STW(并行)
Region:G1 内存模型逻辑分代,物理不分代。每个分区从 1M 到 32M 不等,但都是 2 的幂次方。基础分区大小可通过参数配置。
CSset:Collection Set,一组可被回收的分区的集合,可理解为待回收的 Region 集合。在CSet中存活的数据会在 GC 过程中被移动到另一个可用分区,CSet 中的分区可以来自 Eden 空间、Survivor 空间、或者老年代。CSet 会占用不到整个堆空间的 1% 大小。
RSet:Remembered Set,记录了其他 Region 中的对象到本 Region 的引用,RSet 的价值在于使得 GC 不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描 RSet 即可。
由于 RSet 的存在,每次给对象赋值引用时,需要在 RSet 中做一些额外的记录,在 GC 中被称为写屏障(不是 JVM 的内存屏障)。CardTable:卡表。由于 YGC 时,Y区对象可能由 OLD 区对象引用,因此需要扫描整个 OLD 区,效率非常低,所以 JVM 设计了 CardTable。如果一个 OLD 区 CardTable 中有对象指向 Y 区,就将它设为 Dirty,下次扫描时,只需要扫描 Dirty Card。Card Table 使用 BitMap 实现。堆划分为相等大小的一个个区域,这个小的区域(一般 size 在128-512字节)被当做 Card,而 Card Table 维护着所有的 Card。
-
三色标记
- 白色:未被标记的对象。
- 灰色:自身被标记,成员变量未被标记。
- 黑色:自身和成员变量均已标记完成。
漏标问题:在 Remark 过程中,黑色指向了白色,且灰色指向白色的引用消失了,如果不对黑色重新扫描,则会漏标,会把白色对象当做没有新引用指向,从而回收掉。
解决漏标问题有两种方案:
Incremental Update - 增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性。CMS 使用此方案。
-
SATB - Snapshot at the beginning - 关注引用的删除,当灰色对象指向白色对象引用消失时,要把这个引用推到 GC 的堆栈,保证白色对象还能被 GC 扫描到。
GC 栈中存放的是灰色对象指向白色对象的引用。
G1 使用的是 SATB,只需要把改变过的引用重新扫描即可。当再次扫描白色对象时,仅需判断白色对象所在 Region 的 RSet 是否有引用指向该对象,不需要扫描整个堆,即可标记该对象是否为垃圾。SATB 配合 RSet 使用,浑然天成。
-
日志分析
[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0015790 secs] //young -> 年轻代 Evacuation-> 复制存活对象 //initial-mark 混合回收的阶段,这里是YGC混合老年代回收 [Parallel Time: 1.5 ms, GC Workers: 1] //一个GC线程 [GC Worker Start (ms): 92635.7] [Ext Root Scanning (ms): 1.1] [Update RS (ms): 0.0] [Processed Buffers: 1] [Scan RS (ms): 0.0] [Code Root Scanning (ms): 0.0] [Object Copy (ms): 0.1] [Termination (ms): 0.0] [Termination Attempts: 1] [GC Worker Other (ms): 0.0] [GC Worker Total (ms): 1.2] [GC Worker End (ms): 92636.9] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.0 ms] [Other: 0.1 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.0 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.0 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms] [Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)] [Times: user=0.00 sys=0.00, real=0.00 secs] //以下是混合回收其他阶段 [GC concurrent-root-region-scan-start] [GC concurrent-root-region-scan-end, 0.0000078 secs] [GC concurrent-mark-start] //无法evacuation,进行FGC [Full GC (Allocation Failure) 18M->18M(20M), 0.0719656 secs] [Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)], [Metaspace: 38 76K->3876K(1056768K)] [Times: user=0.07 sys=0.00, real=0.07 secs]
ZGC
ZGC的核心是 Colored Pointer + Load Barrier,不支持32位,不支持指针压缩。
Collored Pointer:颜色指针,GC信息记录在指针上,不记录在头部,Immediate memory use。
JDK12及以前,使用42位指针,寻址空间4T;JDK13及以后,扩展为 ,16T,因为CPU地址总线最大支持48位。
Marked0、Marked1、Remapped、Finalizable 是互斥的,同时只能有一位为1,普通对象全部为0。
Load Barrier 根据指针颜色决定是否做一些事情。
ZGC 可以做到 NUMA(Non Uniform Memory Access) Aware。
8、JVM调优
确定调优之前,应该确定是吞吐量优先(计算型任务),还是响应时间优先(响应型任务),还是在满足一定响应时间的情况下,要求达到多大的吞吐量。
-
吞吐量:用户代码执行时间 / (用户代码执行时间 + 垃圾回收执行时间)
例如科学计算、数据挖掘。GC 一般选择 PS + PO。
-
响应时间:用户线程停顿的时间短
STW 越短,响应时间越好。例如网站、API服务。GC 一般选择 G1。
调优思路
- 根据需求进行 JVM 规划和预调优。
- 优化运行 JVM 运行环境。
- 解决 JVM 运行过程中出现的各种问题。
从规划开始
调优,从业务场景开始,没有业务场景的调优都是耍流氓。
无监控(压力测试,能看到结果),不调优。
-
步骤:
-
熟悉业务场景(没有最好的垃圾回收器,只有最合适的垃圾回收器)。
确定追求吞吐量,还是响应时间。
选择回收器组合。
计算内存需求(很难计算,经验值,或经过测试和监控确定)。
选定 CPU(越高越好)。
设定年代大小、升代年龄。
-
设定日志参数:
-Xloggc:/opt/xxx-xxx-gc-%t.log -XX:+useGCLogFileRotation -XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
观察日志情况。
-
百万并发
淘宝 2019 年双 11,最大支撑 TPS 54万。12306 号称 百万并发。
9、OOM问题
排查过程中,jconsole、jvisualvm 等图形化界面工具,仅仅适用于开发及测试,不适合排查生产问题,因为JMX连接到服务后,对服务器影响很高。一般图形化界面只适用于系统上线前压测监控。
如果生产在线定位,生产一般做了高可用,停掉一台服务器,对其他服务器无影响,因此先进行隔离,停止该服务器对外提供服务,流量降为0之后,基于此服务器在线定位,进行分析。
OOM类型
- 堆溢出:
java.lang.OutOfMemoryError: Java heap space
。 - 栈溢出:
-Xss
设定太小,java.lang.StackOverflowError
。 - 方法区溢出:
java.lang.OutOfMemoryError: Compressed class space
。 - 直接内存溢出:使用Unsafe分配直接内存,或者使用NIO的问题。
排查思路
top
命令观察到问题:内存不断增长,CPU占用率居高不下。top -Hp pid
命令观察进程中的线程,哪个线程CPU和内存占比高。printf %x pid
将10进制PID转换为16进制。-
jstack -l pid
查看进程内线程状态。- nid:16进制线程ID。
- waiting on <0x0000000088拆310> (a java.lang.Object) :要找到哪个线程持有此锁。
jps
定位具体java进程。jinfo pid
查看进程JVM详细信息。-
jstat -gc pid 500
每500毫秒打印 GC 情况,动态观察 GC 情况,阅读 GC 日志发现频繁GC。响应信息很不直观,所以不常用,可通过 arthas 、jconsole 等工具观察。
-
jmap -histo pid | head -20
查找有多少对象产生。此命令对在线系统影响不是很高。此步骤很关键,数量很多的对象,往往是造成 Heap OOM 的问题所在。
Arthas 目前未提供此功能。
-
jmap -dump:format=b,file=xxx pid / jmap -histo
无论进程是否卡顿了,只要进程在,就可以导出。执行此命令,对在线系统的影响特别高。
java -Xms20M -Xmx20M -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError -jar xxx.jar
Heap OOM 时,自动产生堆转储文件。-
使用 MAT / jhat 进行 dump 文件分析。
jhat -J-Xmx512M heap.hprof
默认开启 http 7000 端口,可在浏览器查看分析结果。 找到代码的问题。
jconsole 远程连接
-
程序启动加入参数
java -Djava.rmi.server.hostname=192.168.17.11 \ -Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.port=11111 \ -Dcom.sun.management.jmxremote.authenticate=false \ -Dcom.sun.management.jmxremote.ssl=false -jar Test.jar
-
如果遇到
Local host name unknown: XXX
的错误,修改/etc/hosts
文件,把 XXX 加入进去192.168.17.11 basic localhost localhost.localdomain localhost4 1ocalhost4.1ocaldomain4 ::1 localhost 1ocalhost.1ocaldomain localhost6 1ocalhost6.1ocaldomain6
jvisualvm 远程连接
可以连接 JMX,进行实时监控。
可以进行 Heap dump 文件分析。
抽样器:内存监控 - 观察此界面哪些对象很多,且不断增长,GC回收不掉,一定是相关类代码出了问题。
线程状态说明:
- 运行:线程运行中,对应 JSTACK 中 RUNNABLE。
- 休眠:对应 sleep 操作。
- 等待:对应 wait 操作。
- 驻留:对应线程池里的空闲线程。
- 监视:对应的 synchronized 阻塞。
jprofiler
号称是最好用的,但是收费。
10、JSTACK线程状态
-
RUNNABLE:线程运行中或I/O等待。
public static void runnable() { long i = 0; while (true) { i++; } }
-
BLOCKED:等待互斥量或锁的释放。线程在等待monitor锁(synchronized关键字)。
public static void blocked() { final Object lock = new Object(); new Thread() { public void run() { synchronized (lock) { System.out.println("i got lock, but don't release"); try { Thread.sleep(1000L * 1000); } catch (InterruptedException e) { } } } }.start(); try { Thread.sleep(100); } catch (InterruptedException e) { } synchronized (lock) { try { Thread.sleep(30 * 1000); } catch (InterruptedException e) { } } }
-
TIMED_WAITING:线程在等待唤醒,但设置了时限。Lock.tryLock(30000) 也会触发此状态。
public static void timedWaiting() { final Object lock = new Object(); synchronized (lock) { try { lock.wait(30 * 1000); } catch (InterruptedException e) { } } }
-
WAITING:线程在无限等待唤醒。
public static void waiting() { final Object lock = new Object(); synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { } } }
11、Arthas
- 启动:
java -jar arthas-boot.jar
,之后选择进程:1
。 - 查看JVM详细配置情况:
jvm
,类似 JAVA 中jinfo
命令。 - 实时数据面板:
dashboard
。 - 查看线程栈:
thread 1
。支持管道,thread 1 | rep 'main('
可以查找到main class
。 - 导出堆文件:
heapdump [filepath]
生产环境慎用,影响很大。 - 查找类:
sc -d *MathGame
。 - 反编译:
jad demo.Test
。 - 监控函数参数/返回值/异常信息:
watch demo.MathGame primeFactors "{params,target,returnObj}" [-x 2] [-b] [-s] [-n 2]
。 - 退出:临时退出(可再次连接):
exit
或quit
;彻底退出:stop
。 - 热替换:
redefine /path/Test.class
目前有限制条件:只能改方法实现,不能改方法名,不能改属性。
12、常用参数
参数分类
- 标准:- 开头,所有 HotSpot 都支持。
- 非标准:-X 开头,特定版本 HotSpot 支持。通过
java -X
查看。 - 不稳定:-XX 开头,下个版本可能取消。通过
java -XX:+PrintFlagsFinal -version
查看。
常用GC参数组合(1.8版)
Linux 中没找到默认 GC 的查看方法,而 Windows 中会打印 UseParallelGC。
可通过-XX:+PrintCommandLineFlags -version
(仅在 Windows 有打印) 或通过 GC 日志来分辨。
1.8.0_222 默认 PS + PO。
-
-XX:+UseSerialGC
:Serial New(DefNew) + Serial Old适用于小型程序。
-
-XX:+UseParNewGC
:ParNew + SerialOld此组合很少适用(某些版本已废弃)。
-XX:+UseConcMarkSweepGC
: ParNew + CMS + SerialOld-
-XX:+UseParallelGC
:Parallel Scavenge + Parallel Old1.8 版本默认配置。
-XX:+UseParallelOldGC
:Parallel Scavenge + Parallel Old-XX:+UseG1GC
:G1
JVM常用参数
-
-Xmn
年轻代大小。 -
-Xms
最小堆大小。 -
-Xmx
最大堆大小。 -
-Xss
栈空间大小。 -
-XX:MaxMetaspaceSize
:方法区大小。 -
-XX:+PrintVMOptions
打印JVM启动参数。 -
-XX:+PrintFlagsFinal
打印JVM参数。 -
-XX:+PrintFlagsInitial
初始化默认参数。 -
-verbose:class
类加载详细过程。 -
-XX:-DisableExplicitGC
屏蔽 System.gc() 显式调用。 -
-XX:MaxTenuringThreshold
升代年龄,最大值15。 -
-XX:+HeapDumpOnOutOfMemoryError
:OOM 时,自动 Memory Dump。 -
-XX:+UseTLAB
使用TLAB,默认打开,不建议调整。 -
-XX:+PrintTLAB
打印TLAB的使用情况,不建议调整。 -
-XX:TLABSize
设置TLAB大小,不建议调整。 -
-XX:PreBlockSpin
锁自旋次数,不建议调整。 -
-XX:CompileThreshold
热点代码检测参数,不建议调整。
GC日志参数
-
-Xloggc:/path/logs/gc.log
日志文件目录。 -
-XX:+PrintGC
:打印GC信息。 -
-XX:+PrintGCDetails
:打印详细GC信息。 -
-XX:+PrintGCCause
:打印GC产生的原因。 -
-XX:+PrintHeapAtGC
GC时打印堆栈情况。 -
-XX:+PrintGCTimeStamps
:打印GC产生时详细系统时间。 -
-XX:+PrintGCDateStamps
:打印GC产生的日期+时间。 -
-XX:+PrintGCApplicationConcurrentTime
(低)打印应用程序时间。 -
-XX:+PrintGCApplicationStoppedTime
(低)打印应用程序暂停时长。 -
-XX:+PrintReferenceGC
(低)记录回收了多少种不同引用类型的引用。
Parallel常用参数
-
-XX:SurvivorRatio
幸存区比例。 -
-XX:PreTenureSizeThreshold
大对象体积。 -
-XX:MaxTenuringThreshold
生代年龄,最大15。 -
-XX:+ParallelGCThreads
并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同。 -
-XX:+UseAdaptiveSizePolicy
自动选择各区大小比例。
CMS常用参数
-
-XX:+UseConcMarkSweepGC
指定 GC 为 CMS。 -
-XX:ParallelCMSThreads
CMS 线程数量。 -
-XX:CMSInitiatingOccupancyFraction
老年代使用多少比例后,启动CMS,默认 68%(近似值)。 -
-XX:+UseCMSCompactAtFullCollection
在 FGC 时进行压缩。解决 -
-XX:CMSFullGCsBeforeCompaction
多少次 FGC 后进行压缩。 -
-XX:+CMSClassUnloadingEnabled
回收永久代。 -
-XX:CMSInitiatingPermOccupancyFraction
达到多少比例时,进行 Perm 回收。 -
-XX:GCTimeRatio
设置 GC 时间占应用程序运行时间的百分比。 -
-XX:MaxGCPauseMillis
停顿时间,是一个建议值,GC 会尝试用各种手段达到这个时间,比如减小年轻代。
G1常用参数
-XX:+UseG1GC
指定 GC 为 G1。-XX:MaxGCPauseMillis
建议值,G1会尝试调整 Young 区的块数来达到这个值。-XX:GCPauseIntervalMillis
GC的间隔时间。-XX:+G1HeapRegionSize
分区大小,建议逐渐增大该值,1 2 4 8 16 32。
随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长。ZGC做了改进(动态区块大小)。-XX:G1NewSizePercent
新生代最小比例,默认为5%。-XX:G1MaxNewSizePercent
新生代最大比例,默认为60%。-XX:GCTimeRatio
GC时间建议比例,G1会根据这个值调整堆空间。-XX:ConcGCThreads
线程数量。-XX:InitiatingHeapOccupancyPercent
启动G1的堆空间占用比例。
13、案例
- 案例1:垂直电商,最高每日百万订单,处理订单系统需要什么样的服务器配置?
这个问题比较业余,因为很多不同的服务器配置都能支撑。
找到最巅峰的瞬间,例如某个小时内产生40万订单,做到能够支撑平均 100订单/秒即可。
关于内存,可以计算一个订单产生需要多少内存。一般 512K 就能存储很多数据了。
专业的问法:要求相应时间 100ms。
压测!最简单的是加机器。
- 案例2:12306遭遇春节大规模抢票应该如何支撑?
12306应该是中国并发量最大的秒杀网站,号称并发量 100W 最高。
CDN -> LVS -> NGINX -> 业务系统 -> 每台机器1W并发(单机10K问题) 100台机器
普通电商订单 -> 下单 -> 订单系统(IO)减库存 -> 等待用户付款
12306的一种可能的模型:下单 -> 减库存和订单(redis kafka)同时异步进行 -> 等待付款
减库存最后还会把压力压到一台服务器
可以做分布式本地库存 + 单独服务器做库存均衡
-
案例3:怎么得到一个事务会消耗多少内存?
- 弄台机器,看能承受多少TPS?是不是达到目标?扩容或调优,让它达到。
- 用压测来确定。
-
案例4:硬件升级,系统反而卡顿。
有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了,为什么?如何优化?
原网站由于内存较低,很多数据 load 到内存,内存不足,会频繁GC,响应时间变慢。
升级后,内存扩大了,但 GC 没有调整,GC 和 YGC 频率变低了,但 STW 时间更长了。
可以更换 PS + PO 为 PN + CMS 或 G1。
-
案例5:系统CPU经常100%,如何调优?
CPU 100% 一定有线程在占用系统资源:
-
top
命令找出哪个进程 CPU 高。 -
top -Hp
命令找出该进程中哪个线程 CPU 高。 -
jstack
命令导出该线程的堆栈。 -
jstack
命令查找哪个方法(栈帧)消耗时间。 - 需要确定是工作线程,还是GC线程占比高。
-
-
案例6:系统内存飙高,如何查找问题?
内存飚高,一定是堆内存占用比较多:
-
jmap
导出堆内存。 -
jhat
jvisualvm
mat
jprofiler
等工具分析。
-
-
案例7:JIRA 问题 - 全球协同办公,多地在使用的线上系统,系统不停的 FGC,使用十分卡顿,但是能用,实在用不了的情况下重启。启动参数:
_> /opt/atlassian/jira/jre/bin/java \ _> -Djava.util.logging.config.file=/opt/atlassian/jira/conf/logging.properties \ _> -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \ _> -Xms1024m \ _> -Xmx9216m \ _> -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 \ _> -Datlassian.plugins.startup.options= \ _> -Djdk.ephemeralDHKeySize=2048 \ _> -Djava.protocol.handler.pkgs=org.apache.catalina.webresources \ _> -Xloggc:/opt/atlassian/jira/logs/atlassian-jira-gc-%t.log \ _> -XX:+UseGCLogFileRotation \ _> -XX:NumberOfGCLogFiles=5 \ _> -XX:GCLogFileSize=20M \ _> -XX:+PrintGCDetails \ _> -XX:+PrintGCDateStamps \ _> -XX:+PrintGCTimeStamps \ _> -XX:+PrintGCCause \ _> -classpath /opt/atlassian/jira/bin/bootstrap.jar:/opt/atlassian/jira/bin/tomcat-juli.jar \ _> -Dcatalina.base=/opt/atlassian/jira \ _> -Dcatalina.home=/opt/atlassina/jira \ _> -Djava.io.tmpdir=/opt/atlassina/jira/temp \ _> org.apache.catalina.startup.Bootstrap start
解决过程:
- 调整堆内存
-Xms9216M -Xmx9216M
,阻止弹性扩容缩。 - 由于不能再生产使用
jmap
, 增加-XX:+HeapDumpOnOutOfMemoryError
参数,宕机时导出堆。 - 将 JVM 内存调整到64G,调整 GC 为 G1,之后运行一个月没有出现卡顿,运行正常。
- 直到最后问题解决,也没有找到原因。
- 调整堆内存
-
案例8:Lambda 表达式导致方法区溢出问题
Lambda 表达式会对每一个对象实例产生内部类(新的 class),GC 回收不过来,最终抛出
java.lang.OutOfMemoryError: Compressed class space
异常。方法区的清理,每个GC不同,有些GC不会清理,有些GC会清,但条件很苛刻(不存在该 class 对象)。这件事情很少发生。
-
案例9:重写 finalize 引发频繁GC
小米云,HBase 同步系统,系统通过 nginx 访问超时报警,最后排查,C++ 程序员重写 finalize 引发频繁 GC 问题。
为什么 C++ 程序员会重写 finalize?C++ 语言中,需要手动回收内存,
new
调用构造函数开辟内存,delete
调用析构函数回收内存。由于重写了 finalize 函数,每次回收执行大量逻辑代码,耗时较长,导致 GC 回收不过来。
-
案例10:Disruptor OOM 问题
Disruptor 可以设置链的长度,如果过大,且对象很大,消费完不主动释放,会产生溢出。
-
案例11:内存一直消耗不超过10%,FGC 总是频繁发生。
手动调用了
System.gc()
。可以设置 JVM 参数
-XX:-DisableExplicitGC
屏蔽 GC 显式调用。
附录:常用命令
查看非标参数:
java -X
查看不稳定参数:
java -XX:+PrintFlagsFinal -version
打印启动参数:
-XX:+PrintCommandLineFlags -version
Windows 包含 GC。