1、JVM概述
JVM(Java Virtual Machine)即 Java 虚拟机。虚拟机就是抽象计算机(虚拟计算机)。Java 虚拟机将 Java 、Scala、Kotlin、Groovy、JRuby、Jython、PHP8(IBM) 等基于 Java 虚拟机的高级语言所编写的程序代码转换为本地的机器指令,这些机器指令经过专用的编译器编译之后,交给 CPU(Central Processing Unit)运行这些机器指令。这些机器指令以二进制编码为表现形式。
2、JVM运行时数据区
JVM运行时数据区附图:
①Class Files
Class Files 为二进制字节码文件,也就是以 .class 结尾的文件(排除手动改为 .class 后缀的情况,这里特指 Java 字节码文件)。字节码文件中存储了类、方法、字段、常量池表等有关于该类的信息。
②Class Loader
JVM 的类加载器,用于加载 Class Files 文件。
③Heap(堆)
Java 虚拟机运行时数据区占用空间最多的一块区域。Heap 主要存储了实例对象和数组。同时物理上也包括了方法区(逻辑上是分开的)。Heap 是 GC(Garbage Collector)主要工作区域。事实上,实例对象可以运用 off-heap 技术将实例对象分配到其他区域。堆是线程共享的。
④Stack(栈)
目前 Java 最主流的虚拟机是 HotSpot,而 HotSpot 虚拟机是以栈为架构的。栈的单位为栈帧,栈帧在方法调用时创建,与方法的生命周期一致,也就是说,栈帧随着方法的创建而创建,随着方法的结束而销毁。栈帧中主要存储了局部变量表、操作数栈、动态链接、方法返回值。栈是线程私有的。
⑤Method Area(方法区)
方法区物理上属于堆的一部分,逻辑上则分开。方法区主要存储了每一个 Java 类的结构信息,例如:运行时常量池、字段、方法、构造方法的字节码以及类、实例、接口初始化的特殊数据。方法区同堆一样,属于线程共享的区域。方法区在 JVM 启动时就创建。方法区多被程序员称为 Permanent 区域。
⑥Native Method Stack(本地方法栈)
本地方法栈用于支持本地方法,如C/C++代码编写的方法。
⑦PC 寄存器
PC 寄存器是线程私有的,JVM 的字节码解释器就需要通过改变 PC 寄存器的值来明确接下来执行哪一条语句。
3、GC原理
Java 堆中对象的分配:
GC(Garbage Collector)是 JVM 自动内存管理机制的具体体现。Java 堆是 Java 虚拟机运行时所占内存最大的一片区域,因此也是 GC 管理的重点区域。考虑到 Java 堆中的对象的生命周期不同,也许会存在两级分化的状况(即有的对象创建不久就被废弃,需要 GC 回收,有的对象甚至与 Java 虚拟机的生命周期一致),GC 采用分代收集的策略。Java 堆中的对象可分为两大类:年轻代和老年代。年轻代是需要 Minor GC 的区域,而老年代是需要 Full GC 的区域。而年轻代又可分为Eden域、From Survivor、To Survivor。
Java 堆中内存对象回收机制图(复制算法):
4、GC 标记算法
①引用计数算法
- 原理:引用计数算法会为程序中的每一个对象都创建一个私有的引用计数器,当目标对象被其他存活对象引用时,引用计数器中的值会 + 1,不引用时会 - 1,当对象不存在引用时,引用计数器的值为 0。
- 弊端:当一个死亡的对象与另一个死亡的对象互相存在引用时,这两个对象的引用计数器的值永远不会为 0,就会造成垃圾内存堆积,最终导致内存泄露。
②根搜索算法
- 原理:根搜索算法以对象集合作为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达,如果目标对象不可达,就意味着该对象已经死亡,使用instanceOopDesc 的 Mark Word 将其标记为垃圾对象。
5、GC 回收算法
①标记-清除(Mark-Sweep)
原理:标记-清除算法分为两个步骤:垃圾标记和内存释放。
弊端:被执行内存回收的无用对象所占用的内存空间有可能是一些不连续的内存块,不可避免的产生一些内存碎片,从而导致后续没有足够的可用的内存空间分配给较大的对象。
②复制算法(Copying)
原理:由于 Java 堆中大部分都是瞬时状态,即生命周期很短,需要尽快被回收,因此,复制算法被广泛用于 Minor GC 中。一次 Minor GC,Eden 域中存活的对象将被提升到 To Survivor 中,From Survivor 中存活的对象也将被提升到 To Survivor 中,提升一次,分代年龄增加 1,执行完毕后,Eden 域和 From Survivor 会被清空。若 From Survivor 中的某些对象的分代年龄超过了”-XX:MaxTenuringThreshold”所指定的阀值时,这些对象将会直接送入老年代。
弊端:老年代的对象生命周期都比较长,甚至与 JVM 生命周期一致,若使用复制算法,与年轻代相同的,必须给老年代的对象预留内存空间,并且这些内存空间需要等待很长时间才会被释放,严重影响了效率,因此,复制算法不适合 Full GC。
③标记-压缩算法(Mark-Compact)
原理:当成功标记出内存中的垃圾对象后,该算法会将所有的存活对象都移动到一个规整且连续的内存空间中,然后执行 Full GC。当成功执行压缩之后,已用和未用的内存都各自一边,彼此维系这下一个对象分配的起始点的标记指针。当为新对象分配内存时,则可以使用指针碰撞(Bump the Pointer)技术修改指针的偏移量将新对象分配在第一个空闲的位置上。
弊端:标记-压缩算法通常用于老年代的 Full GC 中,年轻代中更多的是使用复制算法,即 Minor GC 中。