内容简介

随着越来越多的第三方语言（Groovy、Scala、JRuby等）在Java虚拟机上运行，Java也俨然成为了一个充满活力的生态圈。本书将通过200余示例详细介绍Java虚拟机中的各种参数配置、故障排查、性能监控以及性能优化。

本书共11章。第1～3章介绍了Java虚拟机的定义、总体架构、常用配置参数。第4～5章介绍了垃圾回收的算法和各种垃圾回收器。第6章介绍了Java虚拟机的性能监控和故障诊断工具。第7章详细介绍了对Java堆的分析方法和案例。第8章介绍了Java虚拟机对多线程，尤其是对锁的支持。第9～10章介绍了Java虚拟机的核心——Class文件结构，以及Java虚拟机中类的装载系统。第11章介绍了Java虚拟机的执行系统和字节码，并给出了通过ASM框架进行字节码注入的案例。

本书不仅适合Java程序员，还适合任何一名工作于Java虚拟机之上的研发人员、软件设计师、架构师。

未经许可，不得以任何方式复制或抄袭本书之部分或全部内容。

版权所有，侵权必究。

图书在版编目（CIP）数据

实战Java虚拟机：JVM故障诊断与性能优化/葛一鸣著．—北京：电子工业出版社，2015.3

（51CTO学院系列丛书）

ISBN 978-7-121-25612-7

Ⅰ．①实…　Ⅱ．①葛…　Ⅲ．①JAVA语言－程序设计　Ⅳ．①TP312

中国版本图书馆CIP数据核字（2015）第040500号

责任编辑：董　　英

印　　刷：北京中新伟业印刷有限公司

装　　订：三河市华成印务有限公司

出版发行：电子工业出版社

北京市海淀区万寿路173信箱

邮　　编：100036

开　　本：787×980　1/16

印　　张：28.25

字　　数：633千字

版　　次：2015年3月第1版

印　　次：2015年3月第1次印刷

印　　数：3000册

定　　价：79.00元

凡所购买电子工业出版社图书有缺损问题，请向购买书店调换。若书店售缺，请与本社发行部联系，联系及邮购电话：（010）88254888。

质量投诉请发邮件至zlts@phei.com.cn，盗版侵权举报请发邮件至dbqq@phei.com.cn。

服务热线：（010）88258888。

前言

关于Java生态圈

Java是目前应用最为广泛的软件开发平台之一。随着Java以及Java社区的不断壮大，Java也早已不再是简简单单的一门计算机语言了，它更是一个平台、一种文化、一个社区。

作为一个平台，Java虚拟机扮演着举足轻重的作用。除了Java语言，任何一种能够被编译成字节码的计算机语言都属于Java这个平台。Groovy、Scala、JRuby等都是Java平台的一个部分，它们依赖于Java虚拟机，同时，Java平台也因为它们变得更加丰富多彩。

作为一种文化，Java几乎成为了“开源”的代名词。在Java程序中，有着数不清的开源软件和框架，如Tomcat、Struts、Hibernate、Spring等。就连JDK和JVM自身也有不少开源的实现，如OpenJDK、Harmony。可以说，“共享”的精神在Java世界里体现得淋漓尽致。

作为一个社区，Java拥有无数的开发人员，有数不清的论坛和资料。从桌面应用软件、嵌入式开发到企业级应用、后台服务器、中间件，都可以看到Java的身影。其应用形式之复杂、参与人数之众多也令人咋舌。可以说，Java社区已经俨然成为了一个良好而庞大的生态系统。

而本书，将主要介绍这个生态系统的核心——Java虚拟机 。

本书的体系结构

本书立足于实际开发，又不缺乏理论介绍，力求通俗易懂、循序渐进。本书共分为11章：

第1章 主要为综述，介绍了Java虚拟机的概念、定义，讲解了Java语言规范和Java虚拟机规范，最后，还介绍了OpenJDK的调试方法。

第2章 介绍了Java虚拟机的总体架构，说明了堆、栈、方法区等内存空间的作用和彼此之间的联系。

第3章 介绍了Java虚拟机的常用配置参数，重点对垃圾回收跟踪参数、内存配置参数做了详细的介绍，并给出了案例说明。

第4章 从理论层面介绍了垃圾回收的算法，如引用计数、标记清除、标记压缩、复制算法等。本章是第5章的理论基础。

第5章 讲解了基于垃圾回收的理论知识，进一步详细介绍了Java虚拟机中实际使用的各种垃圾回收器，包括串行回收器、并行回收器、CMS、G1等。

第6章 介绍了Java虚拟机的性能监控和故障诊断工具，考虑到实用性，也介绍了系统级性能监控工具的使用，两者结合，可以更好地帮助读者处理实际问题。

第7章 详细介绍了对Java堆的分析方法和案例，主要讲解了MAT和Visual VM两款工具的使用，以及各自OQL的编写方式。

第8章 介绍了Java虚拟机对多线程，尤其是对锁的支持，本章不仅介绍了虚拟机内部锁的实现、优化机制，也给出了一些Java语言层面的锁优化思路，最后，还介绍了无锁的并行控制方法。

第9章 介绍了Java虚拟机的核心——Class文件结构，Class文件作为Java虚拟机的基石，有着举足轻重的作用，对深入理解Java虚拟机有着不可忽视的作用。

第10章 介绍了Java虚拟机中类的装载系统，其中，着重介绍了Java虚拟机中ClassLoader的实现以及设计模式。

第11章 介绍了Java虚拟机的执行系统和字节码，为了帮助读者更快更好地理解Java字节码，本章对字节码进行了分类讲解，并且理论联系实际，给出了通过ASM框架进行字节码注入的案例。

本书特色

本书的主要特点有：

1. 结构清晰 。本书采用从整体到局部的视角，首先第1、2章介绍了Java虚拟机的整体概况和结构。接着步步为营，每一章节对应一个单独的知识点，力求展示虚拟机的全貌。

2. 理论结合实战 。本书不甘心于简单地枚举理论知识，在每一个理论背后，都给出了演示示例供读者参考，帮助读者更好地消化这些理论。比如，在对Class文件结构和字节码的介绍中，不仅仅简单地给出了理论说明，更是使用ASM框架将这些理论应用于实践，尽可能地做到理论和实践结合。

3. 专注专业 。本书着眼于Java虚拟机，对Java虚拟机的原理和实践做了丰富的介绍，包括但不限于体系结构、虚拟机的调试方式、常用参数、垃圾回收系统、Class文件结构、执行系统等，力求从多角度更专业地对Java虚拟机进行探讨。

4. 通俗易懂 。本书依然服务于广大虚拟机初学者，尽量避免采用过于理论的描述方式，简单的白话文风格贯穿全书，尽量做到读者在阅读过程中少盲点、无盲点。

5. 技术全面 。纵横Windows和Linux双系统下的性能诊断、涉及32位系统和64位系统的优化比较、贯穿从JDK 1.5到JDK 1.8的优化演进。

适合阅读人群

虽然本书力求通俗，但要通读本书并取得良好的学习效果，要求读者需要具备基本的Java知识或者一定的编程经验。因此，本书适合以下读者：

- 拥有一定开发经验的Java平台开发人员（Java、Scala、JRuby等）

- 软件设计师、架构师

- 系统调优人员

- 有一定的Java编程基础并希望进一步理解Java的程序员

- 虚拟机爱好者，JVM实践者

本书的约定

本书在叙述过程中，有如下约定：

