JVM虚拟机

前言

jdk的体系结构示意图如下:

可见最下层的jvm是jre(java runtime environment,运行时环境)的组成部分之一。

当我们编写一段代码并运行时,会执行以下步骤:

以helloworld.java(源代码)为例,我们编写的代码会首先被javac(java编译器)编译为java.class(java字节码)文件,接下来这个class文件就会被扔到jvm中去执行。

java代码拥有跨平台的特性,即我们编写的java文件可以在各大操作系统中运行,而这个特性是依赖jvm实现的。回想一下,当我们在下载jdk的时候,会让我们选择操作系统的版本,同时jvm是属于jdk的一部分,自然就有不同平台的jvm实现。这也就是jvm主要做的事情:从软件层面屏蔽不同操作系统在底层硬件与指令上的区别。

jvm三大组成部分​​​​​​​

如图,当我们的java文件被编译器编写为字节码文件(.class文件)后,会被类装载子系统装载到运行时数据区(又称内存模型),最后由字节码执行引擎去执行被装载后的代码。完整的jvm组成部分便由这三部分组成,而其中最为津津乐道的就是内存模型部分。

内存模型

内存模型由以下部分组成:

堆(Heap)

此处存放我们new出来的实例对象。

堆是jvm内存管理模块中最大的一块,主要的gc也是在这一部分执行。

如图,堆内部由年轻代和老年代组成,年轻代又分伊甸区和幸存者区,所占比例如图。

被new出来的对象会优先放在伊甸区,伊甸区如果满了,就会执行minor gc。

gc的底层是由字节码执行引擎后台发起的一个垃圾收集线程。

可达性分析算法

GC Roots根节点:线程栈的本地变量,静态变量、本地方法栈的变量等。

该算法以GC Roots根节点作为起点,从这些节点向下搜索引用对象,找到的对象都是非垃圾对象,其余的对象都是垃圾。

经过可达性分析算法得到伊甸区中的非垃圾对象,会被复制保存到幸存者区,同时分代年龄+1,代表着经历了多少次gc,而剩余在伊甸区的垃圾对象会被gc。

经过一段时间的运行,如果伊甸区又满了,会再次触发gc。gc的对象是整个年轻代的,上一次的非垃圾对象也会被尝试gc。假设上次的非垃圾对象没有被gc,会被移入下一块空着的幸存者区。

以图中的s0、s1为例,如果此时s0存放了一部份非垃圾对象,而伊甸区已满触发gc:

  1. 伊甸区的非垃圾对象被移入s1
  2. 同时s0中上一次gc的非垃圾对象再次经历gc,分化出了新的非垃圾对象和垃圾对象,新的非垃圾对象被移入s1
  3. 伊甸区和s0的两部份的垃圾对象都被gc
  4. 当伊甸区满时,重复上述步骤,伊甸区和之前gc剩余的非垃圾对象在s0和s1流转

当分代年龄达到15时,会被移入老年代。若老年代已满,会触发full gc,它也是由字节码执行引擎后台执行的。full gc的执行对象是整个堆内存区。

若老年区能回收出空间、给年轻代的非垃圾对象移入时,一切如常;若老年代已经回收不出新的空间供移入时,会出现oom(内存溢出)。

栈(Stack)

每当一个线程开始执行时,jvm就会从此处的栈内存中分配一部分内存给该线程使用,这一部分被分配的内存空间就叫做栈帧内存空间。这部分空间用来存储线程执行过程中产生的局部变量。

举例来说,当我们使用一个方法开辟了一个线程,在这个方法内部的局部变量就会被存储在栈帧内存空间。而我们要归还这部分内存空间也十分简单,在线程执行完之后销毁掉即可。

此处的栈与数据结构中的栈是一样的,都是先进后出的结构,用来存储栈帧。

可是为什么要用栈这样的数据结构,去存储这部分内容呢?

再举一个例子,假设有一个main方法,在其内部调用了compute方法。首先执行的main方法会先开辟栈帧内存空间,而在main方法中执行到调用compute方法的这一步时,compute方法也会在栈中开辟属于自己的内存空间。

public class Math {

    public static final initData = 666;
    
    public static User user = new User();

    public static void main(String[] args) throw IOException {
        Math math = new Math();        
        math.compute();
        System.out.println("end");
    }

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }
}

