一些杂想:Java老矣,尚能饭否?( 四 )


图片来源:周志明(就是写《深入理解Java虚拟机》的大牛)的文章:云原生时代 , Java 的危与机[43]
截至今天 , 最新的Java 18中仅包含了 Amber和 的一些特性 , 像 Loom、 等并没有包含 , 更别提难度最大的 了 , 确实是有点落后了 。不管如何 , 了解下这些项目做的事情可以让我们更好地理解Java未来的发展方向 。
提前编译-AOT
我们一直说Java速度慢 , 我觉得这是一个不严谨的误会 , 因为实际上经过JIT编译后Java运行并不慢 。为什么Java给人“更慢”的印象?可能这两方面因素是罪魁祸首:
Java是一门跨平台语言 , 但JVM并不是跨平台的 , Java将源码编译成字节码 , 交给JVM执行 , 这中间装载的开销很高 。
一段程序想要被加载需要经过的流程:
上面这张图能够清晰地看出Java从启动到达到最佳性能的不同阶段 。
如果跳过字节码 , 直接将Java代码编译成本地代码 , 那么所有代码都是在编译期编译和优化好的 , 是不是就不存在JVM初始化和类加载的开销问题 , 也不用等预热到JIT编译(编译时还要耗费额外的运行期CPU资源) , 马上就能达到最大性能?这就是AOT(Ahead-Of-Time )提前编译的思想 。
当然AOT编译也有劣势:
目前来看使用AOT难免需要有一些折中 , 例如后面要讲到的 VM就要求以配置的方式明确告知编译器程序代码中有哪些方法是只通过反射来访问的 , 哪些类会被动态加载等等 。然而另一些功能可能只能妥协或者放弃了 , 就像动态生成字节码这类十分常用的功能 , 我们熟知的默认就会使用CGLib生成动态代理 。从5.2 开始增加了@注解来排除对 CGLib 的依赖 , 仅使用标准的动态代理去增强类 , 但这也就限制了动态代理的能力 。
要获得有实用价值的提前编译能力 , 只有依靠提前编译器、组件类库和开发者三方一起协同才有可能办到 。这就要靠后面说的队友的助攻了 。
协程(虚拟线程)
协程[44]( , 有的地方也称为纤程/Fiber)并不算一个新鲜的概念 , 但与线程相比一直让开发者感觉陌生 , 我觉得最主要的原因是大多数编程语言对于协程的支持并不像线程一样“原生” 。直到Go和这些热门的语言直接内置了协程 , 协程才成为“一等公民”被开发者重新审视 。
对于协程的定义 , 不仅在不同语言中有差异 , 随着时代的变化定义也在变化 , 我试着将主流印象中的协程和线程做一个不严谨的对比:
回到Java , 基本上线程模型分成1:1、N:1 , N:M三种 , 虽然说JVM并没有限定 Java 线程需要使用哪种线程模型来实现 , 但一般来说Java目前主流的线程模型是直接映射到操作系统内核上的1:1 模型[45] , 即一个用户线程就唯一地对应一个内核线程(这里不谈在遥远的JDK1.2之前 , 那会也使用过称为“绿色线程”的N:1模型) 。
1:1的模型对于计算密集型任务这很合适 , 既不用自己去做调度 , 也利于一条线程跑满整个处理器核心;但对于 I/O 密集型任务 , 譬如访问磁盘、访问数据库占主要时间的任务 , 这种模型就显得成本高昂 , 主要在于内存消耗和上下文切换上:64 位 Linux 上的线程栈容量默认是 1MB , 线程的内核元数据( )还要额外消耗 2-16KB 内存 , 所以单个虚拟机的最大线程数量一般只会设置到 200 至 400 条 , 当程序员把数以百万计的请求往线程池里面灌时 , 系统即便能处理得过来 , 其中的切换损耗也是相当可观的 。