- 本书中所述的JDK 1.5、JDK 1.6、JDK 1.7、JDK 1.8等同于JDK 5、JDK 6、JDK 7、JDK 8。

- 如无特殊说明，Java虚拟机均指HotSpot虚拟机。

- 如无特殊说明，本书的程序、示例均在JDK 1.7环境中运行。

- 本书赠送的课程优惠券，可以观看笔者在51CTO学院的JVM课程，地址是http://edu.51cto.com/course/course_id-1952.html。

联系作者

本书的写作过程远比我想象的更艰辛，为了让全书能够更清楚、更正确地表达和论述，我经历了好多个不眠之夜，即使现在回想起来，也忍不住让我打个寒战。由于写作水平的限制，书中难免会有不妥之处，望读者谅解。

为此，如果读者有任何疑问或者建议，非常欢迎大家加入QQ群397196583，一起探讨学习中的困难、分享学习的经验，我期待与大家一起交流、共同进步。同时，也希望大家可以关注我的博客http://www.uucode.net/。

感谢

这本书能够面世，是因为得到了众人的支持。首先，要感谢我的妻子，她始终不辞辛劳，毫无怨言地对我照顾有加，才让我得以腾出大量时间，并可以安心工作。其次，要感谢小编为我一次又一次地审稿改错，批评指正，才能让本书逐步完善。最后，感谢我的母亲30年如一日对我的体贴和关心。

参与本书编写的还有宋玉红、关硕、安继宏、白慧、薛淑英、蒋玺、曹静、马玉杰、陈明明、张丽萍、任娜娜、李清艺、荆海霞、赵全利、孙迪，特此感谢！

葛一鸣

目录

前言

第1章　初探Java虚拟机

1.1　知根知底：追溯Java的发展历程

1.1.1　那些依托Java虚拟机的语言大咖们

1.1.2　Java发展史上的里程碑

1.2　跨平台的真相：Java虚拟机来做中介

1.2.1　理解Java虚拟机的原理

1.2.2　看清Java虚拟机的种类

1.3　一切看我的：Java语言规范

1.3.1　词法的定义

1.3.2　语法的定义

1.3.3　数据类型的定义

1.3.4　Java语言规范总结

1.4　一切听我的：Java虚拟机规范

1.5　数字编码就是计算机世界的水和电

1.5.1　整数在Java虚拟机中的表示

1.5.2　浮点数在Java虚拟机中的表示

1.6　抛砖引玉：编译和调试虚拟机

1.7　小结

第2章　认识Java虚拟机的基本结构

2.1　谋全局者才能成大器：看穿Java虚拟机的架构

2.2　小参数能解决大问题：学会设置Java虚拟机的参数

2.3　对象去哪儿：辨清Java堆

2.4　函数如何调用：出入Java栈

2.4.1　局部变量表

2.4.2　操作数栈

2.4.3　帧数据区

2.4.4　栈上分配

2.5　类去哪儿了：识别方法区

2.6　小结

第3章　常用Java虚拟机参数

3.1　一切运行都有迹可循：掌握跟踪调试参数

3.1.1　跟踪垃圾回收——读懂虚拟机日志

3.1.2　类加载/卸载的跟踪

3.1.3　系统参数查看

3.2　让性能飞起来：学习堆的配置参数

3.2.1　最大堆和初始堆的设置

3.2.2　新生代的配置

3.2.3　堆溢出处理

3.3　别让性能有缺口：了解非堆内存的参数配置

3.3.1　方法区配置

3.3.2　栈配置

3.3.3　直接内存配置

3.4　Client和Server二选一：虚拟机的工作模式

3.5　小结

第4章　垃圾回收概念与算法

4.1　内存管理清洁工：认识垃圾回收

4.2　清洁工具大PK：讨论常用的垃圾回收算法

4.2.1　引用计数法（Reference Counting）

4.2.2　标记清除法（Mark-Sweep）

4.2.3　复制算法（Copying）

4.2.4　标记压缩法（Mark-Compact）

4.2.5　分代算法（Generational Collecting）

4.2.6　分区算法（Region）

4.3　谁才是真正的垃圾：判断可触及性

4.3.1　对象的复活

4.3.2　引用和可触及性的强度

4.3.3　软引用——可被回收的引用

4.3.4　弱引用——发现即回收

4.3.5　虚引用——对象回收跟踪

4.4　垃圾回收时的停顿现象：Stop-The-World案例实战

4.5　小结

第5章　垃圾收集器和内存分配

5.1　一心一意一件事：串行回收器

5.1.1　新生代串行回收器

5.1.2　老年代串行回收器

5.2　人多力量大：并行回收器

5.2.1　新生代ParNew回收器

5.2.2　新生代ParallelGC回收器

5.2.3　老年代ParallelOldGC回收器

5.3　一心多用都不落下：CMS回收器

5.3.1　CMS主要工作步骤

5.3.2　CMS主要的设置参数

5.3.3　CMS的日志分析

5.3.4　有关Class的回收

5.4　未来我做主：G1回收器

5.4.1　G1的内存划分和主要收集过程

5.4.2　G1的新生代GC

5.4.3　G1的并发标记周期

5.4.4　混合回收

5.4.5　必要时的Full GC

5.4.6　G1日志

5.4.7　G1相关的参数

5.5　回眸：有关对象内存分配和回收的一些细节问题

5.5.1　禁用System.gc()

5.5.2　System.gc()使用并发回收

5.5.3　并行GC前额外触发的新生代GC

5.5.4　对象何时进入老年代

5.5.5　在TLAB上分配对象

5.5.6　方法finalize()对垃圾回收的影响

5.6　温故又知新：常用的GC参数

5.7　动手才是真英雄：垃圾回收器对Tomcat性能影响的实验

5.7.1　配置实验环境

5.7.2　配置进行性能测试的工具JMeter

5.7.3　配置Web应用服务器Tomcat

5.7.4　实战案例1——初试串行回收器

5.7.5　实战案例2——扩大堆以提升系统性能

5.7.6　实战案例3——调整初始堆大小

5.7.7　实战案例4——使用ParrellOldGC回收器

5.7.8　实战案例5——使用较小堆提高GC压力

5.7.9　实战案例6——测试ParallelOldGC的表现

5.7.10　实战案例7——测试ParNew回收器的表现

5.7.11　实战案例8——测试JDK 1.6的表现

5.7.12　实战案例9——使用高版本虚拟机提升性能

5.8　小结

第6章　性能监控工具

6.1　有我更高效：Linux下的性能监控工具

6.1.1　显示系统整体资源使用情况——top命令

6.1.2　监控内存和CPU——vmstat命令

6.1.3　监控IO使用——iostat命令

6.1.4　多功能诊断器——pidstat工具

6.2　用我更高效：Windows下的性能监控工具

6.2.1　任务管理器

6.2.2　perfmon性能监控工具

6.2.3　Process Explorer进程管理工具

6.2.4　pslist命令——Windows下也有命令行工具

6.3　外科手术刀：JDK性能监控工具

6.3.1　查看Java进程——jps命令

6.3.2　查看虚拟机运行时信息——jstat命令

6.3.3　查看虚拟机参数——jinfo命令

6.3.4　导出堆到文件——jmap命令

6.3.5　JDK自带的堆分析工具——jhat命令

6.3.6　查看线程堆栈——jstack命令

6.3.7　远程主机信息收集——jstatd命令

6.3.8　多功能命令行——jcmd命令

6.3.9　性能统计工具——hprof

6.3.10　扩展jps命令

6.4　我是你的眼：图形化虚拟机监控工具JConsole