此时就有了一个先后关系,compute方法后执行,那么它就后分配内存空间。当他执行完毕了,自然也就不再需要这部分内存空间,于是他的栈帧内存也就先于main方法的被销毁了。

而对应到栈这一结构,我们的分配内存相当于入栈,销毁内存相当于出栈。这一入栈出栈的先进后出原则,与我们方法的嵌套调用的步骤是一致的。

局部变量表和操作数栈

在栈中,会使用一个局部变量表,来存储局部变量。而除了局部变量表,栈还会包含操作数栈。

compute方法的字节码文件反编译结果如图:

当我们创建一个局部变量并赋值时,以compute方法中的int a = 1为例:

  1. 将int类型常量1、也就是我们要给变量a赋的值1,压入操作数栈(iconst);
  2. jvm会在局部变量表为局部变量a分配内存空间;
  3. 将操作数栈中的1出栈,将它放到局部变量a所对应的内存空间(istore)。

此时a这块内存空间所对应的值就是1。

而当我们要使用局部变量时,以compute方法为例:

  1. 将先前装载到局部变量表的a、b的整数值压入操作数栈(iload)
  2. 作int类型的加法,从操作数栈的栈顶取出a、b的整数值做加法,并把结果重新压回操作数栈(iadd)
  3. 把整型值10压入操作数栈(bipush)
  4. 作int类型的乘法,从操作数栈的栈顶取出两个整数值做乘法,并把结果30重新压回操作数栈(imul)
  5. 把结果30从操作数栈顶存储到局部变量(istore)
  6. 从局部变量表获取局部变量,压入操作数栈(iload),并返回该值(ireturn)

由此可见,操作数栈是临时的内存空间,分配给需要使用的数。

注:

在main方法中有一点较为特殊:生成了Math对象指向变量,而不是给某个变量赋值常量。

局部变量表中会生成指向Math对象的引用,而Math对象被存储到专门用于存储变量的堆中,局部变量表中的引用指向的就是堆中的Math对象。

根据局部变量表的引用我们可以找到堆中的对象,此时栈和堆的关系显而易见:栈的线程中包含了指向堆中对象的引用。

当我们使用new关键字创建对象时,实际的对象存储在堆中,对对象的引用存储在栈中。

方法出口

例子中main方法调用了compute方法,可是程序是怎么知道compute方法执行完毕后,需要回到main方法的呢?

这些内容都存到了方法出口,记录的就是执行完毕后的“出口”,即下一步。

程序计数器

每个线程在运行过程中,都会给程序计数器分配内存空间,用于存这个线程即将执行的下一行代码的内存地址(或称代码行号)。

每个线程包含程序计数器的目的显而易见,因为java是多线程的。比如当前线程为a,它的下一行代码的线程为b。此时有有cpu时间片更高的线程c(或称优先级更高)进入时,流程如下:

  1. 系统通知线程a在执行完后挂起
  2. 优先级更高的线程c开始运行
  3. 线程c执行完毕,系统从线程a的程序计数器去获取线程b的地址
  4. 开始执行线程b

在高优线程插队执行完毕后,之前的线程会恢复,当然它不会从头再来,而是从程序计数器获取下一个代码的内存地址开始执行。

而程序计数器的修改,是由字节码执行引擎完成的。

方法区(元空间)

存储了常量、静态变量、类信息。

在代码示例中,initData和user分别是常量和静态变量。

与线程中新建变量的流程一致,new User( )创建了一个User对象,静态变量user指向了它。

静态变量user会存放在方法区中,User对象会存放在堆中。

此时方法区和堆的关系也出现了:方法区的变量指向堆中的对象。

本地方法栈

以启动线程为例,在使用Thread.start方法时,会使用到一个本地方法start0,它使用native关键字修饰,是使用c++实现的。

不论是使用java还是别的语言实现,本地方法在jvm中被调用时,也需要一部份内存空间去存放数据,而这部份内存空间就是从本地方法栈去获取的。

以main线程为例,如果它运行过程中用到了本地方法,就会从本地方法栈中取出一部份内存空间,即本地方法运行时,是从本地方法栈获得内存的。​​​​​​​

在内存模型中,栈、本地方法栈、程序计数器是线程独有的,堆和方法区是线程共享的。

Android虚拟机 

在Android中,不使用jvm虚拟机,而是使用Dalvik。

不同的是,java文件编译为字节码文件后,还需要dx命令将其转换为dex文件,才能传入Dalvik虚拟机。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值