如何运用 JVM 知识提高编程水平

什么是 JVM?

 A Java Virtual Machine (JVM) is an abstract computing machine that enables a computer to run a Java program – wikipedia.org

 JVM 是 Java Virtual Machine (Java 虚拟机) 的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的 – baike.baidu.com

为什么要有 JVM?

跨平台性

 JVM 的存在,使得 Java 程序 能够轻易地在多平台上移植,基本上脱离了对硬件的依赖性
 _( 这也满足了 David Parnas“信息隐藏”准则 )_

多语言性

 因为底层 JIT 编译优化、GC、JUC 对多线程并发编程的支持,以及社区中海量成熟的库 等优点,使得 很多语言 都开发出可运行在 JVM 上的版本
 同时,多语言混合编程成为一种趋势,在需要快速开发、灵活部署 和 针对特定问题的 DSL 等场景下,选择恰当的 JVM-hosted language,可以最大化原有代码的价值

_那么,在日常的开发过程中,究竟应该如何运用这些 遥不可及的 JVM 知识,来逐步提高实际编程水平呢? 上下而求索了一番后,找到了以下几个层面作为出发点_

编码层面

递归 vs. 尾递归

循环调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def inc(i: Int): Int = i + 1
# 10 亿次 for 循环调用
0: aload_0 |从局部变量 0 中装载引用类型值
1: getfield #29 // Field i$1:Lscala/runtime/IntRef; |从对象中获取字段
4: getstatic #33 // Field com/yuzhouwan/jit/Inc$.MODULE$:Lcom/yuzhouwan/jit/Inc$; |从类中获取静态字段
7: aload_0 |从局部变量 0 中装载引用类型值
8: getfield #29 // Field i$1:Lscala/runtime/IntRef; |从对象中获取字段
11: getfield #38 // Field scala/runtime/IntRef.elem:I |从对象中获取字段
14: invokevirtual #42 // Method com/yuzhouwan/jit/Inc$.inc:(I)I |运行时按照对象的类来调用实例方法
17: putfield #38 // Field scala/runtime/IntRef.elem:I |设置对象中字段的值
20: return |从方法中返回,返回值为 void
# 被调用的累加方法
0: iload_1 |从局部变量 1 中装载int类型值入栈
1: iconst_1 |1(int) 值入栈
2: iadd |将栈顶两 int 类型数相加,结果入栈
3: ireturn |返回 int 类型值
10 亿次循环,大约 4014 ms

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def rec(i: Int): Int = {
if (i == 1)
return 1
rec(i - 1) + 1 // change 1 to i, then counting...
}
0: iload_1 |从局部变量 1 中装载 int 类型值
1: iconst_1 |1(int) 值入栈
2: if_icmpne 7 |若栈顶两 int 类型值前小于等于后则跳转
5: iconst_1 |1(int) 值入栈
6: ireturn |返回 int 类型值
7: aload_0 |从局部变量 0 中装载引用类型值
8: iload_1 |从局部变量 1 中装载 int 类型值
9: iconst_1 |1(int) 值入栈
10: isub |将栈顶两 int 类型数相减,结果入栈
11: invokevirtual #24 // Method rec:(I)I |运行时按照对象的类来调用实例方法
14: iconst_1 |1(int) 值入栈
15: iadd |将栈顶两 int 类型数相加,结果入栈
16: ireturn |返回 int 类型值
1 万次递归,耗时 1 ms,速度低下的同时,超过一定数量 (≈14940),还会报错 StackOverflowError

尾递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@tailrec
def tailRec(i: Int, iterator: Int): Int =
if (iterator > 0) tailRec(i + 1, iterator - 1) else i
0: iload_2 |从局部变量 2 中装载 int 类型值入栈
1: iconst_0 |0(int) 值入栈
2: if_icmple 16 |若栈顶两 int 类型值前小于等于后则跳转
5: iload_1 |从局部变量 1 中装载 int 类型值入栈
6: iconst_1 |1(int) 值入栈
7: iadd |将栈顶两 int 类型数相加,结果入栈
8: iload_2 |从局部变量 2 中装载 int 类型值入栈
9: iconst_1 |1(int) 值入栈
10: isub |将栈顶两 int 类型数相减,结果入栈
11: istore_2 |将栈顶 int 类型值保存到局部变量 2
12: istore_1 |将栈顶 int 类型值保存到局部变量 1
13: goto 0 |无条件跳转到指定位置
16: iload_1 |从局部变量 1 中装载 int 类型值入栈
17: ireturn |返回 int 类型值
10 亿次尾递归,大约 1 ms

 通过以上 Scala 代码 和 对应的 Bytecode 可以分析得出,”尾递归“ 作为递归的一种特殊情况,即保证了 代码的 简洁性
 而且,因为尾递归的调用处于函数的最后,之前函数所累计下的所有信息都可以抹除掉。所以不需要 像递归在每次调用都要存储 寄存器值、返回地址 等信息,从而避免 栈空间上的消耗,即同时也保证了程序的 高效性 (完整代码: here)

并发编程

 提到并发编程,里面可以涉及的东西,线程、锁、多线程、互斥同步、并行、并发(模型)、线程安全、内存模型 等等,足以写成好几本书。但是,这里我们只就一点来讨论,ReentrantReadWriteLock 锁的公平性
 众所周知,重入读写锁 默认的非公平锁,可以避免 ReentrantLock 独占锁带来的吞吐量问题
 那么,进一步思考之后,新问题来了,什么场景下 ReentrantReadWriteLock 的公平锁 会是更佳的选择呢,为什么,又该怎么做?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ReentrantLockFairness {
static Lock FAIR_LOCK = new ReentrantLock(true);
static Lock UNFAIR_LOCK = new ReentrantLock();
static class Fairness implements Runnable {
private Lock lock;
Fairness(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
lock.lock();
try {
System.out.print(Thread.currentThread().getName());
} finally {
lock.unlock();
}
}
}
}
}