6.4.1　JConsole连接Java程序

6.4.2　Java程序概况

6.4.3　内存监控

6.4.4　线程监控

6.4.5　类加载情况

6.4.6　虚拟机信息

6.5　一目了然：可视化性能监控工具Visual VM

6.5.1　Visual VM连接应用程序

6.5.2　监控应用程序概况

6.5.3　Thread Dump和分析

6.5.4　性能分析

6.5.5　内存快照分析

6.5.6　BTrace介绍

6.6　来自JRockit的礼物：虚拟机诊断工具Mission Control

6.6.1　MBean服务器

6.6.2　飞机记录器（Flight Recorder）

6.7　小结

第7章　分析Java堆

7.1　对症才能下药：找到内存溢出的原因

7.1.1　堆溢出

7.1.2　直接内存溢出

7.1.3　过多线程导致OOM

7.1.4　永久区溢出

7.1.5　GC效率低下引起的OOM

7.2　无处不在的字符串：String在虚拟机中的实现

7.2.1　String对象的特点

7.2.2　有关String的内存泄漏

7.2.3　有关String常量池的位置

7.3　虚拟机也有内窥镜：使用MAT分析Java堆

7.3.1　初识MAT

7.3.2　浅堆和深堆

7.3.3　例解MAT堆分析

7.3.4　支配树（Dominator Tree）

7.3.5　Tomcat堆溢出分析

7.4　筛选堆对象：MAT对OQL的支持

7.4.1　Select子句

7.4.2　From子句

7.4.3　Where子句

7.4.4　内置对象与方法

7.5　更精彩的查找：Visual VM对OQL的支持

7.5.1　Visual VM的OQL基本语法

7.5.2　内置heap对象

7.5.3　对象函数

7.5.4　集合/统计函数

7.5.5　程序化OQL分析Tomcat堆

7.6　小结

第8章　锁与并发

8.1　安全就是锁存在的理由：锁的基本概念和实现

8.1.1　理解线程安全

8.1.2　对象头和锁

8.2　避免残酷的竞争：锁在Java虚拟机中的实现和优化

8.2.1　偏向锁

8.2.2　轻量级锁

8.2.3　锁膨胀

8.2.4　自旋锁

8.2.5　锁消除

8.3　应对残酷的竞争：锁在应用层的优化思路

8.3.1　减少锁持有时间

8.3.2　减小锁粒度

8.3.3　锁分离

8.3.4　锁粗化

8.4　无招胜有招：无锁

8.4.1　理解CAS

8.4.2　原子操作

8.4.3　新宠儿LongAddr

8.5　将随机变为可控：理解Java内存模型

8.5.1　原子性

8.5.2　有序性

8.5.3　可见性

8.5.4　Happens-Before原则

8.6　小结

第9章　Class文件结构

9.1　不仅跨平台，还能跨语言：语言无关性

9.2　虚拟机的基石：Class文件

9.2.1　Class文件的标志——魔数

9.2.2　Class文件的版本

9.2.3　存放所有常数——常量池

9.2.4　Class的访问标记（Access Flag）

9.2.5　当前类、父类和接口

9.2.6　Class文件的字段

9.2.7　Class文件的方法基本结构

9.2.8　方法的执行主体——Code属性

9.2.9　记录行号——LineNumberTable属性

9.2.10　保存局部变量和参数——LocalVariableTable属性

9.2.11　加快字节码校验——StackMapTable属性

9.2.12　Code属性总结

9.2.13　抛出异常——Exceptions属性

9.2.14　用实例分析Class的方法结构

9.2.15　我来自哪里——SourceFile属性

9.2.16　强大的动态调用——BootstrapMethods属性

9.2.17　内部类——InnerClasses属性

9.2.18　将要废弃的通知——Deprecated属性

9.2.19　Class文件总结

9.3　操作字节码：走进ASM

9.3.1　ASM体系结构

9.3.2　ASM之Hello World

9.4　小结

第10章　Class装载系统

10.1　来去都有序：看懂Class文件的装载流程

10.1.1　类装载的条件

10.1.2　加载类

10.1.3　验证类

10.1.4　准备

10.1.5　解析类

10.1.6　初始化

10.2　一切Class从这里开始：掌握ClassLoader

10.2.1　认识ClassLoader，看懂类加载

10.2.2　ClassLoader的分类

10.2.3　ClassLoader的双亲委托模式

10.2.4　双亲委托模式的弊端

10.2.5　双亲委托模式的补充

10.2.6　突破双亲模式

10.2.7　热替换的实现

10.3　小结

第11章　字节码执行

11.1　代码如何执行：字节码执行案例

11.2　执行的基础：Java虚拟机常用指令介绍

11.2.1　常量入栈指令

11.2.2　局部变量压栈指令

11.2.3　出栈装入局部变量表指令

11.2.4　通用型操作

11.2.5　类型转换指令

11.2.6　运算指令

11.2.7　对象/数组操作指令

11.2.8　比较控制指令

11.2.9　函数调用与返回指令

11.2.10　同步控制

11.2.11　再看Class的方法结构

11.3　更上一层楼：再看ASM

11.3.1　为类增加安全控制

11.3.2　统计函数执行时间

11.4　谁说Java太刻板：Java Agent运行时修改类

11.4.1　使用-javaagent参数启动Java虚拟机

11.4.2　使用Java Agent为函数增加计时功能

11.4.3　动态重转换类

11.4.4　有关Java Agent的总结

11.5　与时俱进：动态函数调用

11.5.1　方法句柄使用实例

11.5.2　调用点使用实例

11.5.3　反射和方法句柄

11.5.4　指令invokedynamic使用实例

11.6　跑得再快点：静态编译优化

11.6.1　编译时计算

11.6.2　变量字符串的连接

11.6.3　基于常量的条件语句裁剪

11.6.4　switch语句的优化

11.7　提高虚拟机的执行效率：JIT及其相关参数

11.7.1　开启JIT编译

11.7.2　JIT编译阈值

11.7.3　多级编译器

11.7.4　OSR栈上替换

11.7.5　方法内联

11.7.6　设置代码缓存大小

11.8　小结

第1章　初探Java虚拟机

什么是Java虚拟机？什么是Java语言？两者又有何关系？作为本书开篇之章，本章将主要介绍有关Java虚拟机的基本概念、发展历史和实现概要。其中，将重点介绍支撑Java世界的两份重要规范——Java语言规范和Java虚拟机规范，帮助读者更好地理解Java生态圈。

本章涉及的主要知识点有：

- 读懂Java的发展历史。

- 学习Java虚拟机的概念和种类。

- 接触Java语言规范。

- 了解Java虚拟机规范。

- 掌握单步调试Java虚拟机的方法。

1.1　知根知底：追溯Java的发展历程

目前，Java语言可以说是最常用的编程语言之一，在应用软件领域，它唯一的竞争对手似乎只有微软的.NET。C/C++作为曾经的霸主，目前依然占据着系统软件和嵌入式系统绝对的市场份额，但正在逐步退出应用软件领域。和C/C++相比，Java在设计上有着绝对的优势，开发人员可以尽快从语言本身的复杂性中解脱出来，将更多的精力投向软件自身的业务功能。由于Java语言的这份简单性，也可以认为Java是一门极好的初学者入门语言。

但是，人无完人，Java在不少地方依然受到了广大开发人员的诟病，它烦琐的语法经常受到Python等开发人员的耻笑。在语言的动态性上，甚至也远远不如和它年龄相仿的PHP语言。但为了支持动态语言，Java虚拟机推出了新的函数调用指令invokedynamic（本书将在第11章中具体介绍该指令），试图弥补Java在动态调用上的不足。

