JVM(一)

195 阅读10分钟

image.png

1. 栈

下图是运行时栈的信息图

image.png

  1. 每一个线程都有自己的栈,栈中的数据以栈帧的格式存在。
  2. 在这个线程上正在执行的每一个方法都各自对应一个栈帧。
  3. 栈帧是一个内存区块。
  • 在一条活动的线程中,一个时间点上,只会有一个活动的栈帧,即只有只有当前正在执行方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与当前栈帧对应的是当前方法,定义这个方法的类就是当前类。
  • 执行引擎运行的所有字节码指令只会针对当前在栈顶进行操作。
  • 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶部,称为新的当前栈。

image.png 栈帧是用来支持虚拟机进行方法调用和方法执行的数据结构。栈帧中储存着方法的局部变量表,操作数栈,动态连接和方法返回地址等信息,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈中从出栈到入栈的信息。

在编译程序代码的时候,栈帧需要多大的局部变量表,多深的操作数栈已经完全确定了,并写入方法表的Code属性中,因此一个栈帧需要多少内存,不会受程序运行期变量数据的影响。

一个线程中的方法调用链可能会很长,很多方法可能同时处于运行状态,对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有限的。

2. 局部变量表

  • 用于存放方法参数和方法内部定义的局部变量的存储空间。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。
  • 对于32位的boolean、byte、char、short、int、float、reference和returnAddress 一个槽就够了。
  • 类变量有两次赋初始值的过程,一次是准备阶段,赋予系统初始值,整型 = 0,布尔类型 = false;另一次是初始化阶段,赋予程序员定义的初始值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但是局部变量不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,字节码校验的时候也会被虚拟机发现而导致类加载失败!
  • 对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。
  • 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量表。一般存储8大基本类型还有引用地址和返回地址。
  • 虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表最大的数量,但是局部变量表的第0位默认存储方法所属对象的实例,这也就是"this"关键字的实现。
public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) {   
        return 0;   
     }  
    public int runInstanceMethod(char c,double d,short s,boolean b) {   
        return 0;   
     }

image.png runInstanceMethod的局部变量区第一项是个reference(引用),它指定的就是对象本身的引用,也就是我们常用的this, 但是在runClassMethod方法中,没这个引用,那是因为runClassMethod是个静态方法

  • 是变量值的存储空间,由方法参数和方法内部定义的局部变量组成,其容量用Slot1作为最小单位。在编译期间,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法,那局部变量表第0位索引的Slot存储的是方法所属对象实例的引用,因此在方法内可以通过关键字this来访问到这个隐含的参数。其余的参数按照参数表顺序排列,参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。举例说明: 此时有一关于solt槽位复用的问题
  • 局部变量表中的变量是很重要的垃圾回收根节点,被局部变量表中变量直接或者间接引用的对象都不会被回收。

实验

看如下代码,使用JVM的-XX:+PrintGC参数运行下面代码(在main函数中分别执行localVarGcN的每一个函数)

package com.winwill.jvm.basic;
public class GcTest {
    private static final int SIZE = 6 * 1024 * 1024;
    public static void localVarGc1() {
        byte[] b = new byte[SIZE];
        System.gc();
}
    public static void localVarGc2() {
        byte[] b = new byte[SIZE];
        b = null;
        System.gc();
    }
    public static void localVarGc3() {
        {
            byte[] b = new byte[SIZE];
        }
        System.gc();
    }
    public static void localVarGc4() {
        {
            byte[] b = new byte[SIZE];
        }
// b在代码块外面不可见。其使用的变量槽可以被后面的a复用
        int c = 0;
        System.gc();
    }
    public static void localVarGc5() {
        localVarGc1();
        System.gc();
    }
    public static void main(String[] args) {
//        localVarGc1();   // 没有GC
//        localVarGc2();   // GC
//        localVarGc3();   // 没有GC
//        localVarGc4();   // GC
//        localVarGc5();   // GC
    }
}

在main中分别执行localVarGc[1-5]方法,得到如下5次gc日志:

