JVM之ClassFile以及字节码执行

1、Java ClassFile文件结构

在JVM虚拟机中规定了Class文件的基本结构,具体如下:

ClassFile {
     u4             magic;-------------------------------------------->魔数
     u2             minor_version;------------------------------------>主版本号
     u2             major_version;------------------------------------>次版本号
     u2             constant_pool_count;------------------------------>常量池长度
     cp_info        constant_pool[constant_pool_count-1];------------->常量池信息
     u2             access_flags;------------------------------------->该类的访问修饰符
     u2             this_class;--------------------------------------->类索引
     u2             super_class;-------------------------------------->父类索引
     u2             interfaces_count;--------------------------------->接口个数
     u2             interfaces[interfaces_count];--------------------->接口详细信息
     u2             fields_count;------------------------------------->属性个数
     field_info     fields[fields_count];----------------------------->属性详细信息
     u2             methods_count;------------------------------------>方法个数
     method_info    methods[methods_count];--------------------------->方法详情
     u2             attributes_count;--------------------------------->类文件属性个数
     attribute_info attributes[attributes_count];--------------------->类文件属性详细信息
}

ClassFile的文件结构遵循严格的定义:

  • 文件以4个字节的Magic(魔数)开头,作为Class文件的标志,其后紧接着四个字节是编译的JDK的版本号
  • 版本号之后是常量池中常量的数量(constant_pool_count),在Java中常量池容量计数是从1开始的,所以真正的常量数量为constant_pool_count-1
  • 常量池后是访问修饰符,类索引,父类索引,接口数量以及实现的接口
  • 在接口后面是field数量以及描述信息,method数量以及描述信息
  • 最后保存的是类文件的属性信息:SourceFile,LocalVariableTable,Code.....

举例

package cn.edu.learn;

public class Task implements Runnable {
    private Object lock = new Object();
    @Override
    public void run() {
        int i = 10;
        int j = 50;
        int a = i + j;
        System.out.println(a);
    }
}

经过编译以后的Class文件如下(截取部分编译文件):

cafe babe 0000 0033 0027 0a00 0200 1807
0019 0900 0600 1a09 001b 001c 0a00 1d00
1e07 001f 0700 2001 0004 6c6f 636b 0100
124c 6a61 7661 2f6c 616e 672f 4f62 6a65
6374 3b01 0006 3c69 6e69 743e 0100 0328
2956 0100 0443 6f64 6501 000f 4c69 6e65
4e75 6d62 6572 5461 626c 6501 0012 4c6f
6361 6c56 6172 6961 626c 6554 6162 6c65
0100 0474 6869 7301 0013 4c63 6e2f 6564
752f 6c65 6172 6e2f 5461 736b 3b01 0003
7275 6e01 0001 6901 0001 4901 0001 6a01
0001 6101 000a 536f 7572 6365 4669 6c65
0100 0954 6173 6b2e 6a61 7661 0c00 0a00
...............

  • 0-4字节:cafe babe class文件的标识(魔数)
  • 5-8字节:0000 0033 对应的10进制为51,51对应的版本为JDK1.7
  • 9-10字节:0027 对应的10进制为39,意味着常量池中有39-1个常量,使用
    javap -v Task可以查看文件指令,在指令文件中常量的个数的确为38个。
  • 第10个字节:由于简书做复杂的表格很困难,所以关于字节码的解析可以参考周志明的《深入理解Java虚拟机》P167-196 或者参考葛一鸣的《实战Java虚拟机》P289-321.

字节码执行

在Java虚拟会给每个线程分配私有的Java虚拟机栈,进行Java方法调用的时候,会创建一个栈帧进行压入栈顶,当方法结束以后从栈顶弹出。方法内部主要通过局部变量表与操作数栈的协同操作完成了方法的运算过程。
当Java源码被编译为Class文件以后,虚拟机通过类加载机制将类加载到系统中,根据对应的字节码指令,执行对应的操作。执行命令:

javap -v Task

可以获得class文件的字节码指令。Task类的字节码指令文件如下:

Classfile /opt/workspace/learnall/jvm/target/classes/cn/edu/learn/Task.class
  Last modified Sep 13, 2017; size 606 bytes
  MD5 checksum ea84a5acebf80b104627ddf5be76b95e
  Compiled from "Task.java"
