Java 蓝图
这篇文章从 Java 的运行环境与设计理念来重新审视 Java 开发视角,主要引用李三红在阿里云的分享,包括运行时、JVM 和并发三个部分。
高司令在 1995 发表的《The Feel of Java》阐述了对 Java 语言的顶层设计,将近三十年过去 Java 基本没有逃出这个范畴。首先明确 Java 是一门工程语言,是一门可以为工程师谋求职业的「蓝领语言」。
提到 Java,它的设计蓝图包括分布式对象、架构中立、垃圾回收、面向对象等,虽然在云原生时代遇到诸多挑战,但在 2012 年后 OpenJDK 社区的全面爆发依然不影响这是一门全世界最流行的编程语言之一。从语言生态来讲,Java 在企业软件开发领域门槛低、有大公司支撑、好的社区以及 Spring 等杀手级应用。从语言设计上讲,Java 在发布之初就一直贯彻面向对象设计思想,始终致力于解决大规模软件开发问题,也就是布鲁克斯所说的降低偶然复杂度。以及每年两个版本的迭代速度,Java 始终屹立潮头,操作系统层面的语言选择是 C/C++,中间件逐渐被 Go/Rust 所占据,大规模应用开发基本就离不开 Java。
另一方面,编程语言的设计需要取舍(trade off)。Mary Shaw 提到这是遵循软件工程发展规律的,随着语言抽象不断提高,软件工程始终都要在有限时间、资源、知识的约束下开展。Java 基于面向对象的设计也有其硬币的另一面,那就是大量的固化代码,以及导致无法从相同输入获得相同输出的共享状态。好在 Java 8 之后引入了 Stream 和 Lambda,Java 具备了函数式编程的基础,不变对象可以很好抑制共享状态的变化,代码鲁棒性与扩展性有了大幅提升。Java 帮助开发者从 C/C++ 琐碎的内存管理中释放出来,运行时错误检测和限制指针的内存访问等内存安全设计减少了大量内存溢出、内存未回收等问题的发生。
工程效率提升的同时,也在和性能做取舍。Java 提供的动态类加载、反射、动态代码修改等特性支撑了 Spring、Hibernate 等大量框架的繁荣,这也使得内存占用、预热时间、程序体积等面临挑战。除此之外,面对多语言的协同、新兴 AI 等硬件的支持、像基本数据类型一样使用对象技术、协程等趋势都是 Java 始终不下牌桌的筹码。
OpenJDK 与 JRE
Java 的运行环境是虚拟机技术成熟发展的代表,Java 通过提供运行时屏蔽掉了操作系统层面的差异。
JDK 与 JRE 的区别就是增加了 monitor、profiling、debug 等工具。监控工具分为 jps、jstack、jmap 等本地工具,与基于 JMX 的远程工具。剖析工具实现有采样与插针两种方式,实现对 CPU、延迟、资源占用和锁的剖析。debug 则是通过 JDWP 协议连接了实现 JDI 的 debugger 以及实现了 JVMTi 的 debuggee。
提到 JDK 就会涉及版本关系,OpenJDK 始终是 Java SE 的标准实现也就是完全包含,在 Java 11 之前,OracleJDK 是相对于 OpenJDK 提供了 JFR 等商用工具支持的,在 Java 11 开始,OracleJDK 与 OpenJDK 就基本没有区别了,但要注意生产环境使用的 JDK 版本其 license 的法务影响。就 Spring Boot 而言,推荐使用 BellSoft Liberica JDK。
从 Java 生态可以看出,OpenJDK 只是其中一块,还有大数据、Web 容器、微服务框架、基于 JVM 的多语言生态。
Java 运行时从文件系统或者网络通过双亲委派模型加载类,经过编译器或者解释器转换成字节码,装载到 JVM 上最后在操作系统上运行。
JVM 与 GC
以 Java 8 为例,JVM 内存布局由堆、本地方法栈、虚拟机栈、程序计数器、元空间等几部分组成。
堆空间是被频繁使用并经常出现问题的,堆空间的划分来源于 David Ungar 的分代假说,也就是绝大部分对象都是年轻对象,快速创建然后快速死亡,只有很少一部分对象会长期存活。这促成了垃圾回收器与垃圾回收算法的设计基础。
简单来说,垃圾回收基础算法无外乎标记-清除,标记-压缩,标记-复制这几种。直接清除会造成内存碎片,频繁压缩会降低垃圾回收性能,复制存活对象又会降低内存利用率。因此,各垃圾回收器的设计都是基于这几种算法进行组合优化。
常见的垃圾回收器有 Serial、CMS、G1 等。Serial 结构简单,执行快,虽然是单线程处理垃圾回收,但适合云原生场景。CMS 曾在 JDK 8 之前大量使用,由于并发线程争抢以及内存碎片化问题,JDK 9 已标记为废弃,在 JDK 14 被彻底移除。G1 更适合大内存使用。
衡量垃圾回收效果一般从吞吐量、延迟、内存占用等来考虑,如果 GC 频繁发生、回收效率低、延迟大,那就需要进行优化。对于 GC 而言,一定是喜欢小且生命周期短的对象,不喜欢大对象和池化对象,也不喜欢内部对象之间的长期引用。
根据利特尔法则,应用可承载的业务数是吞吐量与 RT 的乘积。想要提高引用承载的业务数,就需要提高吞吐量或者降低 RT,比如硬件升级、JDK 版本升级、JVM 调优减少 STW 时间、算法优化、应用逻辑优化。
同时基于盖尔定律,不论是系统搭建还会性能调优,都从简单开始,不断迭代,罗马不是一天建成的,繁杂的系统噪音与先验经验也往往是障碍。
并发是时代的机遇
Herb Sutter 早在 2005 年便发表了没有免费午餐的论文,随着摩尔定律的终结,硬件时钟频率的增加在计算机体系结构的约束下已经到达瓶颈,多核与众核才能更好应对系统发展。软硬件接口的抽象提升带来的创新机会,多核将责任转移到编程语言,包括使用它的开发者。于是并发技术的大量普及也是时代发展的必然与机遇。
阿尔达姆定律有提到如果想要提升应用加速比,可以考虑将串行程序做并行化改造,改造方式有任务并行与数据并行两种方式。对于数据并行早已进入 MapReduce 为代表的大数据领域,对于任务并行 Java 也在 1.0 版本中就做好了线程模型设计。至于数据并行,Java 的 Vector API 也在积极响应,打算利用硬件的 SIMD 实现数据的并行计算。
冯诺依曼体系的「缺陷」还有 CPU 缓存与内存的速度差异,内存访问延迟优化的解决方案是内存指令重排,这也是并发程序频繁发生故障的根源之一,与之同样「臭名昭著」的还有原子性与可见性。
Java 内存模型(JMM)按需禁用缓存与编译优化,在 Java 1.5 之后保证 SC(Sequential Consistency),提供了并发包这样的高级抽象、Syncronized 这样的低级锁支持,以及 volatile 这样的非阻塞式原语支持。同时,满足 Happen-Before 原则实现程序序与内存序的一致性来应对并发挑战。
参考资料:
Paper:
The Feel of Java
No Silver Bullet
Progress toward an Engineering Discipline of Software------Mary Shaw
Young objects die young------David Ungar
The Free Lunch is Over------Herb Sutter
A New Golden Age for Computer Architecture
Time,Clocks,and the Ordering of Events in Distributed Systems------Lamport
Book:
Agile Software Development
Website: