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内存模型以及垃圾回收
- JVM的stack和heap,JVM内存模型,垃圾回收策略,分代收集,增量收集
- JVM内存模型及垃圾回收机制
- JVM内存模型及垃圾回收机制
- JVM基础 之图解JVM在内存中申请对象及垃圾回收流程
- JVM内存模型及垃圾回收机制
- JVM内存模型以及垃圾回收
- JVM内存模型及垃圾回收机制
- JVM内存模型及垃圾回收算法
- JVM内存模型及垃圾回收机制
- JVM内存模型以及垃圾回收
- jvm的stack和heap,JVM内存模型,垃圾回收策略,分代收集,增量收集
- JVM内存模型以及垃圾回收
- JVM内存模型以及垃圾回收
- JVM内存模型以及垃圾回收
- java 笔记(1)-—— JVM基础,内存数据,内存释放,垃圾回收,即时编译技术JIT,高精度类型
- JVM内存模型以及垃圾回收
- JVM内存模型及垃圾回收机制
- Java基础---JVM内存管理以及垃圾回收机制
- JVM内存模型以及垃圾回收