因此，值得欣慰，到目前为止，Java仍然处于快速发展期，不断壮大，不断完善。

1.1.1　那些依托Java虚拟机的语言大咖们

无论受到多少非议，Java的崛起已经是不争的事实。想起《康熙王朝》中的对白，哪一位千古帝王、功臣名将不是“褒满天下，谤满天下”。而且万幸的是，Java生态系统极具活力，在Java 8中，已经推出了函数式编程语法，试图简化Java语言在语法上的诟病。如果你不喜欢这种新的语法也没关系，Clojure语言作为Lisp的方言，可以很好地在Java虚拟机上执行。如果你受不了Lisp形式的怪异语法，Jython已经可以将Python运行在Java虚拟机上。如果你只需要一个简单的脚本，Groovy也可以成为你的选择。哦，对了，还有Scala，专注于高并发的解决方案。在这里，你可以找到你需要的一切。

而所有这一切，仍然正在不断地蓬勃发展，它们和那个看似呆板的Java语言渐行渐远，但却都深深地扎根于Java虚拟机平台上。

1.1.2　Java发展史上的里程碑

下面，将简要介绍一下Java发展史上的重大事项。

1990年，在Sun计算机公司中，由Patrick Naughton、MikeSheridan及James Gosling领导的小组Green Team，开始研发一种可控制家用电子产品的新型计算机软件技术，并希望能够研究出一种可以跨平台的系统。起先他们试着在C++的功能基础上做修改，但一直无法克服编译器的问题，所以决定自行开发新的程序语言——Oak。这里的Oak已经具备安全性、网络通信、面向对象、垃圾回收、多线程等特性。后来他们发现Oak已经被其他公司注册，于是改名为Java。

1995年，Sun正式发布Java和HotJava产品，Java首次公开亮相。

1996年1月23日Sun Microsystems发布了JDK 1.0。这个版本包括了两部分：运行环境（即JRE）和开发环境（即JDK）。在运行环境中包括了核心API、用户界面API、发布技术、Java虚拟机（JVM）几个部分。开发环境包括了编译Java程序的编译器（即javac）。在JDK 1.0时代，Java使用一款叫作Classic的虚拟机解释执行Java字节码。

1997年2月18日Sun发布了JDK 1.1，在该版本中，已经支持AWT、内部类、JDBC、RMI、反射等特性。同年，Sun收购了一家叫作Longview T echnologies的公司，从而获得了Hotspot虚拟机。

同在1997年，Jim Hugunin创造了Jython，但由于各种原因，Jython的进展相当缓慢，但到现在为止，Jython已经取得了长足的进步，甚至已经可以运行Django框架。

1998年，JDK 1.2版本发布（从这个版本开始的Java技术都称为Java 2 ）。Java 2不仅能兼容智能卡和小型消费类设备，还能兼容大型的服务器系统，它使软件开发商、服务提供商和设备提供商更加容易地抢占市场机遇。这一开发工具极大地简化了编程人员编制企业级Web应用的工作。同时Sun发布了JSP/Servlet、EJB规范，以及将Java分成了J2EE、J2SE和J2ME。这表明了Java开始向企业、桌面应用和移动设备应用3大领域挺进。此时的Java已经做到解释执行和编译执行混合运行。

2000年，JDK 1.3发布，Hotspot虚拟机成为Java的默认虚拟机。

2002年，JDK 1.4发布，古老的Classic虚拟机退出历史舞台。

2003年年底，Java平台的Scala正式发布，同年Groovy也加入了Java阵营。

2004年，JDK 1.5发布。同时JDK 1.5改名为J2SE 5.0。在这个版本中，Java语言做了大量的改进，比如支持了泛型、注解、自动装箱拆箱、枚举类型、可变长参数、增强的foreach循环等。语法上的简化和改进是这一版本的一大特色。

2006年，JDK 1.6发布。同年，Java开源并建立了OpenJDK。顺理成章，Hotspot虚拟机也成为了OpenJDK中的默认虚拟机。

2007年，Java平台迎来了新伙伴Clojure。

2008年，Oracle收购了BEA，得到了JRockit虚拟机。

2009年，Twitter宣布把后台大部分程序从Ruby迁移到Scala，这是Java平台的又一次大规模应用。

2010年，Oracle收购了Sun，获得最具价值的Hotspot虚拟机。此时，Oracle拥有市场占用率最高的两款虚拟机Hotspot和JRockit，并计划在未来对它们进行整合。

2011年，JDK 1.7发布。在JDK 1.7中，正式启用了新的垃圾回收器G1，支持了64位系统的压缩指针，以及NIO 2.0，同时新增的invokedynamic指令也是该版本的一大特色。

2014年，JDK 1.8发布。在JDK 1.8中，全新的Lambda表达式是一大亮点，它彻底改变了Java的编程风格和习惯。

Oracle计划在2016年，发布JDK 1.9。届时，最令人期待的功能应该是Java的模块化。

注意： 在本书中，JDK 1.6等同于JDK 6，JDK 1.7等同于JDK 7，JDK 1.8等同于JDK 8。

1.2　跨平台的真相：Java虚拟机来做中介

在简单了解了Java的发展历程之后，本节将着重介绍Java虚拟机的概念，最后了解一下目前最流行的Java虚拟机。

1.2.1　理解Java虚拟机的原理

所谓虚拟机，就是一台虚拟的计算机。它是一款软件，用来执行一系列虚拟计算机指令。大体上，虚拟机可以分为系统虚拟机和程序虚拟机。大名鼎鼎的Visual Box、VMware就属于系统虚拟机，它们完全是对物理计算机的仿真，提供了一个可运行完整操作系统的软件平台。程序虚拟机的典型代表就是Java虚拟机，它专门为执行单个计算机程序而设计，在Java虚拟机中执行的指令我们称为Java字节码指令。无论是系统虚拟机还是程序虚拟机，在上面运行的软件都被限制于虚拟机提供的资源中。

图1.1显示了同一个Java程序（Java字节码的集合），通过Java虚拟机运行于各大主流系统平台，该程序以虚拟机为中介，实现了跨平台的特性。

图1.1　在操作系统之上执行的虚拟机程序

1.2.2　看清Java虚拟机的种类

Java发展至今，先后出现了不少Java虚拟机。在Java发展最初，Sun使用的是一款叫作Classic的Java虚拟机，之后，在Solaris平台上还曾短暂地使用过Exact VM虚拟机，到现在，最终被大规模部署和应用的是Hotspot虚拟机。

除了Sun公司以外，各大公司以及组织都曾积极研发过Java虚拟机，比如BEA的JRockit，目前，JRockit和Hotspot都被收入Oracle旗下，大有整合的趋势。在IBM内部，使用着一款名为J9的虚拟机，广泛用于IBM的各大产品（如果当年IBM成功收购了Sun，那么很可能是J9和Hotspot进行整合了）。此外，Apache也曾经推出过与JDK 1.5和JDK 1.6兼容的Java运行平台Apache Harmony，它是开源软件，但受到同样开源的OpenJDK的压制，最终于2011年退役，虽然目前并没有Apache Harmony被大规模商用的案例，但是它的出现对Android的发展起到了极为重要的作用。在嵌入式领域，KVM和CDC/CLDC Hotspot两款虚拟机也扮演着重要的角色，在iOS和Android盛行之前，这两款虚拟机也广泛运用于手机平台。

