A **J**ava **V**irtual **M**achine(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 的存在,使得 Java 程序 能够轻易地在多平台上移植,基本上脱离了对硬件的依赖性(这也满足了 David Parnas 的 “信息隐藏” 准则)
因为底层 JIT 编译优化、GC、JUC 对多线程并发编程的支持,以及社区中海量成熟的库 等优点,使得 很多语言 都开发出可运行在 JVM 上的版本
同时,多语言混合编程成为一种趋势,在需要快速开发、灵活部署 和 针对特定问题的 DSL 等场景下,选择恰当的 JVM-hosted language,可以最大化原有代码的价值
那么,在日常的开发过程中,究竟应该如何运用这些 遥不可及的 JVM 知识,来逐步提高实际编程水平呢? 上下而求索了一番后,找到了以下几个层面作为出发点
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
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
@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 可以分析得出,“尾递归” 作为递归的一种特殊情况,即保证了 代码的 简洁性
而且,因为尾递归的调用处于函数的最后,之前函数所累计下的所有信息都可以抹除掉。所以不需要 像递归在每次调用都要存储 寄存器值、返回地址 等信息,从而避免 栈空间上的消耗,即同时也保证了程序的 高效性
Tips: Full code is here.
提到并发编程,里面可以涉及的东西,线程、锁、多线程、互斥同步、并行、并发(模型)、线程安全、内存模型 等等,足以写成好几本书。但是,这里我们只就一点来讨论,ReentrantReadWriteLock 锁的公平性
首先,我们需要明确 公平锁(Fair)和 非公平锁(Nonfair)两者在锁机制上有什么区别?前者,加锁前检查是否有排队等待的线程,优先已经在排队等待的线程,先来先得;后者,则是加锁时不考虑队列中等待的线程,直接尝试获取锁,获取不到自动到队尾等待
因此,重入读写锁默认的 非公平锁,可以避免 ReentrantLock 独占锁带来的吞吐量问题
那么,进一步思考之后,新问题来了,什么场景下 ReentrantReadWriteLock 的公平锁 会是更佳的选择呢,为什么,又该怎么做?
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();
}
}
}
}
}
/**
* 公平锁,很少会连续获取到锁
*
* 0 0 0 0 0 1 2 4 3 1 2 4 3 1 2 4 3 1 2 4 3 1 2 4 3
* 0 1 0 1 0 1 0 1 4 0 1 4 4 4 4 2 2 2 2 2 3 3 3 3 3
* 1 0 2 4 3 1 0 2 4 3 1 0 2 4 3 1 0 2 4 3 1 0 2 4 3
*/
@Test
public void fair() throws Exception {
Thread thread;
for (int i = 0; i < 5; i++) {
thread = new Thread(new ReentrantLockFairness.Fairness(ReentrantLockFairness.FAIR_LOCK));
thread.setName(i + " ");
thread.start();
}
Thread.sleep(1000);
}
/**
* 相比公平锁,非公平锁 能经常性地连续获取到锁
*
* 0 0 0 0 0 3 3 3 3 3 1 1 1 1 1 2 2 2 2 2 4 4 4 4 4
* 0 0 2 2 2 2 2 4 4 4 4 4 1 1 1 1 1 0 0 0 3 3 3 3 3
* 1 1 1 1 1 4 4 4 4 4 0 0 0 0 0 2 2 2 2 2 3 3 3 3 3
*/
@Test
public void unfair() throws Exception {
Thread thread;
for (int i = 0; i < 5; i++) {
thread = new Thread(new ReentrantLockFairness.Fairness(ReentrantLockFairness.UNFAIR_LOCK));
thread.setName(i + " ");
thread.start();
}
Thread.sleep(1000);
}
实际在使用的时候,公平锁 只需要在构造参数中设置即可,内部 AQS(A**bstract**Q**ueued**S**ynchronizer)中,利用 相对 ALock 而言空间复杂度更低的 **CLH 队列锁 来实现公平性
同时,“共享锁” 和 “独占锁” 分别实现了 读写操作,从而读操作之间是没有竞争冲突的,因此 ReentrantReadWriteLock 的最适场景则是 读多于写
Tips: Full code is here and here.
在低并发的情况下( threads<4 t h r e a d s < 4 ),高版本的 JDK 里面 synchronized 实现同步的性能更高。超过 15 个线程之后,则建议使用 ReentrantLock 可重入锁进行并发控制
如果可以使用 ConcurrentHashMap / LongAdder(分段锁)实现的应用场景,则尽量避免使用 synchronized 进行实现
另一方面,synchonrized 方法适用于重复 “释放锁,又获取锁” 的场景。我们可以利用 synchronized 的方法块使得锁,一直被持有,从而提高性能。例如,下面这个 StringBuffer 的场景,增加了 synchronized 块之后,可以使得性能与 StringBuilder 几乎无异
StringBuffer buffer = new StringBuffer();
synchronized (buffer) {
for (int i = 0; i < 9999999; i++) {
buffer.append(i);
buffer.delete(0, buffer.length() - 1);
}
}
Tips: Full code is here.
这里可以列举技术群里讨论过的一个问题作为例子:ResourceManager crash because TimSort [YARN-4743]
首先依据报错信息,定位出 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
# 最快的解决方式
可以在 Jvm 中配置 java.util.Arrays.useLegacyMergeSort=true
或者,在程序中设置 System.setProperty("java.util.Arrays.useLegacyMergeSort", "true")
最终问题将在 Hadoop3.x 中得到解决,官方修复了传递性问题,使其符合 TimSort 的规范
因为,在很多情况下,我们并不需要 Java、Scala 此类静态编译的语言,来通过编译时检查,保证代码的安全性和一致性。而是,希望能够利用 JPython、Groovy 此类的动态语言,从而快速开发出需要的功能
这里可以举两个比较常见的例子,其一,利用 Scala 的表达性来编写 Scala Tester,使得单元测试的代码,更具有可读性
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
}
}
Tips: Full code is here.
其二,利用 Groovy 的简洁语法、开放类特性等,来完成 DSL
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' }
}
}
}
}
Tips: Full code is here.
首先,需要明确定义什么是实时 GC(RTGC,Real-time Collection),即真正的 RTGC 要求对 GC 中 Mutator 赋值器的中断进行精确的控制
常见的回收器 和 实时 GC 调度策略,有如下实现:
回收器 Collector | 工作机制 |
---|---|
万物静止式回收器 Stop-the-world collector | Mutator 在内存分配,发现内存不足时,发起 GC |
增量式回收器 Incremental collector | 要求 Mutator 不仅在内存分配时,还需要在 访问堆(利用 “读写屏障” 的部分有序 来提高性能)的时候检查是否需要发起 GC |
并发回收器 Concurrent collector | GC 与 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 相关知识和经验,对日常编程水平提升的地方还有很多。限于本人有限的水平,暂时只能总结出以上几点。希望此文能起到抛砖引玉的作用,期待各位精彩的观点和建议 ^_^
Technical Discussion Group:(人工智能 1020982(高级)& 1217710(进阶)| BigData 1670647)
Post author:Benedict Jin
Post link: https://yuzhouwan.com/posts/27328/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.