JVM-漫游

  Write once, Run Any where. Java Virtual Machine – JVM 的存在让 Java 开发变得简单,并且一次编写多处运行。其实,JVM 就是一个抽象的计算机,它有自己的指令集,有自己的机器语言,有自己的内存管理,只要能生成规范的字节码文件,它都能运行。本文将站在 Java程序员 的角度,带着一个简单的 Java 代码来一次漫游历程,了解一下这些 Java代码 背后的故事。

  本文基于 Java HotSpot™ 虚拟机,JDK 8,从整体上进行描述。为了方便描述,将会忽略部分细节。更多详细信息请查看以下内容,传送门:

1. 概述

  首先,编写一个 Hello World!

public class Hello {
	public static void main(String[] args) {
        Hello hello = new Hello();
		System.out.println("Hello World! " + hello);
	}
}
00000000 ca fe ba be 00 00 00 34 00 1e 0a 00 06 00 10 09
00000010 00 11 00 12 08 00 13 0a 00 14 00 15 07 00 16 07
00000020 00 17 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
00000030 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
00000040 75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69
00000050 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
00000060 2f 53 74 72 69 6e 67 3b 29 56 01 00 0d 53 74 61
00000070 63 6b 4d 61 70 54 61 62 6c 65 01 00 0a 53 6f 75
00000080 72 63 65 46 69 6c 65 01 00 0a 48 65 6c 6c 6f 2e
...

  使用 javac 将源代码编译成 JVM 能识别的字节码 - Class 文件(如上二进制),Class 文件描述了一个类或接口的字段,方法,父类,常量池等信息,当我们使用 java 命令去执行一个 class文件或者jar 文件时,就启动了一个 JVM 进程。下面是 java 常用的参数和用法:

java [-options] class [args...]
java [-options] -jar jarfile [args...]
options:
  -classpath, -cp : 指定类文件搜索路径
  -D<name>=<value> : 设置一个系统属性
  -jar : 执行jar包中的应用程序
  -verbose, -verbose:class : 显示类加载信息
  -verbose:gc : 显示每个垃圾回收事件
  -verbose:jni : 显示调用本地方法或接口信息

2. VM Lifecycle

(1)使用 Java 启动 JVM,解析命令行选项,如 -client 或者 -server,加载相应的 VM 库

(2)确定堆大小,编译器类型(client or server);确定环境变量(CLASSPATH)

(3)获取 Main-Class 的类名

(4)新建主线程,调用 JNI_CreateJavaVM 创建 VM,并初始化

(5)从 Main-Class 中获取 main 方法,使用 CallStaticVoidMethod 调用main方法

(6)方法结束,检查并清除可能发生的异常,回调 ExceptionOccurred

(7)主线程使用 DetachCurrentThread 退出,减小线程计数,调用 DestroyJavaVM 退出 JVM

  以上就是 JVM 的生命周期,下面着重看一下 JVM的创建,初始化和 JVM的退出。

2.1 JNI_CreateJavaVM

(1)确保只有一个线程调用此方法

(2)检查 JNI 版本,初始化GC日志记录,初始化 OS 模块(如随机数生成器,当前pid,内存页大小,保护页等)

(3)解析传递的命令行参数,初始化标准系统属性

(4)根据解析的参数和属性,进一步初始化 OS模块,堆栈,内存和 safepoint 页。加载VM库如 libzip,libjava。初始化线程库

(5)初始化输出流记录器,初始化一些特定的线程

(6)初始化全局数据,如事件日志,内存分配器,同步原语

(7)创建线程,并把main主线程映射到当前操作系统线程,此时线程还没有添加到线程队列。初始化Java级别的同步

(8)初始化其他模块,如BootClassLoader,Interpreter,Compiler,JNI,SystemDictionary(系统字典)

(9)添加主线程到线程列表,创建VMThread(JVM内部线程,执行一些关键功能)

(10)加载并初始化 Java 核心类库

(11)启动其他辅助线程,并把 JNIEnv 返回给调用者,等待相应其他 JNI 调用

2.2 DestroyJavaVM

(1)等待所有的非守护线程结束

(2)调用 java.lang.Shutdown.shutdown(),若finalization-on-exit为true,调用对象的finalizer

(3)调用 before_exit(),关闭VM级别钩子,停止垃圾收集线程,性能分析线程等

(4)调用 JavaThread::exit(),释放 JNI处理块,移除保护页,移除当前线程

(5)停止 VM线程,停止追踪JNI,将本地运行的线程标记为vm exited,删除当前线程

(6)释放资源,返回调用者

3. Hello World

  接下来让我们深入 JVM 内部探究一下这个Hello类,使用到的工具有:jdb(一个命令行调试器,eclipse太大,还占内存OoO)和 HSDB

  首先启动 jdb,并把断点设置到main函数,这里指定使用 SerialGC 和 10m 的堆。

F:\jvm>jdb -XX:+UseSerialGC -Xms10m
正在初始化jdb...
> stop in Hello.main
正在延迟断点Hello.main。
将在加载类后设置。
> run Hello
运行 Hello
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
>
VM 已启动: 设置延迟的断点Hello.main

断点命中: "线程=main", Hello.main(), 行=9 bci=0
9            Hello hello = new Hello();

main[1] next
>
已完成的步骤: "线程=main", Hello.main(), 行=10 bci=8
10              System.out.println("Hello World! " + hello);

main[1] locals
方法参数:
args = instance of java.lang.String[0] (id=405)
本地变量:
hello = instance of Hello(id=406)
main[1]

  使用 jps 查询进程id,并用 HSDB(或CLHSDB) 连接到此进程。此时 new 了一个Hello对象,可以知道,它首先在 eden区分配,那么让我们验证一下。

JVM-漫游

  首先,查看一下堆各个代的地址空间(可以看到在JDK 8 中,已经没有PermGen区域了),然后在 eden 区扫描一下看看是否有 Hello 的实例,结果显示有一个,查看这个地址的内容,表示这个对象在main 线程的 TLABs 中,接下来又使用 inspect 命令查看了这个对象的具体内容。使用图形界面看一下这个对象的内容,具体信息如下:

JVM-漫游

  字段的意义如下:

  • _mark:Mark Word
  • _klass:元数据指针
  • _java_mirror:instanceKlass 镜像类,在Java级别访问元信息,就是 java.lang.Class 类,应该是为了安全,包装了一层
  • _super:父类
  • _layout_helper:正数表示此实例大小,负数表示数组
  • _access_flags:访问控制
  • _subklass:子类
  • _next_sibling下一个兄弟结点
  • _array_klasses:数组类型信息
  • _nonstatic_field_size:非静态字段域偏移量
  • _static_field_size:静态字段域偏移量
  • _static_oop_field_count:静态oop字段
  • _nonstatic_oop_map_size:实例oop域
  • _is_marked_dependent:是否依赖(?)
  • _init_state:类状态
  • _vtable:内嵌虚方法表
  • _itable:内嵌接口方法表

最后在看一下线程内存信息(地址是向上增长的):

JVM-漫游

  • expression stack:这个是操作数栈
  • frame:这部分表示栈帧
  • locals area:表示局部变量区域
  • New Hello:第 1 个局部变量,hello
  • NewGen objArray:第 0 个局部变量,字符串数组参数

  这里只是 JVM 里的一小部分信息,由于这个 Hello 比较简单,接下来会分析一个常用的类-java.lang.String 在 JVM 内部的信息。

上一篇:System.Configuration引用后ConfigurationManager方法用不了


下一篇:WPF:在ControlTemplate中使用TemplateBinding