注意： 由于目前Hotspot占有绝对的市场地位，若无特别说明，本书的示例以及参数都是针对Hotspot虚拟机的。

1.3　一切看我的：Java语言规范

讲到Java虚拟机，就不得不说Java，说到Java，就不得不提Java语言规范（Java Language Specification）。Java语言规范和Java虚拟机规范目前都可以在Oracle的官方网站上找到（http://docs.oracle.com/javase/specs/）。本节将简要介绍一下Java语言规范。

Java语言规范是用来描述Java语言的，它定义了Java语言的语言特性，比如Java的语法、词法、支持的数据类型、变量类型、数据类型转换的约定、数组、异常等内容。Java语言规范的目的是告诉开发人员“Java代码应该如何编写”。

本节将简单介绍几个在Java语言规范中定义的典型内容，帮助读者理解这份规范的意图。

1.3.1　词法的定义

词法规定了Java语法中每一个单词如何书写才是合乎规范的。比如，词法定义中规定了Java的关键字，如果开发人员使用Java的关键字作为变量名，显然无法正常通过编译。

下面简单地看一个有关标示符的定义：

Identifier:

IdentifierChars but not a Keyword or BooleanLiteral or NullLiteral

IdentifierChars:

JavaLetter

IdentifierChars JavaLetterOrDigit

JavaLetter:

any Unicode character that is a Java letter

JavaLetterOrDigit:

any Unicode character that is a Java letter-or-digit

这里定义了标示符由一个标示符串组成，但是不能是关键字、布尔字面量或者是Null字面量。而标示符串是一个由字母开头的、由字母或数字构成的串。这里的字母或数字并非简单的ABC，而是指任意的Unicode字符。

根据这个定义便可以知道，下面的方法是不符合规范的：

public static void new (){

}

因为new为Java关键字，不属于标示符，因此不能用作方法名，而下述代码则是符合规范的：

public static void 打印(){

System.out.println("中文方法");

}

虽然上述代码使用了中文作为方法名，但是根据定义，“打”、“印”属于Unicode字符，因此是符合规范的名字，也构成了符合规范的标示符。

有关词法另一个典型的案例就是数字的表示。在JDK 1.7中，以下都属于符合规范的数字：

- 整数

- 0、2、0372、0xDada_Cafe、1996、0x00_FF__00_FF

- 长整形

- 01、0777L、0x100000000L、2_147_483_648L、0xC0B0L

- 单精度浮点

- 1e1f、2.f、.3f、0f、3.14f、6.022137e+23f

- 双精度浮点

- 1e1、2.、.3、0.0、3.14、1e-9d、1e137

就直观感觉而言，上述有些数字真是长得太奇怪了，比如0xDada_Café，它不更像一个英语单词吗？而事实上，这是一个符合规范的16进制整数。Java语言规范规定，0x字符开始的表示16进制，同时，为了可读性，也允许在数字中间增加下画线进行分割（这是JDK 1.7中引入的）。

得益于Java语言规范对于词法的定义，现在Java程序可以使用更加丰富的方法来定义数字，无论是在可读性或是在表达能力上，都非常强劲。

1.3.2　语法的定义

词法定义规定了什么样的单词是合理的，语法定义规定了什么样的语句是合乎规范的。以if语句为例，在类似于Basic的语言中，可能会用以下形式定义if语句：

if Expression then

Statement

end if

但是在Java中给出了这样的定义：

IfThenStatement:

if ( Expression ) Statement

即在一个if语句中，表示条件的表达式必须用小括号标示，同时在右小括号后，书写语句块，表示执行内容。而对于Expression和Statement的具体定义，在语言规范中也有十分详细的描述，这里就不一一展开了，有兴趣的读者可以参考Java语言规范，JDK 1.7版第14章的内容“Blocks and Statements”。

1.3.3　数据类型的定义

Java语言规范中还定义了Java的数据类型。根据Java 1.7的规范，Java的数据类型分为原始数据类型和引用数据类型。原始数据类型又分为数字型和布尔型。数字型又有byte、short、int、long、char、float、double。注意，在这里char被定义为整数型，并且在规范中明确定义：byte、short、int、long分别是8位、16位、32位、64位有符号整数，而char为16位无符号整数，表示UTF-16的字符。布尔型只有两种取值：true和false。而对于float和double，规范中规定，它们是满足IEEE 754的32位浮点数和64位浮点数。

注意： 在Java语言中，char占2字节，而不是C语言中的1字节。从这点上看，Java的国际化是在语言底层就提供了强有力的支持。

此外，规范还定义了各类数字的取值范围、初始值，以及能够支持的各种操作。以整数为例，比较运算、数值运算、位运算、自增自减运算等都在规范中有描述。

除了基本数据类型外，引用数据类型也是Java重要的组成部分，引用数据类型分为3种：类或接口、泛型类型以及数组类型。

提醒： 引用类型和原始类型在Java的处理中是截然不同的，尤其对于它们的“相等”操作。

【示例1-1】在Java语言规范中，有一个简短的示例，说明了引用类型和原始类型的区别：

class Value { int val; }

class Test {

public static void main(String[] args) {

int i1 = 3;

int i2 = i1;

i2 = 4;

System.out.print("i1==" + i1);

System.out.println(" but i2==" + i2);

Value v1 = new Value();

v1.val = 5;

Value v2 = v1;

v2.val = 6;

System.out.print("v1.val==" + v1.val);

System.out.println(" and v2.val==" + v2.val);

}

}

上述程序将输出：

i1==3 but i2==4

v1.val==6 and v2.val==6

从上述输出可以看出，对于原始数据类型int，i1和i2表示不同的变量，两者毫无关系，但是对于v1和v2，它们都指向唯一一个由new关键字创建的Value对象。

由于本书并非讲解Java语言，因此对于这部分内容点到即止，有兴趣的读者可以参考Java语言规范的第4章“Types, Values, and Variables”。

1.3.4　Java语言规范总结

除上述基本内容外，Java语言规范还定义了各种不同类型间的转换规则、方法的可见性定义、有关接口的使用、注释等。

总之，Java语言规范完整定义和描述了Java语言的所有特性，因为Java语言本身不属于本书的讨论重点，故在此只做简要介绍。

1.4　一切听我的：Java虚拟机规范

虽然Java语言和Java虚拟机有着密切的联系，但两者是完全不同的内容。Java虚拟机是一台执行Java字节码的虚拟计算机，它拥有独立的运行机制，其运行的Java字节码也未必由Java语言编译而成，像Groovy、Scala等语言生成的Java字节码也可以由Java虚拟机执行。立足于Java虚拟机，可以产生各种各样的跨平台语言。除了语言特性各不相同外，它们可以共享Java虚拟机带来的跨平台性、优秀的垃圾回收器，以及可靠的即时编译器。

因此，与Java语言不同，Java虚拟机是一个高效的、性能优异的、商用级别的软件运行和开发平台，而这也是本书讨论的重点。

Java虚拟机规范的主要内容大概有以下几个部分：

- 定义了虚拟机的内部结构（将在第2章中详细介绍）。

- 定义了虚拟机执行的字节码类型和功能（将在第11章中详细介绍）。

- 定义了Class文件的结构（将在第9章中详细介绍）。

- 定义了类的装载、连接和初始化（将在第10章中详细介绍）。

