JVM内部机制(一)加载字节码
How to load classfile ?
# What ? Why ? When? How ?
public static void main(String[] args) {
int a;
System.out.println(a);
}
在网上已经有很多文章讨论JVM, 以下是讲解JVM构成和运行机理的文章链接:
Java Language and Virtual Machine Specifications
The Architecture of the Java Virtual Machine
History View
在考察一项技术前,习惯查看一下它的history。因为从History里面能观察到一件事情的发展痕迹,它又不受当前环境的约束,很享受这个过程。 而且从历史信息中,你还可以了解到相关联的人、事、物,以及它们体现出来的价值观。
拿Java来说, 它的价值观是WORA(Write Once, Run Anywhere)。
因为它的这个价值取向,他的基本架构以及相关的服务衍生全部基于这个方向。
很多人拿java和C++比,和其他语言比,其实没有什么意义。它的设计目标不仅仅是在语言或者系统指令层面, 它的价值观是平台无关性。
在JVM中,系统指令由字节码动态生成。 至于java源文件,可以简单的理解为排版工具。
看下Java历史上的关键时间点
1991年4月,有james gosling博士领导的绿色计划(green project),名字叫OAK, 是不是很酷? 它是java的前身。
2002年是java很重要的年份, 这一年java被大厂商一致性认可。同时这一年.net发布
2006年, sun 宣布开源java,并成立OpenJdk组织来维护代码。 在jdk1.7中,除了引用头文件和注释之外,其他部分两者基本一样。
2009年,oracle 收购java。
基础准备部分
对字节(byte)的理解
字节(Byte)是理解JVM内存模型的非常容易被忽略却很重的概念,它是JVM内存空间最基本的度量单位。
看一个例子
int[] foo = new int[1000000];
1 sizeof(int) == 4 * sizeof(byte), foo == 4M byte
对比
byte[] foo = new byte[1000000];
foo == 1M byte, 节省了3/4的内存
除了节省存储之外,还存在一个80/20原则。
生活中大部分信息,比如字母、汉字、可以直观感受的信息,”80%”可以用8位bit来表示。
使用byte可以轻松的表达其他类型, 不包括小数类型。浮点类型在JVM中有专门的实现。
Type | Description | Default | Size | Example Literals |
---|---|---|---|---|
boolean | true or false | false | 1 bit | true, false |
byte | twos complement integer | 0 | 1 byte | (none) |
char | Unicode character | \u0000 | 2 bytes | ‘a’, ‘\u0041’, ‘\101’,’\’, ‘'’, ‘\n’, ‘ß’ |
short | twos complement integer | 0 | 2 bytes | (none) |
int | twos complement integer | 0 | 4 bytes | -2, -1, 0, 1, 2 |
long | twos complement integer | 0 | 8 bytes | -2L, -1L, 0L, 1L, 2L |
float | IEEE 754 floating point | 0.0 | 4 bytes | 1.23e100f, -1.23e-100f, .3f, 3.14F |
double | IEEE 754 floating point | 0.0 | 8 bytes | 1.23456e300d, -1.23456e-300d, 1e1d |
IEEE-754对浮点数有巧妙的定义,可参考IEEE-754 浮点数标准与 Java 实现
潜在的问题是,为什么clsassfile要设计成这个结构?
答案是为了保持上层结构的一致性。
Java中的字节文件(classfile)
看一个简单的类
package t1_1;
import java.util.HashMap;
import java.util.Map;
/**
*
* @author zhangqinghua
*/
public class T1_1 {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
// TODO code application logic here
long time = System.currentTimeMillis();
HashMap<String,String> hm = new HashMap<String,String>();
hm.put("now", "bar");
Map<String,String> m = hm;
m.put("foo", "baz");
}
}
带调试信息编译
javac -g T1_1.java
classfile通过vscode-hexdump输出以后,直观感受如下:
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 34 00 27 0A 00 0C 00 15 0A J~:>...4.'......
00000010: 00 16 00 17 07 00 18 0A 00 03 00 15 08 00 19 08 ................
00000020: 00 1A 0A 00 03 00 1B 08 00 1C 08 00 1D 0B 00 1E ................
00000030: 00 1B 07 00 1F 07 00 20 01 00 06 3C 69 6E 69 74 ...........<init
00000040: 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 >...()V...Code..
00000050: 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 .LineNumberTable
00000060: 01 00 04 6D 61 69 6E 01 00 16 28 5B 4C 6A 61 76 ...main...([Ljav
00000070: 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 a/lang/String;)V
00000080: 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 09 ...SourceFile...
00000090: 54 31 5F 31 2E 6A 61 76 61 0C 00 0D 00 0E 07 00 T1_1.java.......
000000a0: 21 0C 00 22 00 23 01 00 11 6A 61 76 61 2F 75 74 !..".#...java/ut
000000b0: 69 6C 2F 48 61 73 68 4D 61 70 01 00 03 6E 6F 77 il/HashMap...now
000000c0: 01 00 03 62 61 72 0C 00 24 00 25 01 00 03 66 6F ...bar..$.%...fo
000000d0: 6F 01 00 03 62 61 7A 07 00 26 01 00 09 74 31 5F o...baz..&...t1_
000000e0: 31 2F 54 31 5F 31 01 00 10 6A 61 76 61 2F 6C 61 1/T1_1...java/la
000000f0: 6E 67 2F 4F 62 6A 65 63 74 01 00 10 6A 61 76 61 ng/Object...java
00000100: 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 00 11 63 /lang/System...c
00000110: 75 72 72 65 6E 74 54 69 6D 65 4D 69 6C 6C 69 73 urrentTimeMillis
00000120: 01 00 03 28 29 4A 01 00 03 70 75 74 01 00 38 28 ...()J...put..8(
00000130: 4C 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 Ljava/lang/Objec
00000140: 74 3B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A t;Ljava/lang/Obj
00000150: 65 63 74 3B 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F ect;)Ljava/lang/
00000160: 4F 62 6A 65 63 74 3B 01 00 0D 6A 61 76 61 2F 75 Object;...java/u
00000170: 74 69 6C 2F 4D 61 70 00 21 00 0B 00 0C 00 00 00 til/Map.!.......
00000180: 00 00 02 00 01 00 0D 00 0E 00 01 00 0F 00 00 00 ................
00000190: 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 .........*7..1..
000001a0: 00 01 00 10 00 00 00 06 00 01 00 00 00 0F 00 09 ................
000001b0: 00 11 00 12 00 01 00 0F 00 00 00 51 00 03 00 05 ...........Q....
000001c0: 00 00 00 25 B8 00 02 40 BB 00 03 59 B7 00 04 4E ...%8..@;..Y7..N
000001d0: 2D 12 05 12 06 B6 00 07 57 2D 3A 04 19 04 12 08 -....6..W-:.....
000001e0: 12 09 B9 00 0A 03 00 57 B1 00 00 00 01 00 10 00 ..9....W1.......
000001f0: 00 00 1A 00 06 00 00 00 15 00 04 00 16 00 0C 00 ................
00000200: 17 00 15 00 18 00 18 00 19 00 24 00 1A 00 01 00 ..........$.....
00000210: 13 00 00 00 02 00 14 .......
看下他的数据结构定义
ClassFile {
u4 magic; # 对应 CA FE BA BE,二进制字节码魔数 Magic。 在elf格式中也存在这样的模式,作用是区分二进制文件类型
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要解析到内存,供JIT或者Intepreter使用,至少要做以下几件事:
-
谁来负责解析? 解析的顺序是什么?
-
把类型的元信息、字面量加载到相应区域(调试情况),比如class name, fileds name, function name 等
-
从Main函数启动主线程后,需要一个角色来驱动指令的往前推进,这是很重要的一步
-
JVM指令的翻译需要精确的上下文,比如堆上对象的引用,如何计算fields相对于instance的偏移?
-
函数是按需加载还是热加载?link的机制是什么?
先来看一个有趣的问题。
who will load the loader?
谁来负责加载根加载器本身?
答案是bootstrap classloader本身是平台相关的一部分,它不是java实现的
看下loader在加载过程中的位置
- Bootstrap class loader
这个功能在openjdk源码中的定义位置
/jdk-8-hotspot/agent/src/share/classes/sun/jvm/hotspot/jdi/VirtualMachineImpl.java
代码片段如下:
javaLangString = st.probe("java/lang/String");
javaLangThread = st.probe("java/lang/Thread");
javaLangThreadGroup = st.probe("java/lang/ThreadGroup");
javaLangClass = st.probe("java/lang/Class");
javaLangClassLoader = st.probe("java/lang/ClassLoader");
javaLangThrowable = st.probe("java/lang/Throwable");
javaLangObject = st.probe("java/lang/Object");
javaLangCloneable = st.probe("java/lang/Cloneable");
javaIoSerializable = st.probe("java/io/Serializable");
javaLangEnum = st.probe("java/lang/Enum");
它实现了String、Thread、ThreadGroup、Class、ClassLoader、Throwable、Object、Cloneable、Serializable、Enum
这些class对象是把其它class文件映射到内存的基础, 包括下面的Ext class loader、System class loader。
- Ext class loader、System class loader
classLoaderExt.hpp 的定义位置, 负责加载jre/lib下的库文件
/jdk-8-hotspot/src/share/vm/classfile/classLoaderExt.hpp
Loader 使用父辈委托代理模型(当有load请求时,直接委托), 当上面的Loader也找不到时, 子辈再亲自查找, 最后无法找到时抛出异常信息。
码字不易,转载注明出处。
下一篇文章 TODO: JVM中的函数调用模型