测试代码: here
 实际在使用的时候,公平锁 只需要在构造参数中设置即可,内部 AQS (AbstractQueuedSynchronizer) 中,利用 相对 ALock 而言空间复杂度更低的 CLH队列锁来实现公平性
 同时,”共享锁” 和 “独占锁” 分别实现了 读写操作,从而读操作之间是没有竞争冲突的,因此 ReentrantReadWriteLock 的最适场景则是 读多于写的

参数调优层面

针对每个版本的 JVM 参数

 这里可以列举一个群里讨论的问题作为栗子,ResourceManager crash because TimSort [YARN-4743] 具体的 issues 详情可以点击链接查看

错误分析:

1
2
3
4
5
6
7
首先依据报错信息,定位出 Yarn 中 MergeSort 的问题
然后怀疑是 jdk7 中为了修复 jdk 本身的漏洞:比较器里面 相比较的两个值 如果同时为空的话,传入的顺序可能决定了返回值 的结果,破坏了 "传递性" (为了解决这个)
从而使用 TimSort 替换了默认的 MergeSort 增强了 comparator 的实现约束
参见 Oracle 官网上 bugs 修复的 archive 列表:
JDK-6804124 : (coll) Replace "modified mergesort" in java.util.Arrays.sort with timsort
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6804124

解决方案:

1
2
3
# 最快的解决方式
可以在 jvm 中配置 java.util.Arrays.useLegacyMergeSort=true
[或者在程序中 System.setProperty("java.util.Arrays.useLegacyMergeSort", "true")]

 最终问题将在 Hadoop3.x 中得到解决,官方修复了传递性问题,使其符合 TimSort 的规范

语种层面

(不同场景下)如何选择不同的 JVM 语言

 因为,在很多情况下,我们并不需要 Java、Scala 此类静态编译的语言,来通过编译时检查,保证代码的安全性和一致性。而是,希望能够利用 JPython、Groovy 此类的动态语言,从而快速开发出需要的功能

 这里可以举两个比较常见的例子,其一,利用 Scala 的表达性来编写 Scala Tester,使得单元测试的代码,更具有可读性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
object DefineA {
def main(args: Array[String]) {
bigMistake()
}
@DefineAnnotation
def bigMistake(): Unit = {
println("bigMistake...")
}
}
abstract class UnitTestStyle extends FlatSpec
with Matchers with OptionValues with Inside with Inspectors
class DefineATest extends UnitTestStyle {
"DefineA's bigMistake method" should "output a information" in {
new DefineA
}
}

 其二,利用 Groovy 的简洁语法、开放类特性等,来完成 DSL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def invokeMethod(String name, args) {
print "<${name}"
args.each {
arg ->
if (arg instanceof Map) {
arg.each {
print " ${it.key} ='${it.value}' "
}
} else if (arg instanceof Closure) {
print '>'
arg.delegate = this
def value = arg.call()
if (value) {
print "${value}"
}
}
}
println "</${name}>"
}
html {
head {
meta {
}
}
body {
table(style: 'margin:2px;') {
tr('class': 'trClass', style: 'padding:2px;') {
td { 'http://' }
td { 'yuzhouwan.' }
td { 'com' }
}
}
}
}

完整代码: here and here

内部机制

实时 GC 的原理

 首先需要明确定义什么是实时 GC (RTGC, Real-time Collection),即真正的 RTGC 要求对 GC 中 Mutator 赋值器的中断进行精确的控制

 常见的回收器 和 实时 GC 调度策略,有如下实现:

回收器 Collector工作机制
万物静止式回收器 Stop-the-world collectorMutator 在内存分配,发现内存不足时,发起 GC
增量式回收器 Incremental collector要求 Mutator 不仅在内存分配时,还需要在 访问堆 (利用 “读写屏障” 的部分有序 来提高性能) 的时候检查是否需要发起 GC
并发回收器 Concurrent collectorGC 与 Mutator 的工作 并发执行
GC调度 Scheduling调度机制
基于工作的调度 Work-based scheduling按照 Mutator 的每个工作单元分配 GC 任务
基于间隙的调度 Slack-based scheduling实时任务的调度间隙完成 GC
基于时间的调度 Time-based scheduling预留出独占式的 GC 时间
整合式的调度 “税收与开支”策略 Tax-and-Spend允许不同的线程可以有着不同的 Mutator 使用率,通过线程分配的弹性,尽可能地减少线程中断

 实际上,对吞吐量的提高 和 时间窗口、延时的缩小,在 Tax-and-Spend 调度策略 应用到 Metronome回收器 之后,得以证实

内存模型

 内存模型中,主要的组成分为 “主内存” 和 “工作内存”两部分 (线程访问工作内存,工作内存通过 8种原子性的操作,来和主内存交互),特性包括 原子性可见性有序性 (其中,有序性 由 volatile / synchronized / happens-before 原则 来保证)

 相信 熟知 JVM 相关知识和经验,对提升日常编程水平的方面 还有很多,限于本人有限的水平 暂时只能总结出以上几点。希望此文能起到 抛砖引玉的作用,期待各位精彩的观点和建议 ^_^

资料

更多资源,欢迎加入,一起交流学习

QQ group: (人工智能 1020982 (高级) & 1217710 (进阶) | BigData 1670647)