JVM内部机制(一)加载字节码

分类: 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

Java bytecode

Understanding JVM Internals

The Architecture of the Java Virtual Machine

为啥别读HotSpot VM的源码

JVM的本质

图片来源

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在加载过程中的位置

jvm bootstrap 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也找不到时, 子辈再亲自查找, 最后无法找到时抛出异常信息。

jvm loader

ext class loader

码字不易,转载注明出处。

下一篇文章 TODO: JVM中的函数调用模型