以Java 1.7为例，读者可以在http://docs.oracle.com/javase/specs/jvms/se7/html/浏览虚拟机规范全文。这份规范可以说是开发Java虚拟机的指导性文件，如果要实现自定义的Java虚拟机，则需要参考和熟悉这份规范，同时这份规范对于了解现存的流行Java虚拟机（如Hotspot、IBM J9等），也有十分重要的意义。

1.5　数字编码就是计算机世界的水和电

数字是计算机内最直接、最基础的表现类型。了解数字在计算机内的表示，对于了解整个计算机系统具有相当重要的作用，数字也是专业计算机从业人员的基本功之一。本节将主要介绍整数以及浮点数在Java虚拟机中的支持情况。

1.5.1　整数在Java虚拟机中的表示

在Java虚拟机中，整数有byte、short、int、long四种，分别表示8位、16位、32位、64位有符号整数。整数在计算机中使用补码表示，在Java虚拟机中也不例外。在学习补码之前，必须先理解原码和反码。

所谓原码，就是符号位加上数字的二进制表示。以int为例，第1位表示符号位（正数或者负数），其余31位表示该数字的二进制值。

10的原码为：00000000 00000000 00000000 00001010

-10的原码为：10000000 00000000 00000000 00001010

对于原码来说，绝对值相同的正数和负数只有符号位不同。

反码就是在原码的基础上，符号位不变，其余位取反，以-10为例，其反码为：

11111111 11111111 11111111 11110101

负数的补码就是反码加1，整数的补码就是原码本身。

因此，10的补码为：

00000000 00000000 00000000 00001010

而-10的补码为：

11111111 11111111 11111111 11110110

在Java中，可以使用位运算查看整数中每一位的实际值，方法如下：

01  int a=-10;

02  for(int i=0;i<32;i++){

03      int t=(a & 0x80000000>>>i)>>>(31-i);

04      System.out.print(t);

05  }

以上代码将打印-10在虚拟机内的实际表示，程序的执行结果如下：

11111111111111111111111111110110

可以看到，这个结果和之前补码的计算结果是完全匹配的。

这段程序的基本思想是：进行32次循环（因为int有32位），每次循环取出int值中的一位，第3行的0x80000000是一个首位为1、其余位为0的整数，通过右移i位，定位到要获取的第i位，并将除该位外的其他位统一设置为0，而该位不变，最后将该位移至最右，并进行输出。

相对于原码，使用补码作为计算机内的实际存储方式至少有以下两个好处。

（1）可以统一数字0的表示。由于0既非正数，又非负数，使用原码表示时符号位难以确定，把0归入正数或者负数得到的原码编码是不同的。但是使用补码表示时，无论把0归入正数或者负数都会得到相同的结果。计算过程如下：

如果0为正数，则补码为原码本身为：00000000 00000000 00000000 00000000。

如果0为负数，则补码为反码加1，负数0的原码为：

10000000 00000000 00000000 00000000

反码为：

11111111 11111111 11111111 11111111

补码在反码的基础上加1，结果为：

00000000 00000000 00000000 00000000

可以看到，使用补码作为整数编码，可以解决数字0的存储问题。

（2）使用补码可以简化整数的加减法计算，将减法计算视为加法计算，实现减法和加法的完全统一，实现正数和负数加法的统一。现使用8位（byte）整数说明这个问题。

计算-6+5的过程如下。

-6补码：11111010

5补码：00000101

直接相加得：11111111

通过计算可知，11111111表示-1。

计算4+6的过程如下。

4的补码：00000100

6的补码：00000110

直接相加得：00001010

通过计算可知，00001010表示10（十进制）。

可以看到，使用补码表示时，只需要将补码简单地相加，即可得到算术加法的正确结果，而无须区别正数或者负数。

1.5.2　浮点数在Java虚拟机中的表示

在Java虚拟机中，浮点数有float和double两种，分别是32位和64位浮点数。浮点数在虚拟机中的表示比整数略显复杂。目前，使用最为广泛的是由IEEE 754定义的浮点数格式。目前Java虚拟机中对于浮点数的处理参考IEEE 754的规范。本节将以float为例，简要介绍浮点数的表示方法。

在IEEE 754的定义中，一个浮点数由3部分组成，分别是：符号位、指数位和尾数位。以32位float为例，符号位占1位，表示正负数，指数位占8位，尾数位占剩余的23位，如图1.2所示。

图1.2　浮点数格式

其中，sflag表示符号，当s为0时，sflag为1，当s为1时，sflag为-1。m为尾数值，实际占用空间为23位，但是根据e的取值，有24位精度。当e全为0时，尾数位附加为0，否则，尾数位附加为1。e为指数位，用8位表示。

以浮点数-5为例，其内部表示为：

1 10000001 01000000000000000000000

符号位为1表示负数，指数位为10000001，表示129。

尾数位为：01000000000000000000000。因为e不全为0，故实际的尾数位为：

101000000000000000000000

尾数位表示2的指数次幂的和，每一位表示求和数列中的对应项是否为0，这里表示：

1*20 +0*2-1 +1*2-2 +0*2-3 +0*2-4 +0*2-5 ….，对应关系如图1.3所示。

图1.3　尾数的计算

故1 10000001 01000000000000000000000的值为：

-1*2(129-127) *(1*20 +0*2-1 +1*2-2 +0*2-3 +0*2-4 +0*2-5 ) = -1*4*1.25 = -5

浮点数float还可以表示一些特殊的数字，如表1.1所示。

表1.1　float的特殊数字

正无穷

0 11111111 00000000000000000000000

负无穷

1 11111111 00000000000000000000000

NaN

0 11111111 10000000000000000000000

最大浮点数(3.4028235E38)

0 11111110 11111111111111111111111

最小规范化正浮点数(1.17549435E-38)

0 00000001 00000000000000000000000

最小正浮点数(1.4E-45)

0 00000000 00000000000000000000001

0

0 00000000 00000000000000000000000

其中，指数位全为1的表示无穷大和NaN等特殊数字。指数位全为0的为非规范化的浮点数。

【示例1-2】在Java中，使用Float. floatToRawintBits()函数可以获得一个单精度浮点数的IEEE 754表示。以下代码打印了-5的内部表示：

float a=-5;

System.out.println(Integer.toBinaryString(Float.floatToRawIntBits(a)));

程序运行结果为：

11000000101000000000000000000000

其中，Float.floatToRawIntBits()函数最终由native方法实现，具体实现如下：

JNIEXPORT jint JNICALL

Java_java_lang_Float_floatToRawIntBits(JNIEnv *env, jclass unused, jfloat v)

{

union {

int i;

float f;

} u;

u.f = (float)v;

return (jint)u.i;

}

从上述代码可以看出，为了获取float的内部表示，使用了C语言中的union自然实现这个转换。

1.6　抛砖引玉：编译和调试虚拟机

如果要对虚拟机进行深入的研究，那么编译和调试Java虚拟机是必不可少的。为何要编译自己的虚拟机呢？主要原因有两点。

第一，通过自己编译可以得到一个debug或者fastdebug版本的调试用虚拟机，调试用虚拟机可以支持更多的虚拟机参数，这些开发专用的虚拟机参数可以帮助开发人员获得更多的虚拟机内部信息，而这些参数在正常发行版本中是无法使用的。因此考虑到本书的实用性，本书并不会过多介绍那些只在调试版本中使用的参数，但如果读者有兴趣，可以编译自己的虚拟机，进行尝试。

第二，使用自己编译的调试版的虚拟机可以用于虚拟机代码的单步调试，有利于实现对虚拟机代码的理解。由于虚拟机代码比较复杂，仅通过代码阅读很难深入理解其实现机制，有时不得不依靠单步调试进行辅助，而编译后调试版本可以帮助实现这一功能。