public class cn.edu.learn.Task implements java.lang.Runnable
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#24         // java/lang/Object."<init>":()V
   #2 = Class              #25            // java/lang/Object
   #3 = Fieldref           #6.#26         // cn/edu/learn/Task.lock:Ljava/lang/Object;
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // cn/edu/learn/Task
   #7 = Class              #32            // java/lang/Runnable
   #8 = Utf8               lock
   #9 = Utf8               Ljava/lang/Object;
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcn/edu/learn/Task;
  #17 = Utf8               run
  #18 = Utf8               i
  #19 = Utf8               I
  #20 = Utf8               j
  #21 = Utf8               a
  #22 = Utf8               SourceFile
  #23 = Utf8               Task.java
  #24 = NameAndType        #10:#11        // "<init>":()V
  #25 = Utf8               java/lang/Object
  #26 = NameAndType        #8:#9          // lock:Ljava/lang/Object;
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               cn/edu/learn/Task
  #32 = Utf8               java/lang/Runnable
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public cn.edu.learn.Task();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putfield      #3                  // Field lock:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   Lcn/edu/learn/Task;

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        50
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 6
        line 11: 10
        line 12: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  this   Lcn/edu/learn/Task;
            3      15     1     i   I
            6      12     2     j   I
           10       8     3     a   I
}
SourceFile: "Task.java"

字节码的执行是通过局部变量表和操作数栈协同完成的,接下来我们看执行run()方法的字节码指令时局部变量表和操作数栈的变化。

stack=3, locals=1, args_size=1
局部变量表,操作数栈所需的内存空间,内存空间大小在编译期确定,保存在code属性中

0: bipush 10
如果调用的方法是普通方法,那么在局部变量表的第一个槽位是this变量。这个指令的意思是将指令将10压入操作数栈,此时堆栈情况如下:

槽位 局部变量表 操作数栈
0 this 10
1
2

2: istore_1
从操作数栈顶弹出一个元素,将其保存到局部变量表中。该命令中1的含义是将弹出的元素放在局部变量表的第1个位置。(从0开始计数)

槽位 局部变量表 操作数栈
0 this
1 10
2

3: bipush 50
将50压入操作数栈

槽位 局部变量表 操作数栈
0 this 50
1 10
2

5: istore_2
将从操作数中弹出一个元素,并把该元素保存在局部变量表的第2个位置。

槽位 局部变量表 操作数栈
0 this
1 10
2 50

6: iload_1
7: iload_2
将局部变量表中第1和2位置的元素压入操作数栈

槽位 局部变量表 操作数栈
0 this 10
1 10 50
2 50

8: iadd
从操作数栈中弹出两个元素,相加之后将结果压入操作数栈

槽位 局部变量表 操作数栈
0 this 60
1 10
2 50

9: istore_3
从操作数栈中弹出一个元素,压入局部变量表的第3个位置

槽位 局部变量表 操作数栈
0 this
1 10
2 50
3 60

10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
该指令有一个参数,该参数指向的是常量池的Fielddref,该指令的意思是获取Fieldref指定的对象,将其压入操作数栈。

槽位 局部变量表 操作数栈
0 this System.out
1 10
2 50
3 60

13: iload_3
将局部变量表的第3个位置的元素压入操作数栈

槽位 局部变量表 操作数栈
0 this System.out
1 10 60
2 50
3 60

14: invokevirtual #5
17: return
调用println方法,打印60到控制台,然后run()方法的调用结束。

Java定义了许多指令,下面对指令进行归类:
push:将给定的数压入操作数栈
load:将局部变量表中的第n个数压入操作数栈
store:从操作数栈顶弹出一个元素保存到局部变量表的第n个位置
dup:duplicate复制栈顶元素并且继续压入栈
pop:从栈顶弹出,直接抛弃
iadd、ladd、dadd、fadd :相加
isub、lsub、dsub、fsub:相减
imul、lmul、dmul、fmul:相乘
idiv、ldiv、ddiv、fdiv:相除
irem、lrem、drem、frem:取余
ineg、lneg、dneg、fneg:取反
iinc:自增
new、newarray:创建指令
getfield、putfield、getstatic、putstatic:操作类field
ifeq、iflt、ifle......:条件跳转
invokeXXX函数调用指令
monitorenter、monitorexit:同步指令
const,、ldc:将常量压入操作数栈

字节码解释器:

在JVM中如何处理这些指令,在hotspot\src\share\vm\interpreter\bytecodeInterpreter.cpp,通过字节码解释器对字节码命令进行处理。由于篇幅问题,请自行查看源码,下面简单讲一下关于new指令的操作。

