您的位置:首页 > 编程语言 > Java开发

JVM内存模型、垃圾回收、字节码基础

2017-01-19 23:50 323 查看

前言

java文件被jvm编译成.class文件,.class文件中全部是二进制的数据。在JVM中用一个8bit的变量类型存储指令,这样0到255可以表示总共256个指令。我们编写的代码被编译成相应的指令码交给计算机执行,而在对代码进行调优的时候,可以读懂编译后的.class文件也是很重要的。

基础知识

Java内存模型



首先要先了解一下Java的内存模型,我们比较关心的主要有四个部分:堆,虚拟机栈,本地方法栈,方法区。

堆一般存放对象实例,也就是我们new 分配的对象,堆中依据对象存活时间分为新生代和老年代,新生代中又分为Eden空间和Survivor空间(主要是执行复制回收算法时使用的空间),新生代的Survivor空间又分成from空间和to空间。这里简单介绍一下新生代中发生垃圾回收的过程:



新生代生成对象的时候,首先分配在Eden区,当Eden区满了以后,执行复制清除算法,将Eden区的对象复制到From然后清除Edne区。这样下一轮时,可以继续在Eden区中分配,Eden和From区是目前存活的对象,当Eden再次满了以后,下一次MinorGC(针对新生代的GC,MajorGC针对老年代,FullGC针对全部)会将Eden和From复制到To中并清除原来的内容,以此类推。如果存活的对象太多导致Survivor区域无法容纳,还需要老年代进行分配担保,将无法保存在Survivor中的对象直接晋升到老年代。

方法区主要存放一些静态变量,类信息等,通常我们可以把这部分看作永久代,因为其中分配的对象不会被垃圾收集。

运行时常量池是class文件中每一个类或者接口的常量表,包含了字面量和符号饮用,充当一个符号表的作用。比如Java中的字符串,默认声明其实作为字面量存储在常量池中的,参考如下代码:

String a = "abc";
String b = "abc";
String c = new String("abc");
System.out.println(a == b);
System.out.println(a == c);


代码中,a==b返回true,a==c返回false,这是因为默认a和b的声明方式实际是在常量池中声明一个符号,然后a和b都指向那个符号”abc”,而c是在堆中声明了一个char数组。java中==默认判断的是两个值地址是否相等,所以第一个返回true,第二个返回false。

前面提到,常量池相当于一个符号表。我们可以把它看作一个表结构,键是常量地址,值就是存储的值。如下:



而符号引用,其实就是指类和接口的全限定名和方法的描述符,熟悉jni的话会比较清楚这些东西。简单来讲,在一个实例中可能有另一个对象的引用,那么在class文件中其实存放的是符号引用,也就是java/lang/object 这种字符串,而不是真正的指向内存地址的引用。在动态链接的过程,才会把class文件中这些“假的”符号饮用转换成真正的直接引用(也就是指向一个内存地址)。

本地方法栈就是通过Jni调用底层方法的时候,本地C/C++代码执行时候的方法栈。

虚拟机栈就是我们的Java代码执行的栈了,也是本文的重点。



Java虚拟机栈

栈帧

当一个方法被调用的时候,就进入它所在的方法栈,栈帧随着方法的创建而创建,随着方法的结束而销毁。每一个栈帧都拥有自己的本地变量表,操作数栈和运行时常量池的引用。

局部变量表

局部变量表中用slot来进行存储,一个slot可以存放一个boolean、byte、char、short、int、float、reference或returnAddress的数据,两个slot可以存储一个long或double。

操作数栈

每个栈帧中有一个操作数栈,用来存放指令执行的中间结果。有点类似于一个栈实现的计算器。

比如当我们执行一个iadd指令中,则要求操作数栈顶是两个int类型的数值,执行iadd后会把两个数值取出来求和再把结果放回操作数栈中。

基本指令集

.class文件是二进制文件,严格规定了每个字节的含义。是一组以8位字节为基础单位的二进制流,所以又叫字节码。只有两种数据类型:无符号数和表。

无符号数可以存放数字、索引引用或者UTF-8编码构成的字符串值。

表是无符号数和其他表构成的符合数据类型。指令码由一个字节表示,不同的数字0到200多代表不同的指令。

当然在分析的时候,我们一般使用javap -verbose 命令对class文件进行反编译,可以得到相应的明文指令,避免了我们对字节码参照JVM规范手册人工去翻译。所以我们主要关注的是一些指令的具体含义。

加载和存储指令

将一个本地变量加载到操作数栈:load相关指令,比如iload加载int类型,fload加载float类型等

将数值从操作数栈加载到本地变量表:store相关指令

加载常量到操作数栈:push、const相关指令

对于一些指令,比如iload_1,iload_2就是将操作数隐藏在指令中,就等同于iload 1,iload 2.

此外,需要说明,iconst n是把常量n压入操作数栈,istore n 是把操作数栈顶的数存在本地变量表第n个位置,iload n是把本地变量表第n个元素压入操作数栈,本地变量表可以看作一个ArrayList链表数据结构。



可以看到在栈帧中,对于变量的操作流程,基本就是把值从常量池拿到操作数栈,要存储的话就放在本地变量表,要计算了再拿到操作数栈,然后调用相应的指令进行计算。