为编译虚拟机，首先必须获得虚拟机源码，读者可以在OpenJDK的官方网站上下载最新源码，下载地址为https://jdk7.java.net/source.html。笔者目前使用的是openjdk-7u40-fcs-src-b43-26_aug_2013.zip。推荐读者使用较新的版本，因为老版本的编译脚本可能在某些平台上存在问题。

笔者的编译环境为Ubuntu系统，读者可以选择自己喜欢的Linux发行版进行编译。编译虚拟机之前，还必须做一些准备工作。

1．编译前的准备工作

（1）安装好依赖库。在Ubuntu平台上可以通过apt-get命令安装，在CentOS平台上可以通过yum命令安装。比如，笔者在编译前至少安装了以下库：

apt-get install build-essential

apt-get install libxrender-devsudo

apt-get install xorg-devsudo

apt-get install libasound2-devsudo

apt-get install libcups2-dev

apt-get install libasound2-dev

apt-get install libfreetype6-dev

apt-get install libcups2-dev

apt-get install zip

apt-get install libX11-dev

apt-get install libxext-dev

apt-get install libxrender-dev

apt-get install libxtst-dev

apt-get install libxt-dev

如果依赖库安装不全，在编译过程中就会提示错误，从错误提示中，读者应该可以得知缺少了哪些库或者头文件，相应地安装即可。当然了，gcc和g++作为基本的编译工具也是必须要安装的。

（2）准备一个Boot JDK和ANT。Boot JDK用于OpenJDK编译的执行。笔者在这里使用的是jdk1.6.0u45，也推荐读者使用JDK 1.6的版本作为JDK 1.7的Boot JDK。此外，还需要准备ANT 1.7.1以上版本，作为编译的基础执行工具。

2．准备编译

准备就绪之后，就可以开始准备编译了。

（1）进入解压后的openjdk目录：

geym@hzlab001:~/openjdk/openjdk$ ls

ASSEMBLY_EXCEPTION  build.sh  get_source.sh  hotspot.log  jaxws  langtools  make README test

build corba hotspot jaxp jdk LICENSE Makefile README-builds.html THIRD_PARTY_README

（2）新建文件build.sh，作为编译启动执行脚本。建立此脚本的目的是方便编译执行，因为编译前可能会预设一些环境变量，写成脚本后更加容易维护和执行。读者可以根据自己的执行环境，自行修改，笔者的脚本如下：

#!/bin/bash

export ANT_HOME=/home/geym/tools/apache-ant-1.9.4

export PATH=$ANT_HOME/bin:$PATH

export ALT_BOOTDIR=/home/geym/tools/jdk1.6.0_45

unset JAVA_HOME

export ALT_JDK_IMPORT_PATH=/home/geym/tools/jdk1.6.0_45

unset CLASSPATH

export SKIP_DEBUG_BUILD=false

export SKIP_FASTDEBUG_BUILD=false

export DEBUG_NAME=debug

export LANG=C

cd /geym/home/openjdk/openjdk

make sanity && make BUILD_JAXWS=false BUILD_JAXP=false  WARNINGS_ARE_ERRORS=

该脚本中，首先设置了ANT_HOME，并将ANT执行目录加入PATH，接着设置了Boot JDK的路径。这里注意，需要unset JAVA_HOME以及CLASSPATH变量。

（3）通过make命令执行整个编译，根据这个build脚本，将会同时生成debug版本、fastdebug和常规发行版本三个虚拟机的编译结果。编译的过程可能会花费比较长的时间，一般来说，编译一个版本可能需要15到45分钟，视计算机性能而定。当编译成功后，会有以下输出，显示了编译耗时。

#-- Build times ----------

Target debug_build

Start 2014-11-10 18:04:19

End   2014-11-10 18:21:14

00:01:27 corba

00:07:18 hotspot

00:07:43 jdk

00:00:26 langtools

00:16:55 TOTAL

-------------------------

进入build目录，可以看到编译的结果，下面显示了3个版本的编译结果：

geym@:~/openjdk/openjdk/build$ du -h --max-depth=1

2.5G    ./linux-amd64-fastdebug

2.4G    ./linux-amd64-debug

2.4G    ./linux-amd64

有了debug版本的虚拟机，就可以使用gdb对虚拟机进行调试了。接下来将简单地演示Java虚拟机的调试方法。

3．Java虚拟机的调试

笔者选用linux-amd64-debug下的虚拟机进行调试。

（1）首先进入linux-amd64-debug目录，查找名为gamma的文件。

geym@:~/openjdk/openjdk/build/linux-amd64-debug$ find -name gamma

./hotspot/outputdir/linux_amd64_compiler2/jvmg/gamma

该文件就是目标文件，用于调试，实际上，它就是java程序的debug版本。这里记下文件的路径。

（2）接着，进入含有Java Class执行文件的目录下并启动gdb。启动gdb后，先为gamma程序的运行设定环境变量：

(gdb) set environment JAVA_HOME=/home/geym/tools/jdk1.6.0_45

(gdb) set environment  CLASSPATH=.:${JAVA_HOME}/jre/lib/rt.jar:${JAVA_HOME}/jre/lib/i18n.jar

(gdb) set environment  HOTSPOT_BUILD_USER="geym in hotspot"

(gdb) set environment LD_LIBRARY_PATH=/home/geym/openjdk/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg:$JAVA_HOME/jre/lib/amd64:$JAVA_HOME/jre/lib/amd64/native_threads

（3）使用file指令将gamma程序设为目标文件，此时应该会显示如下信息：

(gdb) file /home/geym/openjdk/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/gamma

Reading symbols from /home/geym/openjdk/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/gamma...done.

这表示，读取调试符号信息成功。

（4）设置传递给gamma程序的参数，也就是传递给Java程序的参数，一般为需要执行的Main Class的名称，这里运行SimpleGc类，并为SimpleGc程序传入参数ggg，这相当于执行java SimpleGc ggg。

(gdb) set args SimpleGc ggg

（5）设置断点，这里在Java进程的main()函数中设置断点。

(gdb) break main

Breakpoint 1 at 0x403a02: file /home/geym/openjdk/openjdk/hotspot/src/share/tools/launcher/java.c, line 228.

（6）执行run命令启动Java程序，开始调试。

(gdb) run

Starting program: /home/geym/openjdk/openjdk/build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/gamma SimpleGc ggg

[Thread debugging using libthread_db enabled]

Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (argc=3, argv=0x7fffffffe3d8) at /home/geym/openjdk/openjdk/hotspot/src/share/tools/launcher/java.c:228