CASE(_new): {
        //拿到运行时常量池中的索引
        u2 index = Bytes::get_Java_u2(pc+1);
        constantPoolOop constants = istate->method()->constants();
        if (!constants->tag_at(index).is_unresolved_klass()) {
          // 确保klass被初始化并且没有被终结
          oop entry = constants->slot_at(index).get_oop();
          assert(entry->is_klass(), "Should be resolved klass");
          klassOop k_entry = (klassOop) entry;
          assert(k_entry->klass_part()->oop_is_instance(), "Should be instanceKlass");
          instanceKlass* ik = (instanceKlass*) k_entry->klass_part();
          //Klass是否被初始化已经是否快速分配
          if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
            //计算分配对象所需空间
            size_t obj_size = ik->size_helper();
            oop result = NULL;
            // If the TLAB isn't pre-zeroed then we'll have to do it
            //记录是否需要将对象所有字段置零值  
            bool need_zero = !ZeroTLAB;
           //使用TLAB分配
            if (UseTLAB) {
              //使用指针碰撞尝试分配
              result = (oop) THREAD->tlab().allocate(obj_size);
            }
            if (result == NULL) {
              need_zero = true;
              // 尝试在Eden区分配
        retry:
              HeapWord* compare_to = *Universe::heap()->top_addr();
              HeapWord* new_top = compare_to + obj_size;
              if (new_top <= *Universe::heap()->end_addr()) {
                //使用CAS机制分配内存
                if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
                  goto retry;
                }
                result = (oop) compare_to;
              }
            }
            if (result != NULL) {
              // 初始化对象
              if (need_zero ) {
                HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
                obj_size -= sizeof(oopDesc) / oopSize;
                if (obj_size > 0 ) {
                  memset(to_zero, 0, obj_size * HeapWordSize);
                }
              }
             //根据是否使用偏向锁设置对象头
              if (UseBiasedLocking) {
                result->set_mark(ik->prototype_header());
              } else {
                result->set_mark(markOopDesc::prototype());
              }
              result->set_klass_gap(0);
              //元数据指针
              result->set_klass(k_entry);
              //设置在栈中的引用
              SET_STACK_OBJECT(result, 0);
              //更新PC计数器
              UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
            }
          }
        }
        // 慢分配
        CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
                handle_exception);
        SET_STACK_OBJECT(THREAD->vm_result(), 0);
        THREAD->set_vm_result(NULL);
        UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
      }

下图展示了内存分配的流程:


案例

/**
 * -Xmx10m -Xms10m -Xmn4m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseParNewGC -Xloggc:/opt/log/gc.log
 */
public class TesyFullGC {
    public static void main(String[] args) {

        byte[] bytes = new byte[1024 * 1024 * 1];

        bytes = new byte[1024 * 1024 * 4];
        //注释字段
        //bytes = null;
        bytes = new byte[1024 * 1024 * 4];
    }
}

该程序分配了10M内存,给年轻带分配了4M,只用ParNewGC作为年轻代垃圾回收器,上面的代码会抛出OutOfMemoryError的异常,按理来说bytes对象引用一个新的对象,原来的对象讲不被引用,在进行垃圾回收的时候被回收,但是结果和我想的不一样,于是我添加了

bytes = null

就能够正常执行,当时我就想测试一下Full GC会对那些内存区域进行回收,为什么添加了一行代码就能够正常执行,于是我使用

javap -v ClassFile

指令查看对应的字节码指令,对应的字节码指令如下:

Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // int 1048576
         2: newarray       byte
         4: astore_1
         5: ldc           #3                  // int 4194304
         7: newarray       byte
         9: astore_1
        10: ldc           #3                  // int 4194304
        12: newarray       byte
        14: astore_1
        15: return

添加bytes=null后的字节码

 Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // int 1048576
         2: newarray       byte
         4: astore_1
         5: ldc           #3                  // int 4194304
         7: newarray       byte
         9: astore_1
        10: aconst_null
        11: astore_1
        12: ldc           #3                  // int 4194304
        14: newarray       byte
        16: astore_1
        17: return

增加了10、11两行字节码,该字节码的意思是将局部变量表中的对象引用设置未NULL,在11行以后的指令是进行对象的分配。通过查看字节码发现在第二次分配4M内存时,局部变量表中还有存在对象的引用,不能进行垃圾回收。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容