Java学习 - JVM, JDK, JRE

本文最后更新于 2024年12月4日 晚上

Java SE vs Java EE

简单来说,对比于Java,Java SE是Java的基础版本,包括了支持Java变成语言的基本功能和标准库,比如集合框架,IO,网络,并发等等。Java SE 适合用于小型或单机的应用程序,亦或者是控制台应用程序和工具(自动化脚本等)。而Java EE则是面向于开发复杂的企业级分布式应用,提供了面向企业级开发的扩展框架和API,比如Web服务、EJB、JPA等。Java EE适合用于企业级分布式应用,适用于具有多层架构系统和高并发,高可用性需求的大型应用程序。

由此,显然可以看出,Java EE对比Java SE是一个复杂得多的Java开发框架,因此,Java SE的应用场景和部署,配置难度对比起Java EE都简单许多。

JVM vs JDK vs JRE

从包含的角度来看,可以看成

JVMJREJDKJVM \subset JRE \sub JDK

JVM (Java Virtual Machine), 顾名思义,就是Java虚拟机。JVM是实现让Java程序真正做到"Write Once, Run Everywhere"的功臣,其可以针对不同系统,使用相同的字节码和不同的实现方法,以得到相同的结果。

JRE (Java RunTime Environment), 顾名思义,也就是Java运行环境。其中显然包含了虚拟机,也包含了一系列基本的内置类库,用与基本运行Java程序所需的环境和类库。

JDK (Java Development Kit),顾名思义,也就是Java开发工具包,其中包含了JRE和JVM,并能够创建和编译Java程序(javac)。JDK拥有更多额外的工具,比如javadoc(文档生成器)、jdb(调试器)、jconsole(监控工具)、javap(反编译工具)等[2]

优化一下Java运行速度吧!

Java程序的运行逻辑基本上可以理解为:

graph LR

c1[编写.java文件] --javac编译--> c2[生成.class 字节码文件]
c2 --JVM解释器&JIT--> c3[机器码]
c3 --> 运行程序

在将.class文件转化为机器码的过程中,JVM类加载器首先会加载.class文件,然后通过解释器逐行解释执行。可以看出,这样子会比较慢。由于有些代码块会经常被调用,因此,不难想到可以将这些经常被用到的代码块的机器码保存下来,以节省大量的时间,这看起来就像是一种缓存的机制。

综上,Java引进了名为**JIT (Just In Time Compilation)**的编译器,使其在程序运行时进行动态的编译。其核心逻辑是,对于热点代码块,JIT会在完成第一次编译后将对应的机器码保存下来,以供下次直接使用。这时候我想到了一个问题:对于一个庞大的系统来说,JIT所带来的缓存量也会很大,也许会对内存带来很大的压力。我们应该如何处理这个问题?

JVM如何管理JIT缓存的内存[1]

JVM 使用以下机制来管理 JIT 编译生成的本地代码及其缓存:

  1. 代码缓存区(Code Cache):
    • JVM 在内存中分配一个固定大小的区域,用于存储 JIT 编译后的本地机器码。
    • 代码缓存有容量限制,当缓存满时,JVM 会自动清理较老或较少使用的编译代码,以腾出空间给新的热点代码。
  2. 分层编译(Tiered Compilation):
    • JVM 会分阶段优化代码,减少不必要的内存占用。
      • 初始阶段:使用 C1 编译器快速生成基本的本地代码,占用内存少但优化程度较低。
      • 热点阶段:使用 C2 编译器对高频代码进行深度优化,占用更多内存,但执行效率更高。
    • 如果代码热点不再频繁使用,JVM 可能会将其标记为可清理对象。
  3. 垃圾回收(GC)与代码缓存清理:
    • JVM 的垃圾回收机制不仅负责清理堆内存,还可以清理代码缓存中的无用代码。
    • 无效或不再使用的 JIT 编译代码会被标记并清理,以释放内存。

对于代码缓存,JVM提供了一些参数用来调整缓存区域的大小,或者是将代码块识别为热点代码所需的阈值:

  • -XX:InitialCodeCacheSize: 代码缓存的初始大小
  • -XX:ReservedCodeCacheSize: 代码缓存的最大大小
  • -XX:CompileThreshold: 将代码识别为热点代码所需的调用次数阈值

Java包装类的缓存

包装类是对Java中基本类型的封装。一般来说,包装类的创建.valueOf(...)会占用内存,而对于一些常用的、小范围的数值(例如 0 到 127 的 Integer),频繁创建和销毁对象会导致较大的内存浪费。因此,在JDK5中引入了一个缓存机制,其会在类初始化时,提前创建好会被频繁使用的包装类对象。这种机制常见于 IntegerByteShortLongCharacter 等包装类,目的是为了优化内存的使用并提高性能。

注意:在构造函数中,不会使用缓存机制,比如:Integer a = new Integer(100);

自动装箱的过程会使用缓存,因为其底层代码是调用了封装类的valueOf()这个方法

参考资料


Java学习 - JVM, JDK, JRE
http://example.com/2024/11/24/Java学习/Java学习 - JVM, JDK, JRE/
作者
Clain Chen
发布于
2024年11月24日
许可协议