228     {

(gdb) n

省略部分输出

(gdb)

329       if (!ParseArguments(&argc, &argv, &jarfile, &classname, &ret, jvmpath)) { (gdb) p argv

$4 = (char **) 0x7fffffffe3e0

(gdb) p *argv

$7 = 0x7fffffffe695 "SimpleGc"

(gdb) p *(argv+1)

$8 = 0x7fffffffe69e "ggg"

可以看到，当前Java程序从java.c文件的第228行开始执行。使用命令next（缩写n），进行单步调试，这里省略了中间部分输出，在运行到ParseArguments()函数时，通过print（缩写p）命令显示了传递给Java进程的参数，这里可以看到作为Main Class的SimpleGc及其参数“ggg”。

至此，读者就可以使用这套环境作为辅助，更好地深入Java虚拟机内部了。

1.7　小结

本章主要介绍了Java语言和Java虚拟机的发展历史，并介绍了Java生态环境中两份非常重要的规范——Java语言规范和Java虚拟机规范。其中Java虚拟机规范将成为本书后续讨论的重点内容。同时，作为了解Java虚拟机的第一步，简要介绍了整数和浮点数在Java虚拟机中的表示方式。最后，为了方便读者更加深入地了解虚拟机，还给出了单步调试Java虚拟机的方法。

第2章　认识Java虚拟机的基本结构

Java虚拟机那么复杂，它的基本结构是什么？各个组成部分有何作用？又是如何相互协调工作的呢？本章将试图解答这些问题，而要解答这些问题就必须先了解Java堆、Java栈、永久区和元数据区的基本概念。

本章涉及的主要知识点有：

- 认识Java虚拟机中的堆。

- 了解有关栈的概念和使用。

- 了解存放类型描述的永久区和元数据区。

2.1　谋全局者才能成大器：看穿Java虚拟机的架构

Java虚拟机的基本结构如图2.1所示。

图2.1　Java虚拟机的基本结构

类加载子系统负责从文件系统或者网络中加载Class信息，加载的类信息存放于一块称为方法区的内存空间。除了类的信息外，方法区中可能还会存放运行时常量池信息，包括字符串字面量和数字常量（这部分常量信息是Class文件中常量池部分的内存映射）。

Java堆在虚拟机启动的时候建立，它是Java程序最主要的内存工作区域。几乎所有的Java对象实例都存放于Java堆中。堆空间是所有线程共享的，这是一块与Java应用密切相关的内存区间。

Java的NIO库允许Java程序使用直接内存。直接内存是在Java堆外的、直接向系统申请的内存区间。通常，访问直接内存的速度会优于Java堆。因此出于性能考虑，读写频繁的场合可能会考虑使用直接内存。由于直接内存在Java堆外，因此它的大小不会直接受限于Xmx指定的最大堆大小，但是系统内存是有限的，Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

垃圾回收系统是Java虚拟机的重要组成部分，垃圾回收器可以对方法区、Java堆和直接内存进行回收。其中，Java堆是垃圾收集器的工作重点。和C/C++不同，Java中所有的对象空间释放都是隐式的。也就是说，Java中没有类似free()或者delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象，垃圾回收系统会在后台默默工作，默默查找、标识并释放垃圾对象，完成包括Java堆、方法区和直接内存中的全自动化管理。有关垃圾回收系统的更多信息，可以参阅第4章和第5章。

每一个Java虚拟机线程都有一个私有的Java栈。一个线程的Java栈在线程创建的时候被创建。Java栈中保存着帧信息（参阅本章2.4节），Java栈中保存着局部变量、方法参数，同时和Java方法的调用、返回密切相关。

本地方法栈和Java栈非常类似，最大的不同在于Java栈用于Java方法的调用，而本地方法栈则用于本地方法调用。作为对Java虚拟机的重要扩展，Java虚拟机允许Java直接调用本地方法（通常使用C编写）。

PC（Program Counter）寄存器也是每个线程私有的空间，Java虚拟机会为每一个Java线程创建PC寄存器。在任意时刻，一个Java线程总是在执行一个方法，这个正在被执行的方法称为当前方法。如果当前方法不是本地方法，PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法，那么PC寄存器的值就是undefined。

执行引擎是Java虚拟机的最核心组件之一，它负责执行虚拟机的字节码。现代虚拟机为了提高执行效率，会使用即时编译技术将方法编译成机器码后再执行。执行引擎的进一步细节描述可以参阅第11章。

2.2　小参数能解决大问题：学会设置Java虚拟机的参数

Java虚拟机可以使用JAVA_HOME/bin/java程序启动（JAVA_HOME为JDK的安装目录），一般来说，Java进程的命令行使用方法如下：

java [-options] class [args...]

其中，-options表示Java虚拟机的启动参数，class为带有main()函数的Java类，args表示传递给主函数main()的参数。

如果需要设定特定的Java虚拟机参数，在options处指定即可。目前，Hotspot虚拟机支持大量的虚拟机参数，可以帮助开发人员进行系统调优和故障排查。相关的一些参数将在本书的后续章节中逐步展开介绍，本节则主要介绍参数的设置方法。

【示例2-1】以如下代码为例，读者先来了解一下如何设置参数。

package geym.zbase.ch2;

public class SimpleArgs {

public static void main(String[] args) {

for(int i=0;i<args.length;i++){

System.out.println("参数"+(i+1)+":"+args[i]);

}

System.out.println("-Xmx"+Runtime.getRuntime().maxMemory()/1000/1000+"M");

}

}

上述代码打印了传递给main()函数的参数，同时打印了系统的最大可用堆内存。使用如下命令行运行这段代码：

java  -Xmx32m geym.zbase.ch2.SimpleArgs a

参数1:a

-Xmx32M

从结果可以看到，第一个参数-Xmx32m传递给Java虚拟机，生效后，使得系统最大可用堆空间为32MB，参数a则传递给主函数main()，作为应用程序的参数。

有关-Xmx会在本书后续章节中展开讨论，除了-Xmx外，虚拟机还支持大量的调优诊断参数，其设置方式都是类似的，在本书后续章节中会逐步介绍这些参数。

如果读者使用Eclipse等开发工具运行程序，在运行对话框的参数选项卡上，也可以设置这两个参数，如图2.2所示，显示了“程序参数”和“虚拟机参数”两个文本框，将所需的参数填入即可。

图2.2　通过Eclipse为虚拟机设置启动参数

2.3　对象去哪儿：辨清Java堆

Java堆是和Java应用程序关系最为密切的内存空间，几乎所有的对象都存放在堆中。并且Java堆是完全自动化管理的，通过垃圾回收机制，垃圾对象会被自动清理，而不需要显式地释放。

根据垃圾回收机制的不同，Java堆有可能拥有不同的结构。最为常见的一种构成是将整个Java堆分为新生代和老年代。其中，新生代存放新生对象或者年龄不大的对象，老年代则存放老年对象。新生代有可能分为eden区、s0区、s1区，s0和s1也被称为from和to区域，它们是两块大小相等、可以互换角色的内存空间。详细信息可以参阅第4章。

图2.3显示了一个堆空间的一般结构。

图2.3　堆空间的一般结构

在绝大多数情况下，对象首先分配在eden区，在一次新生代回收后，如果对象还存活，则会进入s0或者s1，之后，每经过一次新生代回收，对象如果存活，它的年龄就会加1。当对象的年龄达到一定条件后，就会被认为是老年对象，从而进入老年代。

【示例2-2】下面通过一个简单的示例，来展示Java堆、方法区和Java栈之间的关系。

public class SimpleHeap {

private int id;

public SimpleHeap(int id){

this.id=id;

}

public void show(){

System.out.println("My ID is "+id);

}

public static void main(String[] args) {

SimpleHeap s1=new SimpleHeap(1);

SimpleHeap s2=new SimpleHeap(2);

s1.show();

s2.show();

}

}

上述代码声明了一个SimpleHeap类，并在main()函数中创建了两个SimpleHeap实例。此时，各对象和局部变量的存放如图2.4所示。SimpleHeap实例本身分配在堆中，描述SimpleHeap类的信息存放在方法区，main()函数中s1和s2局部变量存放在Java栈中，并指向堆中的两个实例。

免费样章到此结束。

喜欢这本书？

点击购买

或

前往Kindle商店查看图书详情。