- 加载
Java
将字节码数据从不同的数据源读取到JVM
中,并映射为JVM
认可的数据结构(Class
对象)- 数据源可能是各种各样的形态,如
jar
文件、class
文件,甚至是网络数据源
- 数据源可能是各种各样的形态,如
- 链接
- 把原始的类定义信息平滑地转化入
JVM
运行的过程中 - 验证
- 核验字节信息是符合
Java
虚拟机规范的,否则就被认为是VerifyError
- 核验字节信息是符合
- 准备
- 创建类或接口中的静态变量,并初始化静态变量的初始值
- 分配所需要的内存空间,不会去执行更进一步的
JVM
指令
- 解析
- 将常量池中的符号引用替换为直接引用
- 把原始的类定义信息平滑地转化入
- 初始化
- 真正去执行类初始化的代码逻辑
- 静态字段赋值的动作
- 执行类定义中的静态初始化块内的逻辑
- 父类型的初始化逻辑优先于当前类型的逻辑
- 真正去执行类初始化的代码逻辑
当类加载器(Class-Loader
)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。
- 作用
- 避免重复加载 Java 类型
示例:
public class CLPreparation {
public static int a = 100;
public static final int INT_CONSTANT = 1000;
public static final Integer INTEGER_CONSTANT = 10000;
}
编译为字节码后并输出字节码文件的信息:
Javap –v CLPreparation.class
输出的内容信息:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #2 // Field a:I
5: sipush 10000
8: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
11: putstatic #4 // Field INTEGER_CONSTANT:Ljava/lang/Integer;
14: return
LineNumberTable:
line 3: 0
line 5: 5
}
普通原始类型静态变量和引用类型(即使是常量),是需要额外调用 putstatic
等 JVM
指令的
-
启动类加载器
- 加载
jre/lib
下面的jar
文件,如rt.jar
。它是个超级公民
- 加载
修改 JDK
的基础代码:
# 指定新的bootclasspath,替换java.*包的内部实现
java -Xbootclasspath:<your_boot_classpath> your_App
# a意味着append,将指定目录添加到bootclasspath后面
java -Xbootclasspath/a:<your_dir> your_App
# p意味着prepend,将指定目录添加到bootclasspath前面
java -Xbootclasspath/p:<your_dir> your_App
-
扩展类加载器
- 加载我们放到
jre/lib/ext/
目录下面的jar
包,这就是所谓的extension
机制。 - 覆盖方式:设置
java.ext.dirs
- 加载我们放到
-
应用类加载器
- 加载我们最熟悉的
classpath
的内容 - 系统(
System
)类加载器- 默认就是
JDK
内建的应用类加载器
- 默认就是
- 加载我们最熟悉的
修改 JDK
的内建类加载器:
java -Djava.system.class.loader=com.yourcorp.YourClassLoader HelloWorld
- 双亲委派模型
- 特殊情况
JNDI
、JDBC
、文件系统、Cipher
都是是可能要加载用户代码的- 利用所谓的上下文加载器
- 特殊情况
- 可见性
- 子类加载器可以访问父加载器加载的类型
- 反向不行
- 单一性
- 父加载器的类型对于子加载器是可见的
- 父加载器中加载过的类型,就不会在子加载器中重复加载
- 类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。
对相应模块的修补:
确认要修改的类文件已经编译好,并按照对应模块(假设是 java.base)结构存放
java --patch-module java.base=your_patch yourApp
- 扩展类加载器被重命名为平台类加载器
- 部分不需要
AllPermission
的Java
基础模块,被降级到平台类加载器中,相应的权限也被更精细粒度地限制起来。 rt.jar
和tools.jar
同样是被移除了!- 增加了
Layer
的抽象,JVM
启动默认创建BootLayer
目前的 JVM 内部结构就变成了下面的层次,
内建类加载器都在 BootLayer
中,
其他 Layer
内部有自定义的类加载器,
不同版本模块可以同时工作在不同的 Layer
常见场景
- 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。
- 应用需要从不同的数据源获取类定义信息
- 需要自己操纵字节码,动态修改或者生成类型
自定义类加载过程:
- 通过指定名称,找到其二进制实现
- 创建
Class
对象,并完成类加载过程
有什么通用办法,不需要代码和其他工作量,就可以降低类加载的开销呢?
- AOT
- 直接编译成机器码,降低的其实主要是解释和编译开销
AppCDS
(Application Class-Data Sharing)
AppCDS
基本原理和工作过程:
- JVM 将类信息加载, 解析成为元数据
- 根据需求是否需要修改进行分类:
Read-Only
部分Read-Write
部分
这些元数据直接存储在文件系统中,作为所谓的 Shared Archive
命令:
Java -Xshare:dump -XX:+UseAppCDS -XX:SharedArchiveFile=<jsa> \
-XX:SharedClassListFile=<classlist> -XX:SharedArchiveConfigFile=<config_file>
应用程序启动时,指定归档文件,并开启 AppCDS
:
Java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=<jsa> yourApp
JVM
会通过内存映射技术,直接映射到相应的地址空间,免除了类加载、解析等各种开销。
使用 Java Compiler API
JDK
提供的标准 API
,里面提供了与 javac
对等的编译器功能
如何让 JVM
加载字节码?
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
ProxyGenerator
生成字节码,并以 byte 数组的形式保存,然后通过调用 Unsafe
提供的 defineClass
入口。
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
try {
Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile,
0, proxyClassFile.length,
loader, null);
reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);
return pc;
} catch (ClassFormatError e) {
// 如果出现ClassFormatError,很可能是输入参数有问题,比如,ProxyGenerator有bug
}
使用硬编码的方式生成字节码:
import java.io.DataOutputStream;
import java.io.IOException;
import static sun.tools.java.RuntimeConstants.opc_wide;
public class GenerateHardCodedBitCoce {
public static void main(String[] args) {
}
private void codeLocalLoadStore(int lvar, int opcode, int opcode_0, DataOutputStream out) throws IOException {
assert lvar >= 0 && lvar <= 0xFFFF;
// 根据变量数值,以不同格式,dump操作码
if (lvar <= 3) {
out.writeByte(opcode_0 + lvar);
} else if (lvar <= 0xFF) {
out.writeByte(opcode);
out.writeByte(lvar & 0xFF);
} else {
// 使用宽指令修饰符,如果变量索引不能用无符号byte
out.writeByte(opc_wide);
out.writeByte(opcode);
out.writeShort(lvar & 0xFFFF);
}
}
}
普通的 Java
动态代理,其实现过程可以简化成为:
- 提供一个基础的接口,作为被调用类型和代理类之间的统一入口
- 实现
InvocationHandler
,对代理对象方法的调用,会被分派到其invoke
方法来真正实现动作。 - 通过
Proxy
类,调用其newProxyInstance
方法,生成一个实现了相应基础接口的代理类实例
动态代码生成是具体发生在什么阶段?
是在
newProxyInstance
生成代理类实例的时候
第一步,生成对应的类:
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_8, // 指定Java版本
ACC_PUBLIC, // 说明是public类型
"com/mycorp/HelloProxy", // 指定包和类的名称
null, // 签名,null表示不是泛型
"java/lang/Object", // 指定父类
new String[]{ "com/mycorp/Hello" }); // 指定需要实现的接口
按照需要为代理对象实例,生成需要的方法和逻辑:
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC, // 声明公共方法
"sayHello", // 方法名称
"()Ljava/lang/Object;", // 描述符
null, // 签名,null表示不是泛型
null); // 可能抛出的异常,如果有,则指定字符串数组
mv.visitCode();
// 省略代码逻辑实现细节
cw.visitEnd(); // 结束类字节码生成
字节码操纵技术,除了动态代理,还可以应用在什么地方?
- 各种 Mock 框架
- ORM 框架
- IOC 容器
- 部分 Profiler 工具,或者运行时诊断工具等
- 生成形式化代码的工具
分析一个简单的 Hello
程序:
在源码目录下通过以下命令执行命令:
java -XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics Hello
NMT 的输出:
Native Memory Tracking:
Total: reserved=10092994KB, committed=649038KB
- Java Heap (reserved=8388608KB, committed=528384KB)
(mmap: reserved=8388608KB, committed=528384KB)
- Class (reserved=1056878KB, committed=4974KB)
(classes #487)
( instance classes #413, array classes #74)
(malloc=110KB #539)
(mmap: reserved=1056768KB, committed=4864KB)
( Metadata: )
( reserved=8192KB, committed=4352KB)
( used=140KB)
( free=4212KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=1048576KB, committed=512KB)
( used=7KB)
( free=505KB)
( waste=0KB =0.00%)
- Thread (reserved=17482KB, committed=17482KB)
(thread #17)
(stack: reserved=17408KB, committed=17408KB)
(malloc=56KB #104)
(arena=18KB #32)
- Code (reserved=247724KB, committed=7584KB)
(malloc=36KB #396)
(mmap: reserved=247688KB, committed=7548KB)
- GC (reserved=367866KB, committed=76178KB)
(malloc=22742KB #2190)
(mmap: reserved=345124KB, committed=53436KB)
- Compiler (reserved=167KB, committed=167KB)
(malloc=3KB #40)
(arena=165KB #5)
- Internal (reserved=550KB, committed=550KB)
(malloc=518KB #915)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=1130KB, committed=1130KB)
(malloc=770KB #27)
(arena=360KB #1)
- Native Memory Tracking (reserved=126KB, committed=126KB)
(malloc=5KB #60)
(tracking overhead=122KB)
- Shared class space (reserved=11284KB, committed=11284KB)
(mmap: reserved=11284KB, committed=11284KB)
- Arena Chunk (reserved=974KB, committed=974KB)
(malloc=974KB)
- Logging (reserved=5KB, committed=5KB)
(malloc=5KB #196)
- Arguments (reserved=13KB, committed=13KB)
(malloc=13KB #418)
- Module (reserved=59KB, committed=59KB)
(malloc=59KB #1038)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
- Synchronization (reserved=119KB, committed=119KB)
(malloc=119KB #1773)
参数
-XX:NativeMemoryTracking=summary
开启 NMT 并选择 summary 模式,
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics
在应用退出时打印 NMT 统计信息
-
Class
内存占用,它所统计的就是Java
类元数据所占用的空间- 调整参数:
-XX:MaxMetaspaceSize=value
- 调整参数:
-
调整启动类加载器元数据区
-XX:InitialBootClassLoaderMetaspaceSize=30720
面对进程时间较为短暂的函数式服务,如何降低内存的占用率
- 降低其并行线程数目
- 切换
GC
类型
JIT
编译默认开启了 TieredCompilation
关闭 TieredCompilation
并 更换 GC
java -XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:-TieredCompilation -XX:+UseParallelGC Hello
输出内容如下:
Native Memory Tracking:
Total: reserved=9862960KB, committed=874120KB
- Java Heap (reserved=8388608KB, committed=524288KB)
(mmap: reserved=8388608KB, committed=524288KB)
- Class (reserved=1056879KB, committed=4975KB)
(classes #501)
( instance classes #426, array classes #75)
(malloc=111KB #537)
(mmap: reserved=1056768KB, committed=4864KB)
( Metadata: )
( reserved=8192KB, committed=4352KB)
( used=121KB)
( free=4231KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=1048576KB, committed=512KB)
( used=6KB)
( free=506KB)
( waste=0KB =0.00%)
- Thread (reserved=12341KB, committed=12341KB)
(thread #11)
(stack: reserved=12288KB, committed=12288KB)
(malloc=40KB #74)
(arena=13KB #23)
- Code (reserved=49562KB, committed=2542KB)
(malloc=26KB #318)
(mmap: reserved=49536KB, committed=2516KB)
- GC (reserved=341736KB, committed=316140KB)
(malloc=35256KB #185)
(mmap: reserved=306480KB, committed=280884KB)
- Compiler (reserved=164KB, committed=164KB)
(malloc=1KB #21)
(arena=163KB #3)
- Internal (reserved=535KB, committed=535KB)
(malloc=503KB #882)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=1151KB, committed=1151KB)
(malloc=791KB #1389)
(arena=360KB #1)
- Native Memory Tracking (reserved=90KB, committed=90KB)
(malloc=3KB #42)
(tracking overhead=87KB)
- Shared class space (reserved=11284KB, committed=11284KB)
(mmap: reserved=11284KB, committed=11284KB)
- Arena Chunk (reserved=492KB, committed=492KB)
(malloc=492KB)
- Logging (reserved=5KB, committed=5KB)
(malloc=5KB #196)
- Arguments (reserved=13KB, committed=13KB)
(malloc=13KB #418)
- Module (reserved=59KB, committed=59KB)
(malloc=59KB #1038)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
- Synchronization (reserved=33KB, committed=33KB)
(malloc=33KB #416)
Thread 从 17 降到了 11
Code
统计信息,也就是 CodeCache
相关内存,即 JIT compiler
存储编译热点方法等信息的地方
设置初始值和最大值的参数
## 初始值
-XX:InitialCodeCacheSize=value
## 保存code 缓存的大小
-XX:ReservedCodeCacheSize=value
我们进行下一步的实验:
java -XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:-TieredCompilation -XX:+UseParallelGC -XX:InitialCodeCacheSize=4096 Hello
这是实验的输出值:
Native Memory Tracking:
Total: reserved=9862960KB, committed=872060KB
- Java Heap (reserved=8388608KB, committed=524288KB)
(mmap: reserved=8388608KB, committed=524288KB)
- Class (reserved=1056879KB, committed=4975KB)
(classes #501)
( instance classes #426, array classes #75)
(malloc=111KB #537)
(mmap: reserved=1056768KB, committed=4864KB)
( Metadata: )
( reserved=8192KB, committed=4352KB)
( used=121KB)
( free=4231KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=1048576KB, committed=512KB)
( used=6KB)
( free=506KB)
( waste=0KB =0.00%)
- Thread (reserved=12341KB, committed=12341KB)
(thread #11)
(stack: reserved=12288KB, committed=12288KB)
(malloc=40KB #74)
(arena=13KB #23)
- Code (reserved=49562KB, committed=482KB)
(malloc=26KB #318)
(mmap: reserved=49536KB, committed=456KB)
- GC (reserved=341736KB, committed=316140KB)
(malloc=35256KB #185)
(mmap: reserved=306480KB, committed=280884KB)
- Compiler (reserved=164KB, committed=164KB)
(malloc=1KB #21)
(arena=163KB #3)
- Internal (reserved=535KB, committed=535KB)
(malloc=503KB #883)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=1151KB, committed=1151KB)
(malloc=791KB #1389)
(arena=360KB #1)
- Native Memory Tracking (reserved=90KB, committed=90KB)
(malloc=3KB #42)
(tracking overhead=87KB)
- Shared class space (reserved=11284KB, committed=11284KB)
(mmap: reserved=11284KB, committed=11284KB)
- Arena Chunk (reserved=492KB, committed=492KB)
(malloc=492KB)
- Logging (reserved=5KB, committed=5KB)
(malloc=5KB #196)
- Arguments (reserved=13KB, committed=13KB)
(malloc=13KB #418)
- Module (reserved=59KB, committed=59KB)
(malloc=59KB #1038)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
- Synchronization (reserved=33KB, committed=33KB)
(malloc=33KB #416)
Remembered Set 通常都会占用 20%~30% 的堆空间。
改为序列化的方式
增加以下的参数
-XX:+UseSerialGC
运行
java -XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:-TieredCompilation -XX:+UseSerialGC -XX:InitialCodeCacheSize=4096 Hello
输出结果:
Native Memory Tracking:
Total: reserved=9547523KB, committed=556623KB
- Java Heap (reserved=8388608KB, committed=524288KB)
(mmap: reserved=8388608KB, committed=524288KB)
- Class (reserved=1056879KB, committed=4975KB)
(classes #501)
( instance classes #426, array classes #75)
(malloc=111KB #536)
(mmap: reserved=1056768KB, committed=4864KB)
( Metadata: )
( reserved=8192KB, committed=4352KB)
( used=121KB)
( free=4231KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=1048576KB, committed=512KB)
( used=6KB)
( free=506KB)
( waste=0KB =0.00%)
- Thread (reserved=11313KB, committed=11313KB)
(thread #11)
(stack: reserved=11264KB, committed=11264KB)
(malloc=37KB #68)
(arena=12KB #21)
- Code (reserved=49562KB, committed=482KB)
(malloc=26KB #318)
(mmap: reserved=49536KB, committed=456KB)
- GC (reserved=27336KB, committed=1740KB)
(malloc=24KB #118)
(mmap: reserved=27312KB, committed=1716KB)
- Compiler (reserved=164KB, committed=164KB)
(malloc=1KB #21)
(arena=163KB #3)
- Internal (reserved=531KB, committed=531KB)
(malloc=499KB #813)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=1151KB, committed=1151KB)
(malloc=791KB #1389)
(arena=360KB #1)
- Native Memory Tracking (reserved=87KB, committed=87KB)
(malloc=3KB #35)
(tracking overhead=85KB)
- Shared class space (reserved=11284KB, committed=11284KB)
(mmap: reserved=11284KB, committed=11284KB)
- Arena Chunk (reserved=492KB, committed=492KB)
(malloc=492KB)
- Logging (reserved=5KB, committed=5KB)
(malloc=5KB #196)
- Arguments (reserved=13KB, committed=13KB)
(malloc=13KB #418)
- Module (reserved=59KB, committed=59KB)
(malloc=59KB #1038)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
- Synchronization (reserved=33KB, committed=33KB)
(malloc=33KB #407)
- 内存占用(
footprint
: 英文直译是脚印) - 延时(
latency
:直译是潜在因素) - 吞吐量(
throughput
)
OOM
也可能与不合理的GC
相关参数有关- 应用启动速度方面的需求
-
理解应用需求和问题,确定调优目标。
-
掌握
JVM
和GC
的状态,定位具体的问题,确定真的有GC
调优的必要- jstat 等工具查看 GC 等相关状态
- 开启
GC
日志,或者是利用操作系统提供的诊断工具
-
选择的
GC
类型是否符合我们的应用特征- 是
Minor GC
过长,还是Mixed GC
等出现异常停顿情况 CMS
和G1
都是更侧重于低延迟的GC
- 是
-
分析确定具体调整的参数或者软硬件配置
-
验证是否达到调优目标,如果达到目标,即可以考虑结束调优
- 其内部是类似棋盘状的一个个
region
组成 region
的大小是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数G1
会将超过region
50% 大小的对象(在应用中,通常是byte
或char
数组)归类为Humongous
(巨大的) 对象- Humongous region 算是老年代的一部分
region
大小和大对象很难保证一致,这会导致空间的浪费
- 在新生代,
G1
采用的仍然是并行的复制算法,所以同样会发生Stop-The-World
的暂停。 - 在老年代,大部分情况下都是并发标记,是增量进行的
- 人们喜欢把新生代
GC
(Young GC
)叫作Minor GC
- 老年代
GC
叫作Major GC
Minor GC
仍然存在,虽然具体过程会有区别。涉及Remembered Set
等相关处理- 老年代回收,则是依靠
Mixed GC
具体的参数:
# 触发阈值,并且设定最多被包含在一次 Mixed GC 中的 region 比例。
–XX:G1MixedGCLiveThresholdPercent
–XX:G1OldCSetRegionThresholdPercent
G1
的很多开销都是源自Remembered Set
Humongous
对象的分配和回收
Humongous region
作为老年代的一部分,通常认为它会在并发标记结束后才进行回收,但是在新版 G1 中,Humongous 对象回收采取了更加激进的策略。 阻止它被回收的唯一可能,就是新生代是否有对象引用了它,但这个信息是可以在 Young GC 时就知道的
现代的 G1 已经不是如此了,8u40 以后,G1 增加并默认开启下面的选项
-XX:+ClassUnloadingWithConcurrentMark
并发标记阶段结束后,JVM 即进行类型卸载. 最新的 JDK, Full GC 也是并行进行的了,在通用场景中的表现还优于 Parallel GC 的 Full GC 实现。
- 建议尽量升级到较新的 JDK 版本
- 掌握
GC
调优信息收集途径。掌握尽量全面、详细、准确的信息,是各种调优的基础,不仅仅是 GC 调优。
常用的选项:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
其他选项:特定问题的诊断都是要依赖这些选项
-XX:+PrintAdaptiveSizePolicy // 打印G1 Ergonomics相关信息
引用清理不及时的情况:
##打开
-XX:+PrintReferenceGC
##开启选项下面的选项进行并行引用处理
-XX:+ParallelRefProcEnabled
JDK 9
中JVM
和GC
日志机构进行了重构,其实我前面提到的PrintGCDetails
已经被标记为废弃,而PrintGCDateStamps
已经被移除
可使用以下参数进行:
java -Xlog:help
Young GC
非常耗时,这很可能就是因为新生代太大了,我们可以:
## 减小新生代的最小比例
-XX:G1NewSizePercent
## 降低其最大值
-XX:G1MaxNewSizePercent
可以利用下面参数提高 Mixed GC 的个数
-XX:G1MixedGCCountTarget
是
Java
内存模型中保证多线程操作可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精确定义
-
线程内执行的每个操作,都保证
happen-before
后面的操作 -
对于
volatile
变量,对它的写操作,保证happen-before
在随后对该变量的读取操作 -
对于一个锁的解锁操作,保证
happen-before
加锁操作 -
对象构建完成,保证
happen-before
于finalizer
的开始动作 -
类似线程内部操作的完成,保证
happen-before
其他Thread.join()
的线程 -
happen-before 关系是存在着传递性的
- 明确目的,克制住技术的诱惑。除非你是编译器或者
JVM
工程师 - 克制住对“秘籍”的诱惑。尽量遵循语言规范进行
-
简化多线程编程、保证程序可移植性
-
让普通
Java
开发者和编译器、JVM
工程师,能够清晰地达成共识 -
相对简单并准确地判断出,多线程程序什么样的执行序列是符合规范的。
JMM 内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式,提供内存可见性保证,也就是实现了各种 happen-before 规则
volatile
变量的可见性发生了增强,能够起到守护其上下文的作用
Docker
其内存、CPU
等资源限制是通过 CGroup
(Control Group)实现的
早期的 JDK 不能识别这些限制
- 未配置合适的
JVM
堆和元数据区、直接内存等参数,Java
就有可能试图使用超过容器限制的内存,最终被容器OOM kill
,或者自身发生OOM
- 错误判断了可获取的
CPU
资源 - 镜像非常多的时候,镜像的存储等开销就比较明显了。
Java
自身的大小、内存占用、启动速度,都存在一定局限性
-
Docker
并不是一种完全的虚拟化技术,而更是一种轻量级的隔离技术。 -
Docker
基于namespace
,为每个容器提供了单独的命名空间,对网络、PID
、用户、IPC
通信、文件系统挂载点等实现了隔离 -
Docker
通过CGroup
进行管理管理计算资源 -
Docker
仅在类似Linux
内核之上实现了有限的隔离和虚拟化
- 容器环境对于计算资源的管理方式是全新的,
CGroup
作为相对比较新的技术,历史版本的 Java 显然并不能自然地理解相应的资源限制 namespace
对于容器内的应用细节增加了一些微妙的差异,比如jcmd
、jstack
等工具会依赖于“/proc//”下面提供的部分信息Docker
的设计改变了这部分信息的原有结构
JVM
会大概根据检测到的内存大小,设置最初启动时的堆大小为系统内存的1/64
JVM
检测到系统的CPU
核数,则直接影响到了Parallel GC
的并行线程数目 、JIT complier
线程数目- 由于容器环境的差异,
Java
的判断很可能是基于错误信息而做出的 JVM
的一些原有诊断或备用机制也会受到影响。- 为保证服务的可用性,一种常见的选择是依赖“-XX:OnOutOfMemoryError”功能
- 这种机制是基于 fork 实现的,当 Java 进程已经过度提交内存时,fork 新的进程往往已经不可能正常运行了
- 为保证服务的可用性,一种常见的选择是依赖“-XX:OnOutOfMemoryError”功能
-
升级到最新的
JDK
版本 -
针对内存限制,可以使用下面的参数设置
## 只支持 Linux 环境。而对于 CPU 核心数限定,
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap
实践中发现有问题,也可以使用“-XX:-UseContainerSupport”,关闭 Java 的容器支持特性,这可以作为一种防御性机制,避免新特性破坏原有基础功能
在环境中,这样限制容器内存
$ docker run -it --rm --name yourcontainer -p 8080:8080 -m 800M repo/your-java-container:openjdk
额外配置下面的环境变量,直接指定 JVM
堆大小
-e JAVA_OPTIONS='-Xmx300m'
明确配置 GC
和 JIT
并行线程数目,以避免二者占用过多计算资源
-XX:ParallelGCThreads
-XX:CICompilerCount
明确告知 JVM
系统内存限额。
-XX:MaxRAM=`cat /sys/fs/cgroup/memory/memory.limit_in_bytes`
也可以指定 Docker 运行参数
--memory-swappiness=0
java-buildpack-memory-calculator
对于容器镜像大小的问题,如果你使用的是 JDK 9
以后的版本,完全可以使用 jlink
工具定制最小依赖的 Java
运行环境,将 JDK
裁剪为几十 M 的大小,这样运行起来并不困难。
-
清晰地定位问题:
- 是长时间的还是突发性的
- 是否重复出现
- 是系统对其他方面的请求的反应延时变长吗
-
理清问题的症状:
- 问题可能来自于
Java
服务自身,也可能仅仅是受系统里其他服务的影响- 一些
Java
诊断工具也可以用于这个诊断,例如通过JFR
- 一些
- 监控应用是否大量出现了某种类型的异常。
- 有:异常可能就是个突破点
- 无: 先检查系统级别的资源等情况
- CPU、内存等资源是否被其他进程大量占用
- 并且这种占用是否不符合系统正常运行状况
- 监控
Java
服务自身,例如GC
日志里面是否观察到Full GC
等恶劣情况出现- 利用
jstat
等工具,获取内存使用的统计信息也是个常用手段; - 利用
jstack
等工具检查是否出现死锁等
- 利用
- 如果还不能确定具体问题,对应用进行
Profiling
也是个办法 - 定位了程序错误或者
JVM
配置的问题后,就可以采取相应的补救措施
- 问题可能来自于
- 系统架构不同
- 分布式
- 单体式
Charlie Hunt
曾将其方法论总结为两类:
- 自上而下。从应用的顶层,逐步深入到具体的不同模块,或者更近一步的技术细节单元,找到可能的问题和解决办法。
- 自下而上。从类似
CPU
这种硬件底层,判断类似Cache-Miss
之类的问题和调优机会,出发点是指令级别优化
系统性能分析中,CPU、内存和
IO` 是主要关注项。
先用 top
命令查看负载状况
top
命令获取相应 pid
,“-H”代表 thread
模式,你可以配合 grep
命令更精准定位。
top –H
转换为十六进制
printf "%x" your_pid
利用 jstack
获取的线程栈,对比相应的 ID
即可。
更加通用的诊断方向,利用 vmstat
之类,查看上下文切换的数量
vmstat -1 -10
如果上下文切换很高,就表明是由于不合理的多线程调用引起的。
当然还需要利用 pidstat
等手段,进行更加具体的定位
JVM
层面的性能分析
- 利用
JMC、JConsole
等工具进行运行时监控。 - 利用各种工具,在运行时进行堆转储分析,或者获取各种角度的统计数据
GC
日志等手段,诊断Full GC
、Minor GC
,或者引用堆积
JFR/JMC
完全具备了生产系统 Profiling
的能力,
不需要重新启动系统或者提前增加配置。例如,你可以在运行时启动 JFR
记录,并将这段时间的信息写入文件:
Jcmd <pid> JFR.start duration=120s filename=myrecording.jfr
- 基准测试是一个非常有效的通用手段,让我们以直观、量化的方式,判断程序在特定条件下的性能表现。
- 基准测试必须明确定义自身的范围和目标,否则很有可能产生误导的结果。
- 虽然
Lambda/Stream
为Java
提供了强大的函数式编程能力,但是也需要正视其局限性:Lambda/Stream
提供了与传统方式接近对等的性能,但是如果对于性能非常敏感,就不能完全忽视它在特定场景的性能差异了,例如:初始化的开销。- 增加了程序诊断等方面的复杂性,程序栈要复杂很多
- 性能往往是特定情景下的评价,泛泛地说性能“好”或者“快”,往往是具有误导性的
- 通过引入基准测试,我们可以定义性能对比的明确条件、具体的指标,进而保证得到定量的、可重复的对比数据,这是工程中的实际需要。
- 开发共享类库,为其他模块提供某种服务的
API
API
对于性能,如延迟、吞吐量有着严格的要求
最为广泛的框架之一就是 JMH
。
使用 JMH
也非常简单,你可以直接将其依赖加入 Maven
工程。
使用 mvn
命令生成一个项目
$ mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0
JMH
利用注解(Annotation
),定义具体的测试方法,以及基准测试的详细配置
- @Benchmark”以标识它是个基准测试方法
- BenchmarkMode 则指定了基准测试模式
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void testMethod() {
// Put your benchmark code here.
}
实现了具体的测试后,就可以利用下面的 Maven
命令构建:
mvn clean install
运行:
java -jar target/benchmarks.jar
需要从白盒层面理解代码,尤其是具体的性能开销,不管是 CPU 还是内存分配
- 需要保证我们写出的基准测试符合测试目的,确实验证的是我们要覆盖的功能点
- 通常对于微基准测试,我们通常希望代码片段确实是有限的,
- 微基准测试基本上都是体量较小的
API
层面测试,最大的威胁来自于过度“聪明”的 JVM!
- 保证代码经过了足够并且合适的预热。
- 在 server 模式下,JIT 会在一段代码执行 10000 次后,将其编译为本地代码,client 模式则是 1500 次以后。我们需要排除代码执行初期的噪音,保证真正采样到的统计数据符合其稳定运行状态。
使用下面的参数来判断预热工作到底是经过了多久
-XX:+PrintCompilation
避免后台编译:
-Xbatch
也要保证预热阶段的代码路径和采集阶段的代码路径是一致的.
防止 JVM 进行无效代码消除(Dead Code Elimination):
public void testMethod() {
}
尽量保证方法有返回值,而不是 void
方法,或者使用 JMH
提供的BlackHole设施
,
public void testMethod(Blackhole blackhole) {
int left = 10;
int right = 100;
int mul = left * right;
blackhole.consume(mul);
}
防止发生常量折叠:
JVM
如果发现计算过程是依赖于常量或者事实上的常量,就可能会直接计算其结果,所以基准测试并不能真实反映代码执行的性能。JMH
提供了 State
机制来解决这个问题
JMH
还会对 State
对象进行额外的处理,以尽量消除伪共享(False Sharing)的影响
JVM
的优化方式仅仅作用在运行应用代码的时候
runtime
- 解释执行和动态编译通用的一些机制
- 锁机制
- 内存分配机制
- 专门用于优化解释执行效率的
- 模版解释器
- inline cache
- 优化虚方法调用的动态绑定
- 解释执行和动态编译通用的一些机制
JIT
- 将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上
- 多种优化方式
- 方法内联
- 逃逸分析
- 基于程序运行
profile
的投机性优化
请看图
我自己绘制の
- 字节码
Java
通过引入字节码这种中间表达方式,屏蔽了不同硬件的差异,由 JVM
负责完成从字节码到机器码的转化
- 编译期
javac
等编译器或者相关 API
等将源码转换成为字节码的过程。此阶段,会进行少量类似常量折叠之类的优化,只要利用反编译工具,就可以直接查看细节。
javac
优化与 JVM
内部优化也存在关联,毕竟它负责了字节码的生成
请看图:
JVM
会根据统计信息,动态决定:
- 什么方法被编译,
- 什么方法解释执行,
- 即使是已经编译过的代码,也可能在不同的运行阶段不再是热点,
JVM
有必要将这种代码从Code Cache
中移除出去,
毕竟其大小是有限的。
- 锁优化,
Intrinsic
机制 (内建方法),就是针对特别重要的基础方法- 即时编译器(
JIT
),则是更多优化工作的承担者- 另外一个优化场景,则是最针对所谓热点循环代码,利用通常说的栈上替换技术
JIT
可以看作就是基于两个计数器实现- 方法计数器和回边计数器提供给
JVM
统计数据,以定位到热点代码。 JIT
机制要复杂得多- 逃逸分析、循环展开、方法内联
- 方法计数器和回边计数器提供给
- 打印编译发生的细节。
-XX:+PrintCompilation
- 输出更多编译的细节
LogFile
选项是可选的,不指定则会输出到
-XX:UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=<your_file_path>
- 打印内联的发生,可利用下面的诊断选项,也需要明确解锁
-XX:+PrintInlining
- JMC、JConsole 之类
- NMT
- 调整热点代码门限值
-XX:CompileThreshold=N
还有一个办法就是关闭计数器衰减。
-XX:-UseCounterDecay
debug
版本的 JDK
,还可以利用下面的参数进行试验
-XX:CounterHalfLifeTime
- 调整
Code Cache
大小
如果 Code Cache
太小,可能只有一小部分代码可以被 JIT
编译,其他的代码则没有选择,只能解释执行
-XX:ReservedCodeCacheSize=<SIZE>
调整其初始大小
-XX:InitialCodeCacheSize=<SIZE>
-调整编译器线程数,或者选择适当的编译器模式
client
模式默认只有一个编译线程,而 server
模式则默认是两个
通过下面的参数指定的编译线程数:
-XX:CICompilerCount=N
- 减少进入安全点
它远远不只是发生在动态编译的时候,GC 阶段发生的更加频繁,你可以利用下面选项诊断安全点的影响
-XX:+PrintSafepointStatistics ‑XX:+PrintGCApplicationStoppedTime
!> 注意,在 JDK 9
之后,PrintGCApplicationStoppedTime
已经被移除了,你需要使用“-Xlog:safepoint”之类方式来指定。
和安全点相关:
- 在
JIT
过程中,逆优化等场景会需要插入安全点 - 常规的锁优化阶段也可能发生
- 偏斜锁的设计目的是为了避免无竞争时的同步开销,但是当真的发生竞争时,撤销偏斜锁会触发安全点,是很重的操作
在并发场景中偏斜锁的价值其实是被质疑的,经常会明确建议关闭偏斜锁。
-XX:-UseBiasedLocking