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内存时,局部变量表中还有存在对象的引用,不能进行垃圾回收。