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

什么是 JVM?

 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?

跨平台性

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

多语言性

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

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

编码层面

递归 vs. 尾递归

循环调用

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 类型值保存到局部变量 212: istore_1              |将栈顶 int 类型值保存到局部变量 113: goto            0     |无条件跳转到指定位置

16: iload_1               |从局部变量 1 中装载 int 类型值入栈
17: ireturn               |返回 int 类型值

10 亿次尾递归,大约 1 ms

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

Tips: Full code is here.

并发编程

ReentrantReadWriteLock 锁的公平性

 提到并发编程,里面可以涉及的东西,线程、锁、多线程、互斥同步、并行、并发(模型)、线程安全、内存模型 等等,足以写成好几本书。但是,这里我们只就一点来讨论,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);
}

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

Tips: Full code is here and here.

synchronized 的性能之争

 在低并发的情况下( 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.

参数调优层面

针对每个版本的 JVM 参数

描述

 这里可以列举技术群里讨论过的一个问题作为例子: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 的规范

语种层面

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

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

Scala

 这里可以举两个比较常见的例子,其一,利用 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

 其二,利用 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 的原理

 首先,需要明确定义什么是实时 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 回收器 之后,得以证实

沙箱

(利用 StarUML™ 绘制而成)

内存模型

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

(利用 Visio™ 绘制而成)

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

资料

  • 《代码大全 2》
  • 《分布式 Java 应用:基础与实践》
  • 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版)》
  • 《Java 程序员修炼之道》
  • 《Java 并发编程实战》
  • 《The Art of Multiprocessor Programming》
  • 《The Garbage Collection Handbook:The Art of Automatic Memory Management》

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

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.

你可能感兴趣的:(JVM,Java,Scala,Groovy,JVM)