[GC (Allocation Failure) 512K->374K(130560K), 0.0006220 secs]
[GC (Allocation Failure) 886K->600K(130560K), 0.0011130 secs]
[GC (Allocation Failure) 1112K->752K(130560K), 0.0006960 secs]
[GC (Allocation Failure) 1264K->950K(131072K), 0.0015540 secs]
[GC (System.gc()) 7944K->7363K(131072K), 0.0008640 secs]
[Full GC (System.gc()) 7363K->7116K(131072K), 0.0085270 secs]
[GC (Allocation Failure) 512K->390K(130560K), 0.0008690 secs]
[GC (Allocation Failure) 902K->592K(130560K), 0.0008500 secs]
[GC (Allocation Failure) 1104K->718K(130560K), 0.0007220 secs]
[GC (Allocation Failure) 1230K->924K(131072K), 0.0012260 secs]
[GC (System.gc()) 7919K->7309K(131072K), 0.0018500 secs]
[Full GC (System.gc()) 7309K->975K(131072K), 0.0059300 secs]
[GC (Allocation Failure) 512K->374K(130560K), 0.0007940 secs]
[GC (Allocation Failure) 886K->598K(130560K), 0.0007240 secs]
[GC (Allocation Failure) 1110K->718K(130560K), 0.0007680 secs]
[GC (Allocation Failure) 1230K->916K(131072K), 0.0009900 secs]
[GC (System.gc()) 7887K->7340K(131072K), 0.0008910 secs]
[Full GC (System.gc()) 7340K->7116K(131072K), 0.0091600 secs]
[GC (Allocation Failure) 512K->416K(130560K), 0.0007990 secs]
[GC (Allocation Failure) 928K->584K(130560K), 0.0008580 secs]
[GC (Allocation Failure) 1096K->728K(130560K), 0.0007360 secs]
[GC (Allocation Failure) 1240K->910K(131072K), 0.0010150 secs]
[GC (System.gc()) 7883K->7339K(131072K), 0.0011770 secs]
[Full GC (System.gc()) 7339K->971K(131072K), 0.0069840 secs]
[GC (Allocation Failure) 512K->406K(130560K), 0.0005700 secs]
[GC (Allocation Failure) 918K->622K(130560K), 0.0011430 secs]
[GC (Allocation Failure) 1134K->710K(130560K), 0.0015010 secs]
[GC (Allocation Failure) 1222K->948K(131072K), 0.0020340 secs]
[GC (System.gc()) 7921K->7304K(131072K), 0.0013160 secs]
[Full GC (System.gc()) 7304K->7110K(131072K), 0.0091750 secs]
[GC (System.gc()) 7121K->7142K(131072K), 0.0002990 secs]
[Full GC (System.gc()) 7142K->966K(131072K), 0.0050000 secs]

从上面的gc日志中(加粗部分为System.gc触发的)可以得到如下结论:

  1. 申请了一个6M大小的空间,赋值给b引用,然后调用gc函数,因为此时这个6M的空间还被b引用着,所以不能顺利gc;
  2. 申请了一个6M大小的空间,赋值给b引用,然后将b重新赋值为null,此时这个6M的空间不再被b引用,所以可以顺利gc;
  3. 申请了一个6M大小的空间,赋值给b引用,过了b的作用返回之后调用gc函数,但是因为此时b并没有被销毁,还存在于栈帧中,这个空间也还被b引用,所以不能顺利gc;
  4. 申请了一个6M大小的空间,赋值给b引用,过了b的作用返回之后重新创建一个变量c,此时这个新的变量会复用已经失效的b变量的槽位,所以b被迫销毁了,所以6M的空间没有被任何变量引用,于是能够顺利gc;
  5. 首先调用localVarGc1(),很显然不能顺利gc,函数调用结束之后再调用gc函数,此时因为localVarGc1这个函数的栈帧已经随着函数调用的结束而被销毁,b也就被销毁了,所以6M大小的空间不被任何对象引用,于是能够顺利gc。

3. 操作数栈


操作数栈
和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时,临时数据的存储区域。下面我们通过一段简短的程序片段外加一幅图片来了解下操作数栈的作用。

**Int** a= 100;

**Int** b = 98;

**Int** c = a+b;

image.png

4. 动态链接

  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如: invokedynamic指令
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( symbolic Reference)保存在class文件的常量池里。 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。 a.b(); image.png

5. 方法返回地址

当一个方法执行的时候,只有两种方法退出改方法:

  • 执行引擎遇到任意一个方法返回的字节码。

  • 方法执行过程中遇到了异常。 一个方法使用异常完成出口方式退出,不会给他上层调用者产生任何返回值(不进行异常处理)。 方法退出的过程实际上等于把当前操作栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈帧中,调整PC计数器的值以只想方法调用指令后面的一条指令