算数指令

算数指令用于两个操作数栈上的值进行特定运算,并把计算结果压入操作数栈。比如add,sub,mul,div相关。

方法调用和返回指令

invokevirtual用于调用实例方法,invokespecial用于调用一些特殊实例方法,比如构造方法,invokestatic用于调用静态方法。

返回指令即return相关,比如ireturn。

其余指令

其余指令包括类型转换指令,对象创建与操作指令,操作数栈管理指令和控制转移指令,在这里不详细介绍可以查阅Java虚拟机规范。

实例

下面介绍一个工程实例,Java代码如下:

public class Main {
public int cal() {
int a = 1;
int b = 1;
return a + b;
}
public int getInteger() {
Random random = new Random();
return random.nextInt(5);
}
}


利用javap -verbose命令对class文件进行反编译,查看字节码:

反编译后,生成的字节码如下:

public class Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
#2 = Class              #25            // java/util/Random
#3 = Methodref          #2.#24         // java/util/Random."<init>":()V
#4 = Methodref          #2.#26         // java/util/Random.nextInt:(I)I
#5 = Class              #27            // Main
#6 = Class              #28            // java/lang/Object
#7 = Utf8               <init>
#8 = Utf8               ()V
#9 = Utf8               Code
#10 = Utf8               LineNumberTable
#11 = Utf8               LocalVariableTable
#12 = Utf8               this
#13 = Utf8               LMain;
#14 = Utf8               cal
#15 = Utf8               ()I
#16 = Utf8               a
#17 = Utf8               I
#18 = Utf8               b
#19 = Utf8               getInteger
#20 = Utf8               random
#21 = Utf8               Ljava/util/Random;
#22 = Utf8               SourceFile
#23 = Utf8               Main.java
#24 = NameAndType        #7:#8          // "<init>":()V
#25 = Utf8               java/util/Random
#26 = NameAndType        #29:#30        // nextInt:(I)I
#27 = Utf8               Main
#28 = Utf8               java/lang/Object
#29 = Utf8               nextInt
#30 = Utf8               (I)I
{
public Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1                  // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start  Length  Slot  Name   Signature
0       5     0  this   LMain;

public int cal();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: ireturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
LocalVariableTable:
Start  Length  Slot  Name   Signature
0       8     0  this   LMain;
2       6     1     a   I
4       4     2     b   I

public int getInteger();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new           #2                  // class java/util/Random
3: dup
4: invokespecial #3                  // Method java/util/Random."<init>":()V
7: astore_1
8: aload_1
9: iconst_5
10: invokevirtual #4                  // Method java/util/Random.nextInt:(I)I
13: ireturn
LineNumberTable:
line 12: 0
line 13: 8
LocalVariableTable:
Start  Length  Slot  Name   Signature
0      14     0  this   LMain;
8       6     1 random   Ljava/util/Random;
}
SourceFile: "Main.java"


下面开始分析主要部分:

运行时常量池

Constant pool 就是前面提到的运行时常量池,类似一个符号表,前面表示在常量池中的地址,后面的字段就是相应的值,同时Javap还会帮助我们生成一些字段辅助查看:

#1 = Methodref #6.#24 // java/lang/Object.””:()V

#2 = Class #25 // java/util/Random

#25 = Utf8 java/util/Random

#26 = NameAndType #29:#30 // nextInt:(I)I

在我们class文件的常量池中有四种类型的常量,Utf8就是字面常量,一个字符串。而Methodref是一个方法的符号引用,为什么说是符号引用呢,就是因为最后看到它其实就是一个方法描述符:字符串而已。同时,Class表示引用到的一个类,NameAndType表示一个字段或者方法。

Code属性

下面是方法的描述符标志位等信息,然后是最重要的信息:code属性。

分析其中的cal方法:

public int cal();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
...
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
LocalVariableTable:
Start  Length  Slot  Name   Signature
0       8     0  this   LMain;
2       6     1     a   I
4       4     2     b   I


stack = 2表明操作数栈最大深度是2,locals=3是本地变量表最大数目为3,args_size = 1,是因为该方法是实例方法,所以默认有一个输入参数this,指向方法所在的实例。

下面的LineNumberTable表示java源代码和字节码的对应关系,用于堆栈跟踪。

LocalVariableTable描述局部变量表中的变量和Java源代码定义变量的对应关系。其中start代表局部变量声明开始,length表示在字节码中存活的长度,结合起来就是作用域范围。参考下面的指令码:this在一开始就声明,时间限定为0,变量a在第二个时刻 istore_1存入局部变量表,所以start的值就是2,b在第四个时刻存入,所以start是4,声明周期可以依次推理。

0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: ireturn


再来简单分析一下指令,结合前面提高的指令介绍:

iconst_1,将常数1加载到操作数栈,istore_1将1从操作数栈栈顶元素1存储到局部变量表第一个位置,下面的2,3条指令同理。iload_1把局部变量表第一个元素加入操作数栈,iload_2同理,现在操作数栈有两个元素 1、1,然后iadd取出栈顶两个元素相加,ireturn返回。

总结

基本内容就是这些,对于字节码的学习,更详细的内容可以参考Java虚拟机规范。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jvm 虚拟机 内存 java