学习笔记【Java 虚拟机①】内存结构


若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。


总目录



前言


  • 参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

写这篇博客旨在制作笔记,方便个人在线阅览,巩固知识。

博客的内容主要来自视频内容和资料中提供的学习笔记。


引言


JVMJava 程序的运行环境,学习 JVM 方能了解 Java 程序是如何被执行的,为进一步深入底层原理乃至程序性能调优打好基础。


(1)什么是 JVM


定义

  • Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

好处

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

比较

  • jvm(Java Virtual Machinejre(Java Runtime Environmentjdk(Java Development Kit)

在这里插入图片描述


(2)学习 JVM 有什么用


  • 提高面试的竞争力
  • 理解底层的实现原理(有利于长远发展)
  • 中高级程序员的必备技能

(3)常见的 JVM


在这里插入图片描述


(一)内存结构


1.整体架构


在这里插入图片描述


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

JAVA 虚拟机 | 运行时数据区域

  • JAVA 虚拟机在执行 Java 程序的过程中,会把它所管理的内存划分为若干不同的数据区域。
  • 这些区域有各自的用途,以及创建和销毁的时间。
    • 有的区域随着虚拟机进程的启动而一直存在。
    • 有些区域则是依赖用户线程的启动和结束而建立和销毁。
  • 根据 《Java 虚拟机规范》 的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域。

在这里插入图片描述


2.程序计数器


Program Counter Register 程序计数器(寄存器)

  • 作用:记住下一条 jvm 指令的执行地址
  • 特点:是线程私有的;不会存在内存溢出

0: getstatic #20 			// PrintStream out = System.out;
3: astore_1					// --
4: aload_1 					// out.println(1);
5: iconst_1 				// --
6: invokevirtual #26 		// --
9: aload_1 					// out.println(2);
10: iconst_2 				// --
11: invokevirtual #26 		// --
14: aload_1 				// out.println(3);
15: iconst_3 				// --
16: invokevirtual #26	 	// --
19: aload_1 				// out.println(4);
20: iconst_4 				// --
21: invokevirtual #26 		// --
24: aload_1 				// out.println(5);
25: iconst_5 				// --
26: invokevirtual #26 		// --
29: return

一段 Java 代码的执行流程:Java 代码 —> 二进制字节码 —> 解释器 —> 机器码 —> cpu(执行具体操作)


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

Program Counter Register 程序计数器

  • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
    • 在 JAVA 虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
    • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 由于 JAVA 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的。
    • 在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。
    • 因此,为了线程切换后可以恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
    • 各条线程之间计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存。
  • 如果线程正在执行的是一个 JAVA 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
  • 如果正在执行的是本地(Native)方法,这个计数器则应为空(Undefined)。
  • 此内存区域是唯一一个在《JAVA 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。

3.虚拟机栈


栈:线程运行需要的内存空间。

一个线程需要一个虚拟机栈,多个线程就需要多个虚拟机栈。


每个栈是由什么组成的?

  • 一个栈内是可以看作由多个栈帧组成。

栈帧又是什么?

  • 一个栈帧对应一次方法的调用
  • 线程最终的目的是去执行代码,代码是由一个个的方法组成。
    所以在线程在运行的时候,每个方法运行时所需要的内存我们就称为栈帧。
  • 一个方法包括参数,局部变量,返回地址值,所以栈帧中也会存储这些数据。

3.1.定义


Java 虚拟机栈Java Virtual Machine Stacks

  • 每个线程运行时所需要的总内存,称为虚拟机栈
  • 每个栈由多个栈帧Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
/**
 * 演示线帧
 */
public class Demo1_1 {
    public static void main(String[] args) {
        method1();
    }

    private static void method1() {
        method2(1, 2);
    }

    private static int method2(int a, int b) {
        int c = a + b;
        return c;
    }
}

于主方法中调用 method() 方法处打个断点,进行 debug,观察控制台输出的信息变化。

发现符合栈 后进先出(Last In First Out,LIFO) 的特点。

在这里插入图片描述


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明


与程序计数器一样,Java 虚拟机栈Java Virtual Machine Stack也是线程私有的,它的生命周期与线程相同

虚拟机栈描述的是 Java 方法执行的线程内存模型

  • 每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧Stack Frame
  • 栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息
  • 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

经常有人把Java内存区域笼统地划分为堆内存(Heap)和栈内存(Stack

这种划分方式直接继承自传统的 C、C++ 程序的内存布局结构

在 Java 语言里就显得有些粗糙了,实际的内存区域划分要比这更复杂。

不过这种划分方式的流行也间接说明了程序员最关注的、与对象内存分配关系最密切的区域是 “堆” 和 “栈” 两块。

其中的 “栈” 通常就是指这里讲的虚拟机栈,或者更多的情况下只是指虚拟机栈中局部变量表部分。


局部变量表 存放了编译期可知的各种 Java 虚拟机的

  • 基本数据类型boolean、bytecharshortintfloatlongdouble
  • 对象引用reference 类型),它并不等同于对象本身
    • 可能是一个指向对象起始地址的引用指针
    • 也可能是指向一个代表对象的句柄或者其他与此对象相关的位置
  • returnAddress 类型(指向了一条字节码指令的地址)。

这些数据类型在局部变量表中的存储空间以 局部变量槽Slot)来表示

  • 其中 64 位长度的 longdouble 类型的数据会占用两个变量槽
  • 其余的数据类型只占用一个。
  • 局部变量表所需的内存空间在编译期间完成分配
    • 当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
    • 请读者注意,这里说的 “大小” 是指变量槽的数量
    • 虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽
    • 这是完全由具体的虚拟机实现自行决定的事情。

3.2.问题辨析


  1. 垃圾回收是否涉及栈内存?
    • 不会。
    • 栈内存是一次次的方法调用所产生的一个个栈帧内存。方法调用结束后,栈帧内存会被弹出栈,即被自动回收掉。
  2. 栈内存分配越大越好吗?
    • 不是。
    • 物理内存一定时,栈内存越大,虽然可以进行更多次的方法递归调用,但是可执行的线程数目会变少。
  3. 方法内的局部变量是否线程安全?
    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

3.3.栈内存溢出


  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

在《Java 虚拟机规范》中,对这个内存区域规定了两类异常状况

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;
  • 如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。

HotSpot 虚拟机 的栈容量是不可以动态扩展的,以前的 Classic 虚拟机 倒是可以。

所以在 HotSpot 虚拟机 上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常

  • 只要线程申请栈空间成功了就不会有 OOM
  • 但是如果申请时就失败,仍然是会出现 OOM 异常

3.4.线程运行诊断


3.4.1.案例1:CPU 占用过多


定位

  • top 定位哪个进程对 cpu 的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id(用 ps 命令进一步定位是哪个线程引起的 cpu 占用过高)
  • jstack 进程id(其输出的 线程编号16 进制
    • 可以根据 线程 id 找到有问题的线程,进一步定位到问题代码的源码行号

3.4.2.案例2:程序运行很长时间没有结果


推测:可能是线程此时发生了死锁

使用 jstack 进程id 观察其最后的输出(详细地分析了代码异常)

之后再根据其输出的信息进一步定位,从而找到问题代码的源码行号


4.本地方法栈


本地方法栈(Native Method Stacks

JAVA 有时候没法直接和操作系统底层交互,需要用到用 C 或者 C++ 语言编写的本地方法。

本地方法运行时使用的内存即为本地方法栈。

一些带有 native 关键字的方法就是用 用 C 或者 C++ 语言来编写的,Java 代码则是通过本地方法接口来间接调用。

诸如:object 中的 clone 方法,wait 方法,synchonized 关键字;


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

本地方法栈与虚拟机栈所发挥的作用是非常相似的。

  • 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务
  • 本地方法栈则为虚拟机栈使用到本地(Native)方法服务。

《Java 虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构没有任何强制规定。

  • 因此具体的虚拟机可以根据需要自由实现它。
  • 甚至有的 JAVA 虚拟机(譬如 HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowErrorOutOfMemoryError 异常。


5.堆


5.1.定义


堆(Heap

  • 通过 new 关键字,创建对象都会使用堆内存

特点

  • 它是线程 共享 的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

目的

  • 创建对象是为了后续使用该对象,JAVA 程序会通过 [栈] 上的 reference 数据来操作 [堆] 上的具体对象
  • HotSpot 虚拟机中的 reference 主要使用直接指针来定位、访问到堆中对象的具体位置。(当然,也存在使用句柄访问的情况)

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

对于 Java 应用程序来说,Java 堆(Java Heap)是虚拟机所管理的内存中最大的一块。

Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例,Java 世界里 “几乎” 所有的对象实例都在这里分配内存。

由于即时编译技术的进步,说 Java 对象实例都分配在堆上也渐渐变得不是那么绝对了。

Java 堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作 GC 堆”(Garbage Collected Heap

根据《Java虚拟机规范》的规定,Java 堆可以处于物理上不连续的内存空间中

但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。

但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。


5.2.堆内存溢出


在这里插入图片描述
控制台输出报错信息

java.lang.OutOfMemoryError: Java heap space
... ...

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

JAVA 堆既可以被实现成固定大小的,也可以是可拓展的。

不过当前主流的 JAVA 虚拟机都是按照可扩展来实现的(通过参数 -XmxXms 设定)。

如果在 JAVA 堆中没有内存完成实例分配,并且堆也无法再扩展时,JAVA 虚拟机将会抛出 OutOfMemoryError 异常。


5.3.堆内存诊断


jps 工具

  • 查看当前系统中有哪些 java 进程

jmap 工具

  • 查看堆内存占用情况:jmap -heap 进程id

jconsole 工具

  • 图形界面的,多功能的监测工具,可以连续监测

5.4.案例:垃圾回收后内存占用依然高


使用 jvisualvm 工具里面的 堆 Dump 功能

堆 Dump 功能界面有 检查 功能,抓取堆的当前快照,从而进一步定位分析

在其中可以仔细查看实例的情况,可以设置按内存大小次序排序实例的数目

找到占用最大内存的实例,并进一步结合源码定位分析

在这里插入图片描述


6.方法区


6.1.定义


链接JVM 规范-方法区定义


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域。

  • 它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
  • 虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分。
  • 但是它却有一个别名叫作 “非堆”Non-Heap),目的是与 Java 堆区分开来

《Java虚拟机规范》对方法区的约束是非常宽松的。

  • 除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。
  • 相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样 “永久” 存在了。
  • 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意。
    • 尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。
  • 以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。
  • 根据《Java 虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。

6.2.永久代


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

说到方法区,不得不提一下 “永久代” 这个概念。


  • 尤其是在 JDK 8 以前,许多 Java 程序员都习惯在 HotSpot 虚拟机上开发、部署程序。
  • 很多人都更愿意把方法区称呼为 “永久代”Permanent Generation),或将两者混为一谈。
  • 本质上这两者并不是等价的。
    • 仅仅是因为当时的 HotSpot 虚拟机设计团队选择把收集器的分代设计扩展至方法区。
      • 或者说使用永久代来实现方法区而已。
    • 这样使得 HotSpot 的垃圾收集器能够像管理 Java 堆一样管理这部分内存。
      • 省去了专门为方法区编写内存管理代码的工作。

在这里插入图片描述

  • 但是对于其他虚拟机实现,譬如 BEA JRockitIBM J9 等来说,是不存在永久代的概念的。

  • 原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。
  • 但现在回头来看,当年使用永久代来实现方法区的决定并不是一个好主意。
    • 这种设计导致了 Java 应用更容易遇到内存溢出的问题
      • 永久代有 -XX:MaxPermSize 的上限,即使不设置也有默认大小。
      • J9JRockit 只要没有触碰到进程可用内存的上限,就不会出问题。
        • 例如 32 位系统中的 4GB 限制
    • 而且有极少数方法会因永久代的原因而导致不同虚拟机下有不同的表现
      • 例如 String::intern()

  • 当 Oracle 收购 BEA 获得了 JRockit 的所有权后。
  • 准备把 JRockit 中的优秀功能移植到 HotSpot 虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。
    • 譬如 Java Mission Control 管理工具
  • 考虑到 HotSpot 未来的发展
  • JDK 6 的时候 HotSpot 开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了。
  • 到了 JDK 7HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出。
  • 而到了 JDK 8,终于完全废弃了永久代的概念,改用与 JRockitJ9 一样在本地内存中实现的元空间(Meta-space)来代替。
    • JDK 7 中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

在这里插入图片描述


6.3.方法区内存溢出


jdk 1.8 以前会导致永久代内存溢出

  • 永久代内存溢出:java.lang.OutOfMemoryError: PermGen space
  • -XX:MaxPermSize=8m

jdk 1.8 之后会导致元空间内存溢出

  • 元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
  • -XX:MaxMetaspaceSize=8m

6.3.1.案例演示


import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

public class Demo extends ClassLoader {// ClassLoader可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_11 test = new Demo1_11();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                
                // 参数:版本号, 类的权限修饰符, 类名, 包名, 父类, 接口
                cw.visit(
                        Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i,
                        null, "java/lang/Object", null
                );
                
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

输出(jdk 1.6 版本)

cMethodAreaTests/permGenSpace/Demo1_12.java

19315
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:465)
	at cMethodAreaTests.permGenSpace.Demo1_12.main(Demo1_12.java from InputFileObject:24)

Process finished with exit code 1

输出(jdk 1.8 版本)

cMethodAreaTests/metaSpace/Demo1_11.java

5411
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at cMethodAreaTests.metaSpace.Demo1_11.main(Demo1_11.java:31)

Process finished with exit code 1

6.3.2.场景


举例 1:spring

  • 使用 cglib 生成一些代理类,这些代理类是 Spring 中 AOP 的核心

举例 2:mybatis

  • 使用 cglib 去产生 Mapper 接口的实现类

补充介绍:cglibCode Generation Library

  • cglib 是一个强大的高性能的代码生成包。
  • cglib 包的底层是通过使用一个小而快的字节码处理框架 ASM,来转换字节码并生成新的类。

若调用次数十分频繁就有可能导致永久代方法区内存溢出


6.4.运行时常量池


6.4.1.了解字节码文件


*.Class 文件的内容中

  • 除了有类的版本、字段、方法、接口等描述信息外
  • 还有一项信息是 常量池表(Constant Pool Table
    • 用于存放编译期生成的各种字面量与符号引用
    • 这部分内容将在类加载后存放到方法区的运行时常量池中

Java 虚拟机对于 Class 文件每一部分(自然也包括常量池)的格式都有严格规定

  • 如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行

创建一个文件:HelloWorld.java

// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

在该 HelloWorld.java 文件所在目录下,打开终端执行下面的命令。

  1. 编译文件
javac HelloWorld.java

使用上面的代码编译文件的话,控制台的内容输出缺少 LocalVariableTable

所以还是老老实实用 ideaIDE 工具来运行代码吧

  1. 显示 HelloWorld.class 字节码文件的详细信息
javap -v HelloWorld.class

在控制台输出信息中可以找到 Constant Pool,其中有字节码的具体信息


6.4.2.定义


  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池(Runtime Constant Pool 是方法区的一部分。
    • 常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

对于运行时常量池,《Java虚拟机规范》并没有做任何细节的要求。

  • 不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。
  • 不过一般来说,除了保存 Class 文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性

  • Java 语言并不要求常量一定只有编译期才能产生
    • 也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池
    • 运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制

  • 当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常

6.5.StringTable


StringTable(字符串常量池),又称 String Pool:用于存放字符串常量


6.5.1.问题


代码 1(问和答都在下方代码块中)

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";// "ab"

String s4 = s1 + s2;// new String("ab")

String s5 = "ab";

String s6 = s4.intern();

System.out.println(s3 == s4);// 答:flase
System.out.println(s3 == s5);// 答:true
System.out.println(s3 == s6);// 答:true

代码 2(问和答都在下方代码块中)

预设:环境可以是 java 1.8 ,也可以是 java 1.6

String x2 = new String("c") + new String("d");// new String("cd")

String x1 = "cd";// "cd"
x2.intern();

System.out.println(x1 == x2);// 答:false

问:如果调换上方代码中最后两行的位置呢(环境是 java 1.8 )?具体情况如下

String x2 = new String("c") + new String("d");// new String("cd")

x2.intern();
String x1 = "cd";


System.out.println(x1 == x2);// 答:true

问:如果调换上方代码中最后两行的位置呢(环境是 java 1.6 )?具体情况如下

String x2 = new String("c") + new String("d");// new String("cd")

x2.intern();// "cd" 副本
String x1 = "cd";


System.out.println(x1 == x2);// 答:false

6.5.2.常量池与串池的关系


在类加载后,常量池的内容会存放到方法区的运行时常量池中,此时 a、b、ab 都是常量池中的符号,还没有变为 java 字符串对象。

执行到引用它们的代码,即具体的某一行(如 String s2 = "b")时,会把常量池中的符号变为 java 字符串对象

从字节码角度来看的话,就是当指令执行到 ldc #2 这条指令的时候才会把 a 符号变为 “a” 字符串对象

在这里插入图片描述
在 String 对象创建完成后(即程序运行到具体的某一行时),虚拟机会开启一个空的空间 StringTable

  • 它的底层数据结构是 HashTable
  • 每个元素都是 key-value 结构
  • 采用了 数组+单向链表 的实现方式
  • 里面存储的是字符串对象的地址引用值,该引用是指向堆中对应的对象

在 String 对象创建完成后(即程序运行到具体的某一行时),会去 StringTable 中查找相应的对象。

  • 如果没有一个值相同的 key-value,就会把新创建的 String 对象放入 StringTable 中,再把地址给新建的变量
  • 如果有一个值相同的 key-value,就直接从 StringTable 中取出来用,即直接返回对应地址给新建的变量

6.5.3.StringTable 字符串拼接


常量拼接

String s1 = "a";
String s2 = "b";
String ss = "ab";

String s3 = "a" + "b";

此处是 javac 在编译期间的优化,因为都是 “a”、“b” 常量,其结果已经在编译期确定为 “ab”

此时 s3 就已经在 StringTable 中了。

在这里插入图片描述


变量拼接

String s1 = "a";
String s2 = "b";

String s4 = s1 + s2;

此处可以理解为 new StringBuilder().append("a").append("b").toString()

在这里插入图片描述

其中 StringBuilder 类中的 toString() 方法如下

@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

所以我们可以认为这里的字符串变量的拼接,就是新建了一个值为 “ab” 的 String 对象。

基本可以认为堆内存就是用来存放对象实例的。


s3 是常量拼接出的字符串,在编译期即确定并存储在 StringTable 中(其实在 JDK 8 中, StringTable 也是在堆中的)

而 s4 是在堆中新创建的一个字符串对象(是俩变量动态拼接出来的字符串),是在堆内存中的

上述二者虽然同在堆中,但所在的空间是不同的。

所以说 s3 和 s4 俩地址是不同的

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";

String s4 = s1 + s2;
String s5 = "ab";
System.out.println(System.identityHashCode(s1));//1836019240

System.out.println(System.identityHashCode(s2));//325040804

System.out.println(System.identityHashCode(s3));//1173230247

System.out.println(System.identityHashCode(s4));//856419764

System.out.println(System.identityHashCode(s5));//1173230247
System.out.println(s3 == s4);//flase
System.out.println(s3 == s5);//true

此外,s3 和 s5 的情况还说明了:如果 StringTable 中已经有了相同的对象,其就会指向同一个对象而不是指向新的对象。


此处再贴上一个示例代码的截图

在这里插入图片描述


6.5.4.StringTable 字符串延迟加载


所谓延迟加载,即程序运行到具体的某一行的时候,才会去加载。


此处可以借助 idea 工具演示一下字符串延迟加载的现象

在这里插入图片描述

相关的 count

System.out.println();//2393

System.out.println("1");//2394
System.out.println("2");//2395
System.out.println("3");//2396

System.out.println("1");//2397
System.out.println("2");//2397

6.5.5.intern 方法


Java 语言并不要求常量一定只有编译期才能产生

  • 也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池
  • 运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法
  • java 1.6java 1.8intern() 中的实现方式是不一样的(因为两者的 JVM 不同)。

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

  • String::intern() 是一个本地方法,它的作用是:
    • 如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象的引用;
    • 否则,会将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。

intern() 方法的作用(Java 1.8

  • 尝试将一个字符串放入 StringTable
    • 若不存在就放入 StringTable 并返回 StringTable 中的地址
    • 若存在,就直接返回 StringTable 中的地址。
String s1 = new String("a") + new String("b");
String s2 = s1.intern();

System.out.println(s1 == s2);//true

System.out.println(System.identityHashCode(s1));//1836019240
System.out.println(System.identityHashCode(s2));//1836019240
String x = "ab";
String s1 = new String("a") + new String("b");
String s2 = s1.intern();

System.out.println(s1 == x);//false
System.out.println(s2 == x);//true

System.out.println(System.identityHashCode(x));//1836019240
System.out.println(System.identityHashCode(s1));//325040804
System.out.println(System.identityHashCode(s2));//1836019240

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

JDK 8

  • intern() 方法的实现就不需要再拷贝字符串的实例到永久代了。
  • 既然字符串常量池已经移到 Java 堆中,那只需要在常量池里记录一下首次出现的实例引用即可。
  • intern() 返回的引用和由 StringBuilder 创建的那个字符串实例就是同一个。

intern() 方法的作用(Java 1.6

  • 尝试将一个字符串放入 StringTable
    • 若不存在,就会把对象复制一份放入 StringTable ,并返回 StringTable 中的字符串的副本地址
      • 需要注意的是:副本地址与原来的对象的地址是不同的
    • 若存在,就直接返回 StringTable 中的字符串的地址。
String s1 = new String("a") + new String("b");
String s2 = s1.intern();

System.out.println(s1 == s2);//false

System.out.println(System.identityHashCode(s1));//1290300832
System.out.println(System.identityHashCode(s2));//796216018
String x = "ab";
String s1 = new String("a") + new String("b");
String s2 = s1.intern();

System.out.println(s1 == x);//false
System.out.println(s2 == x);//true

System.out.println(System.identityHashCode(x));//1290300832
System.out.println(System.identityHashCode(s1));//796216018
System.out.println(System.identityHashCode(s2));//1290300832

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

JDK 6

  • intern() 方法会把首次遇到的字符串实例复制到永久代字符串常量池中存储
  • 返回的也是永久代里面这个字符串实例的引用
  • StringBuilder 创建的字符串对象实例在 Java 堆上,所以必然不可能是同一个引用。

6.5.6.StringTable 特性总结


  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilderjava 1.8
  • 字符串常量拼接的原理是编译期优化

  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池。
    • java 1.8 将这个字符串对象尝试放入串池。
      • 如果有则并不会放入串池;如果没有则放入串池。
      • 无论有无,都会把串池中的对象的引用返回
    • java 1.6 将这个字符串对象尝试放入串池。
      • 如果有则并不会放入;如果没有会把此对象复制一份(此时会获得新的地址),并放入串池。
      • 无论有无,会把串池中的对象的引用返回

6.5.7.StringTable 位置


jdk1.6 的 StringTable 位置是在永久代中,jdk 1.8 的 StringTable 位置是在堆中。

之前在 6.2.永久代 中讲述了 JVM 内存结构变化的原因,此处只贴图,不赘述。

在这里插入图片描述

jdk7 中将 StringTable 放到了堆空间中。因为永久代的回收效率很低,在 Full GC 时才会触发。

而 Full GC 是老年代的空间不足,永久代不足时才会触发。

这就导致 StringTable 的回收效率不高。

而开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。

放入对立,可以及时回收内存。


可以使用下面的代码来检验上图的情况

import java.util.ArrayList;
import java.util.List;

/**
 * 演示 StringTable 位置
 * 
 * 要想程序报错的话,需要更改 idea 中的相关类的 vm options 设置
 * * 在 jdk8 下设置 -Xmx10m -XX:-UseGCOverheadLimit
 * * 在 jdk6 下设置 -XX:MaxPermSize=10m
 */
public class Demo1_18 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 2600000; j++) {
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

java 1.6

java.lang.OutOfMemoryError: PermGen space

java 1.8

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

6.5.8.StringTable 垃圾回收


StringTable 在内存紧张时,会发生垃圾回收

/**
 * 演示 StringTable 垃圾回收
 * 修改相关类的 VM options
 * * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_19 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

在输出的内容中,控制台最上层的几行(诸如下方代码块所示)就是垃圾回收的信息 (随便看看就行)

[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->692K(9728K), 0.0009676 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2536K->504K(2560K)] 2740K->772K(9728K), 0.0008896 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2552K->488K(2560K)] 2820K->788K(9728K), 0.0008643 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

在输出的内容中,需要关注的是 StringTable statistics 这段信息 (随便看看就行)

StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :     25686 =    616464 bytes, avg  24.000
Number of literals      :     25686 =   1497648 bytes, avg  58.306
Total footprint         :           =   2594216 bytes

6.5.9.StringTable 性能调优


StringTable 底层是一个 HashTable,是 key-value 结构。


参考博客看了这篇文章,我搞懂了 StringTable

在介绍性能调优之前先阐述下 StringTable 的底层实现

当存入一个元素的时候,就会将其 key 通过 hash 函数计算得出数组的下标并存放在对应的位置。

哈希冲突

  • 比如现在有一个 key-value,这个 key 通过 hash 函数计算结果为 2,那么就把 value 存放在数组下标为 2 的位置。
  • 但是如果现在又有一个 key 通过 hash 函数计算出了相同的结果,比如也是 2,但 2 的位置已经有值了。
  • 这种现象就叫做哈希冲突

关于哈希冲突的解决办法之一是 链表法

链表法 就是将下标一样的元素通过链表的形式串起来

如果数组容量很小但是元素很多,那么发生哈希冲突的概率就会提高。

众所周知,链表的效率远没有数组那么高,哈希冲突过多会影响性能。

为了减少哈希冲突的概率,我们可以适当的增加数组的大小。

数组的每一格在 StringTable 中叫做 bucket,我们可以增加 bucket 的数量来提高性能。


可以适当增加 HashTable 中桶的个数,来减少字符串放入串池所需要的时间

-XX:StringTableSize=桶个数

但是需要注意的是 bucket 的最小个数为 1009,小于这个数就会报错

Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
StringTable size of 100 is invalid; must be between 1009 and 2305843009213693951

代码

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_20 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\IdeaProjects\\JVMStudies\\chapter01\\src\\main\\resources\\linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }
    }
}

输出结果(部分)

StringTable statistics:
Number of buckets       :      1009 =      8072 bytes, avg   8.000
Number of entries       :    482776 =  11586624 bytes, avg  24.000
Number of literals      :    482776 =  29826168 bytes, avg  61.781
Total footprint         :           =  41420864 bytes

此外,也可以考虑将字符串对象入池来减少数据对内存的大量占用,之前的 intern 方法就可以减少重复入池。


7.直接内存


7.1.定义


Direct Memory 不属于 JVM 管理内存,而是属于操作系统的内存。

  • 常见于 NIO 操作是,用于数据缓存区
  • 分配回收成本较高,但读写性能更加强
  • 不受 JVM 内存回收管理

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。

但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

JDK 1.4 中新加入了 NIONew Input / Output)类

  • 引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式
  • 它可以使用 Native 函数库直接分配堆外内存
  • 然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。
  • 这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

显然,本机直接内存的分配不会受到 Java 堆大小的限制。

但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP 分区或者分页文件)大小以及处理器寻址空间的限制。

一般服务器管理员配置虚拟机参数时,会根据实际内存去设置 -Xmx 等参数信息。

但他们经常忽略掉直接内存。

使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。


7.2.文件读写流程


需要通过到操作系统中的系统内存来读取到磁盘文件,Java 代码是不能直接访问到磁盘文件的。

从下图可以看出来一个问题,系统内存中有缓存区,JAVA 堆内存中也有缓存区,有两份缓存区。

这就造成了一个不必要的数据的复制,这样的话效率会降低。

在这里插入图片描述


JDK 1.4 中新加入了 NIO

  • 引入了一种基于 ChannelBufferI/O 方式,它可以使用 Native 函数库直接分配堆外内存。
  • 然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。
  • 这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

显然,本机直接内存的分配不会受到 Java 堆大小的限制。

在这里插入图片描述

简而言之,就是直接内存是操作系统和 Java 代码都可以访问的一块区域

无需将代码从系统内存复制到 Java 堆内存,效率也因此得到了提高。


7.3.分配和回收原理


  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner虚引用) 来监测 ByteBuffer 对象
    • 一旦 ByteBuffer 对象被垃圾回收
    • 那么就会由 ReferenceHandler 线程通过 Cleanerclean 方法调用 freeMemory 来释放直接内存

参考博客JVM 内存结构 超详细学习笔记(一)

直接内存的释放不是通过 JVM 的回收来管理的,而是通过 Unsafe 类来管理的

通过 Unsafe 中的 freemMemory() 方法进行释放的

public native void freeMemory(long var1);

了解 Unsafe 类的使用(下面的代码块是视频里的一段演示代码)

import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;

/**
 * 直接内存分配的底层原理:Unsafe
 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();

        // 分配内存
        /* ********************************************************************** */
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        /* ********************************************************************** */
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

接下来再看一下 allocateDirect() 方法具体是怎么和 Unsafe 类产生联系的

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

再来查看 DirectBuffer

class DirectByteBuffer

// Primary constructor
//
DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
    	/* ********************************************************************** */
        base = unsafe.allocateMemory(size);
        /* ********************************************************************** */
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    
    /* ********************************************************************** */
    unsafe.setMemory(base, size, (byte) 0);
    /* ********************************************************************** */
    
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    
    /* ********************************************************************** */
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    /* ********************************************************************** */
    
    att = null;

}

在上面的代码块里,有一个很特殊的类库,虚引用类型的 Cleaner

其特点是如果这个虚引用对象所关联的对象被回收后,那么 Cleaner 就会触发 Cleaner>clean 方法

其中 Cleaner.create(this, new Deallocator(base, size, cap) 中的 this 指的是 ByteBufferDirectBuffer 类嘛,自己调用自己)

ByteBuffer 对象是 Java 对象,可以被 JVM 垃圾回收。

ByterBuffer 对象被回收后,会触发 Cleaner 中的 clean() 方法

并且执行 clean 方法的线程并不是主线程,而是有一个专门的线程在后台监测这个虚引用对象

Cleaner extends PhantomReference<Object>

public static Cleaner create(Object var0, Runnable var1) {
    return var1 == null ? null : add(new Cleaner(var0, var1));
}

Cleaner extends PhantomReference<Object>

public void clean() {
    if (remove(this)) {
        try {
        
        	/* ************************************************ */
        	//这个 thunk 在之前定义过:private final Runnable thunk;
            this.thunk.run();
            /* ************************************************ */
            
        } catch (final Throwable var2) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.err != null) {
                        (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                    }

                    System.exit(1);
                    return null;
                }
            });
        }

    }
}

ByteBufferJVM 回收后,触发了 this.thunk.run(); 这行代码

此时再回到 DirectBuffer 类中

DirectBuffer 类中的 run 方法调用了 freeMemory() 方法

class DirectByteBuffer

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

于是乎分配内存讲了,内存回收也讲了,溜了溜了。


/**
 * 禁用显式回收对直接内存的影响
 */
public class Demo1_25 {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC 禁用显式的
     */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    }
}

JVM 实现者可以通过 System.gc() 调用来决定 JVMGC 行为。

System.gc(),是会显示触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存

其不但会回收新生代还会回收老年代,是一种比较吃性能的操作,会降低程序执行的效率。

此外,System.gc() 调用附带一个免责申明,无法保证对垃圾收集器的调用。

使用参数:-XX:+DisableExplicitGC 可以禁用显示的垃圾回收。

但是这个参数可能会影响到直接内存的回收。

没有 System.gc() 来释放直接内存,那么只有等到真正的垃圾回收(空间是真的不足的时候)是才会一起释放直接内存

这样造成的后果就是直接内存的使用过大。

当然也有方法可以避免,即手动内存释放,本质上可以通过 unsafe 对象调用 freeMemory 的方式释放直接内存。


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

  • 虽然使用 DirectByteBuffer 分配内存也会抛出内存溢出异常
  • 但它抛出异常时并没有真正向操作系统申请分配内存
  • 而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常
  • 真正申请分配内存的方法是 Unsafe::allocateMemory()

补充:HotSpot 虚拟机


感觉这一段也可以看看,就当是课外知识了解一下了。

以下的内容都是摘抄自书上。


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明


许多 Java 程序员都会潜意识地把 Java 虚拟机与 OracleJDKHotSpot 虚拟机等同看待,

也许还有一些程序员会注意到 BEA JRockitIBM J9 虚拟机,

但绝大多数人对 Java 虚拟机的认识就仅限于此了。

从1996 年初 Sun 发布的 JDK 1.0 中包含的 Sun Classic 虚拟机到今天,

曾经涌现、湮灭过许多或经典,或优秀,或有特色,或有争议的虚拟机实现。


书中回顾了 Java 虚拟机家族的发展轨迹和历史变迁,但我这里只摘抄了 HotSpot 虚拟机的部分


  • 武林盟主:HotSpot VM

相信所有 Java 程序员都听说过 HotSpot 虚拟机

它是 Sun/OracleJDKOpenJDK 中的默认 Java 虚拟机,也是目前使用范围最广的 Java 虚拟机。

但不一定所有人都知道的是,这个在今天看起来 “血统纯正” 的虚拟机在最初并非由 Sun 公司所开发,

而是由一家名为 “Longview Technologies” 的小公司设计;

甚至这个虚拟机最初并非是为 Java 语言而研发的,它来源于 Strongtalk 虚拟机,

而这款虚拟机中相当多的技术又是来源于一款为支持 Self 语言实现 “达到 C 语言 50% 以上的执行效率” 的目标而设计的 Self 虚拟机,

最终甚至可以追溯到 20 世纪 80 年代中期开发的 Berkeley Smalltalk 上。

Sun 公司注意到这款虚拟机在即时编译等多个方面有着优秀的理念和实际成果,

在 1997 年收购了 Longview Technologies 公司,从而获得了 HotSpot 虚拟机。


HotSpot 既继承了 Sun 之前两款商用虚拟机的优点(如 Exact VM 中的准确式内存管理),也有许多自己新的技术优势。

如它名称中的 HotSpot 指的就是它的热点代码探测技术

这里的描写带有 “历史由胜利者书写” 的味道

  • 其实 HotSpot 与 Exact 虚拟机基本上是同时期的独立产品,HotSpot 出现得还稍早一些。
  • 一开始 HotSpot 就是基于准确式内存管理的,而 Exact VM 之中也有与 HotSpot 几乎一样的热点探测技术。
  • 为了 Exact VMHotSpot 哪个该成为 Sun 主要支持的虚拟机,在 Sun 公司内部还争吵过一场。
  • HotSpot 击败 Exact 并不能算技术上的胜利

HotSpot 虚拟机的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,

然后通知即时编译器以方法为单位进行编译。

如果一个方法被频繁调用,或方法中有效循环次数很多,

将会分别触发标准即时编译和栈上替换编译(On-Stack ReplacementOSR)行为。

通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,

而且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,

这样有助于引入更复杂的代码优化技术,输出质量更高的本地代码。


2006 年,Sun 陆续将 SunJDK 的各个部分在 GPLv2 协议下开放了源码,形成了 Open-JDK 项目。

其中当然也包括 HotSpot 虚拟机。

HotSpot 从此成为 Sun/OracleJDKOpenJDK 两个实现极度接近的 JDK 项目的共同虚拟机。

Oracle 收购 Sun 以后,建立了 HotRockit 项目来把原来 BEA JRockit 中的优秀特性融合到 HotSpot 之中。

到了 2014 年的 JDK 8 时期,里面的 HotSpot 就已是两者融合的结果,

HotSpot 在这个过程里移除掉永久代,吸收了 JRockitJava Mission Control 监控工具等功能。


得益于 Sun/OracleJDKJava 应用中的统治地位

HotSpot 理所当然地成为全世界使用最广泛的 Java 虚拟机

是虚拟机家族中毫无争议的 “武林盟主”。


  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值