关于JVM

1.JVM(Java Virtual Machine,Java虚拟机)是一种规范,是Java虚拟机的一种规范,因此会有很多独立的JVM虚拟机,例如OpenJDK以及OracleJDK中的HotSpot阿里的 Dragonwell(龙井),都是JVM规范的一种实现。官方JVM是一个通用产品,一大目标是尽可能的兼容各个平台和满足大部分应用场景的需求。

由于开发和维护资源有限,对于特定平台和应用场景而言,官方JVM在性能和功能上,都有取舍。

笼统地说,Hotspot JVM解释运行Java字节码。解释运行并发现热点,JIT编译器将热点编译为机器码,这是在现代JVM上运行Java,性能并不差的原因。另外JVM的另一显著的功能是自动垃圾回收,GC大家都知道是垃圾回收器,它属于静态部分,然后就是GC-Invisible Heap,说白了这就是GC管不到的堆,然后是JFR,JFR在JVM调优中很重要,它可以生成性能日志,帮助我们快速定位问题。

JVM内存模型如下图,我是用VISIO画的,如果哪里搞的不对也欢迎各位大佬指正。

JVM内存模型

类加载器(ClassLoader)(用来装载.class文件)
执行引擎(执行字节码,或者执行本地方法)
运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)
从图中我们大概可以看出来,首先是类的加载,具体的加载流程如下图

类加载过程

类的加载大致分为三步,首先对文件进行 装载,然后是 链接,然后 初始化
其中链接中又包含了 验证准备以及 解析。详细说明后续会进行补充。

接下来就是核心的地方了首先是:

1.JVM栈

JVM栈是JVM中的重中之重,是核心,其对应的是一个线程,因此我们可以想象得到方法在其中执行,其中又栈帧填充,如下图所示

JVM栈帧

显然也是一个一个从外到内的去执行,最外层方法先执行,然后调用内层的方法,执行完毕后要返回外层的位置继续执行下一个方法和语句,那么就需要在进入方法的时候给当前外层方法标记一个位置,使得方法执行完毕后,能返回到外层的方法中。画图举例。我们执行一下代码

public class TestStack {
    private int data;
    public int getData() {
        return data;
    }
    public void setData(int data) {
        this.data = data;
    }
    public static void main(String[] args) {
        TestStack stack = new TestStack();
        stack.setData(6);
        System.out.println(stack.getData());
        Integer i = 5;
        test(i);
        System.out.println("main 中的 i = " + i);
    }
    private static void test(Integer i) {
        i = 7;
        System.out.println("test 中的 i = " + i);
    }
}

栈的执行

我们只要看main方法中前几行代码就够了,首先最先把main方法(最外层)压入栈,进而执行main中的方法,每执行完一个,相应的栈帧就要从JVM栈中移除,这时候一个方法已经结束,它需要回到调用它地方,那么我们就需要记录它被调用的位置,所以栈帧中便有了这个返回地址,然后进入图中的状态中,首先stack对象的getData()方法中,其局部变量需要存储,那么就需要开辟一块儿内存空间去存储它(它们),即局部变量表,当我们为其赋值计算的时候,会在操作数栈中执行计算,之后将结果及引用传回至局部变量表中,如果需要调用其他方法,这时候动态链接就起效,以上方法都执行完毕后,无论是否正常退出,都要回到调用它的地方,及返回地址,这样一个函数执行的流程大致就完了。
记住:

栈负责运行

2.堆

堆中主要存放的是所有的实例对象(包括数组),如上述代码中的TestStack对象,不过在JIT(Just-in-time)情况下有些时候也有可能在栈上分配对象实例,这里也是GC垃圾回收器操作的主要地方,由于最近的JVM虚拟机为提升性能,大部分都采用了分代回收算法,因此这里又可以被划分为年轻代和过渡代,以及老年代。过渡代是我自己起的名字,它依然属于年轻代,就是From Survivor空间以及To Survivor空间,负责对象实例的”年龄增长“;
具体的流程可以看下图,之后会详细分析。

i堆中的分代回收

这里面每一代回收都会有不同的算法选择,为的是减少消耗,在各有优势的算法中取舍,如对象多且不宜变化的老年代区域要考虑到内存碎片化的问题,而存活区就不用,这个放到以后分析,我们先来梳理一下对象的回收流程。

1.当一群对象被创建出来的时候,他们首先“诞生”在Eden区(这里会有例外情况,例如当一个很大的对象或者某些情况下,new出来的对象会直接被放在老年代);
2.第一次minorGC执行,将这一批中没有引用的实例对象标记为废弃,回收他们,并把存活的对象移动至From区,年龄+1;
3.minorGC再次执行,1、2步同时也会跟着走,再将这一批对象标记回收,活下来的移动到To区域,年龄+1;
4.minorGC又执行,之前的移动策略依然不变,继续标记对象并回收,活下来的再次移动回From区域,年龄+1
(注意这里在From/To区之间移动,可以这么理解,但本质是To区和From区的区域在交换,而不是里面的对象在交换,只是把对象整理出来,去除碎片后放到另一个区域中,To区一直都是kong)
5.再From和To区域来回移动,每次移动年龄+1,知道年龄增长到一个阈值(例如16),这时候判断为老年代,minorGC时还没有标记为死亡的时候,就移动至老年代。
6.当老年代或者方法区满的情况下,执行fullGC,fullGC会耗费大量的性能。

总结一下:

  • 每次GC都是在区满的情况下才执行;
  • 实例每次移动都会被minorGC标记是否回收;
  • 能再次被移动的一定有用,年龄每次都会跟着移动次数增加,每移动一次增加1岁;
  • 老年代的标准就是岁数,其实就是移动了多少次,一旦移动至老年代之后,minorGC便不再干预此对象,-如果该对象失效,将由fullGC进行回收。

当我们了解了GC回收机制之后,就会明白分代回收的优秀之处在于针对不同的情况做不同的选择和取舍,而不是都用一个方法,只有更合适的,没有最好的。这是一个思想,非常重要,无论是在平常的业务开发中,还是高级一些的技术选型、架构都需要考虑是否有必要,是否合适的取舍问题。

3.方法区

  • 方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法
  • 方法区创建时机:在虚拟机启动的时候创建。
  • 方法区的容量:可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。
  • 以后直接学习元数据区吧,1.8之后已经没有方法区这种说法了
    简单来说,所有定义的方法的信息都保存在该区域,因此方法去时共享区域。

总结:
1.静态变量+常量+运行时常量池 都存储在方法区中

2.实例变量存在堆内存中

4.程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器或者指针,它指向方法区中的方法字节码,就是下一个将要执行的指令代码。每一条JVM线程都有自己的PC寄存器,在任意时刻,一条JVM线程只会执行一个方法的代码。就是下一个将要执行的指令代码,由执行引擎读取指令;该方法称为该线程的当前方法(Current Method)如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址,如果该方法是native,那PC寄存器的值是undefined。我们可以通过反编译class文件看到它的作用。

总结
它相当于一个指针,每一个线程(栈)都会有一个程序计数器,它指向方法区中的方法字节码,就是下一个将要执行的指令代码,由执行引擎读取指令,内存空间非常小;
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

5.本地方法栈

** 具体做法是Native Method Stack中等级Native方法,执行引擎执行时加载本地环境即native Libraies**

6.直接内存

直接内存(Direct Memory)虽然不是程序运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁使用,而且它也可能导致OutOfMemoryError异常出现。

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

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制,从而导致动态扩展时出现OutOfMemoryError异常。

7、执行引擎

将字节码即时编译 优化 为本地代码, 然后执行。

整体总结

  • 在程序运行时类是在方法区(元数据区),实例对象本身在堆里面。

  • 方法字节码在方法区。线程调用方法执行时创建栈帧并压栈,方法的参数和局部变量在栈帧的局部变量表。

  • 对象的实例变量和对象一起在堆里,所以各个线程都可以共享访问对象的实例变量。

  • 静态变量在方法区,所有对象共享。字符串常量等常量在运行时常量池。

  • 各线程调用的方法,通过堆内的对象,方法区的静态数据,可以共享交互信息。

  • 各线程调用的方法所有参数传递、方法返回值的返回,都是使用栈帧里的操作数栈来完成的。