Java假造机——JVM高级特性与最佳实践

打印 上一主题 下一主题

主题 886|帖子 886|积分 2658

深入理解  Java  假造机     ——JVM  高级特性与最佳实践      前言     第  2  版与第  1  版的区别     本书面向的读者     如何阅读本书     语言约定     内容特色     参考资料     勘误和支持     致谢     第一部分 走近  Java     第  1  章 走近  Java     1.1   概述     1.2 Java  技术体系     1.3 Java  发展史     1.4 Java  假造机发展史     1.4.1 Sun Classic/Exact VM     1.4.2 Sun HotSpot VM     1.4.3 Sun Mobile-Embedded VM/Meta-Circular VM     1.4.4 BEA JRockit/IBM J9 VM     1.4.5 Azul VM/BEA Liquid VM     1.4.6 Apache Harmony/Google Android Dalvik VM     1.4.7 Microsoft JVM  及其他     1.5   展望  Java  技术的未来     1.5.1   模块化     1.5.2   混合语言     1.5.3   多核并行     1.5.4   进一步丰富语法     1.5.5 64  位假造机     1.6   实战:本身编译  JDK     1.6.1   获取  JDK  源码     1.6.2   系统需求     1.6.3   构建编译环境     1.6.4   举行编译     1.6.5   在  IDE  工具中举行源码调试     1.7   本章小结     第二部分 自动内存管理机制     第  2  章   Java  内存区域与内存溢出异常     2.1   概述     2.2   运行时数据区域     2.2.1   程序计数器     2.2.2 Java  假造机栈     2.2.3   当地方法栈     2.2.4 Java  堆  2.2.5   方法区     2.2.6   运行时常量池     2.2.7   直接内存     2.3 HotSpot  假造机对象探秘     2.3.1   对象的创建     2.3.2   对象的内存布局     2.3.3   对象的访问定位     2.4   实战:  OutOfMemoryError  异常     2.4.1 Java  堆溢出     2.4.2   假造机栈和当地方法栈溢出     2.4.3   方法区和运行时常量池溢出     2.4.4   本机直接内存溢出     2.5   本章小结     第  3  章 垃圾收集器与内存分配计谋     3.1   概述     3.2   对象已死吗     3.2.1   引用计数算法     3.2.2   可达性分析算法     3.2.3   再谈引用     3.2.4   生存还是死亡     3.2.5   接纳方法区     3.3   垃圾收集算法     3.3.1   标记  -  清除算法     3.3.2   复制算法     3.3.3   标记  -  整理算法     3.3.4   分代收集算法     3.4 HotSpot  的算法实现     3.4.1   罗列根节点     3.4.2   安全点     3.4.3   安全区域     3.5   垃圾收集器     3.5.1 Serial  收集器     3.5.2 ParNew  收集器     3.5.3 Parallel Scavenge  收集器     3.5.4 Serial Old  收集器     3.5.5 Parallel Old  收集器     3.5.6 CMS  收集器     3.5.7 G1  收集器     3.5.8   理解  GC  日记     3.5.9   垃圾收集器参数总结     3.6   内存分配与接纳计谋     3.6.1   对象优先在  Eden  分配     3.6.2   大对象直接进入老年代     3.6.3   长期存活的对象将进入老年代     3.6.4   动态对象年龄判定  3.6.5   空间分配包管     3.7   本章小结     第  4  章 假造机性能监控与故障处理惩罚工具     4.1   概述     4.2 JDK  的命令行工具     4.2.1 jps  :假造机历程状况工具     4.2.2 jstat  :假造机统计信息监视工具     4.2.3 jinfo  :  Java  配置信息工具     4.2.4 jmap  :  Java  内存映像工具     4.2.5 jhat  :假造机堆转储快照分析工具     4.2.6 jstack  :  Java  堆栈跟踪工具     4.2.7 HSDIS  :  JIT  生成代码反汇编     4.3 JDK  的可视化工具     4.3.1 JConsole  :  Java  监视与管理控制台     4.3.2 VisualVM  :多合一故障处理惩罚工具     4.4   本章小结     第  5  章 调优案例分析与实战     5.1   概述     5.2   案例分析     5.2.1   高性能硬件上的程序部署计谋     5.2.2   集群间同步导致的内存溢出     5.2.3   堆外内存导致的溢堕落误     5.2.4   外部命令导致系统迟钝     5.2.5   服务器  JVM  历程瓦解     5.2.6   不适当数据结构导致内存占用过大     5.2.7   由  Windows  假造内存导致的长时间停顿     5.3   实战:  Eclipse  运行速率调优     5.3.1   调优前的程序运行状态     5.3.2   升级  JDK 1.6  的性能厘革及兼容标题     5.3.3   编译时间和类加载时间的优化     5.3.4   调解内存设置控制垃圾收集频率     5.3.5   选择收集器低落延迟     5.4   本章小结     第三部分 假造机执行子系统     第  6  章 类文件结构     6.1   概述     6.2   无关性的基石     6.3 Class  类文件的结构     6.3.1   魔数与  Class  文件的版本     6.3.2   常量池     6.3.3   访问标志     6.3.4   类索引、父类索引与接口索引聚集     6.3.5   字段表聚集     6.3.6   方法表聚集     6.3.7   属性表聚集  6.4   字节码指令简介     6.4.1   字节码与数据范例     6.4.2   加载和存储指令     6.4.3   运算指令     6.4.4   范例转换指令     6.4.5   对象创建与访问指令     6.4.6   操作数栈管理指令     6.4.7   控制转移指令     6.4.8   方法调用和返回指令     6.4.9   异常处理惩罚指令     6.4.10   同步指令     6.5   公有设计和私有实现     6.6 Class  文件结构的发展     6.7   本章小结     第  7  章 假造机类加载机制     7.1   概述     7.2   类加载的时机     7.3   类加载的过程     7.3.1   加载     7.3.2   验证     7.3.3   准备     7.3.4   剖析     7.3.5   初始化     7.4   类加载器     7.4.1   类与类加载器     7.4.2   双亲委派模型     7.4.3   破坏双亲委派模型     7.5   本章小结     第  8  章 假造机字节码执行引擎     8.1   概述     8.2   运行时栈帧结构     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.4   基于栈的字节码解释执行引擎     8.4.1   解释执行     8.4.2   基于栈的指令集与基于寄存器的指令集     8.4.3   基于栈的解释器执行过程     8.5   本章小结  第  9  章 类加载及执行子系统的案例与实战     9.1   概述     9.2   案例分析     9.2.1 Tomcat  :正统的类加载器架构     9.2.2 OSGi  :灵活的类加载器架构     9.2.3   字节码生成技术与动态署理的实现     9.2.4 Retrotranslator  :超过  JDK  版本     9.3   实战:本身动手实现远程执行功能     9.3.1   目标     9.3.2   思绪     9.3.3   实现     9.3.4   验证     9.4   本章小结     第四部分 程序编译与代码优化     第  10  章 早期(编译期)优化     10.1   概述     10.2 Javac  编译器     10.2.1 Javac  的源码与调试     10.2.2   剖析与添补符号表     10.2.3   注解处理惩罚器     10.2.4   语义分析与字节码生成     10.3 Java  语法糖的味道     10.3.1   泛型与范例擦除     10.3.2   自动装箱、拆箱与遍历循环     10.3.3   条件编译     10.4   实战:插入式注解处理惩罚器     10.4.1   实战目标     10.4.2   代码实现     10.4.3   运行与测试     10.4.4   其他应用案例     10.5   本章小结     第  11  章 晚期(运行期)优化     11.1   概述     11.2 HotSpot  假造机内的即时编译器     11.2.1   解释器与编译器     11.2.2   编译对象与触发条件     11.2.3   编译过程     11.2.4   检察及分析即时编译结果     11.3   编译优化技术     11.3.1   优化技术概览     11.3.2   公共子表达式消除     11.3.3   数组边界查抄消除     11.3.4   方法内联     11.3.5   逃逸分析     11.4 Java  与  C/C++  的编译器对比  11.5   本章小结     第五部分 高效并发     第  12  章   Java  内存模型与线程     12.1   概述     12.2   硬件的服从与同等性     12.3 Java  内存模型     12.3.1   主内存与工作内存     12.3.2   内存间交互操作     12.3.3   对于  volatile  型变量的特殊规则     12.3.4   对于  long  和  double  型变量的特殊规则     12.3.5   原子性、可见性与有序性     12.3.6   先行发生原则     12.4 Java  与线程     12.4.1   线程的实现     12.4.2 Java  线程调度     12.4.3   状态转换     12.5   本章小结     第  13  章 线程安全与锁优化     13.1   概述     13.2   线程安全     13.2.1 Java  语言中的线程安全     13.2.2   线程安全的实现方法     13.3   锁优化     13.3.1   自旋锁与自适应自旋     13.3.2   锁消除     13.3.3   锁粗化     13.3.4   轻量级锁     13.3.5   偏向锁     13.4   本章小结     附录     附录  A   编译  Windows  版的  OpenJDK     A.1   获取  JDK  源码     A.2   系统需求     A.3   构建编译环境     A.4   准备依赖项     A.5   举行编译     附录  B   假造机字节码指令表     附录  C HotSpot  假造机重要参数表     C.1   内存管理参数     C.2   即时编译参数     C.3   范例加载参数     C.4   多线程相干参数     C.5   性能参数     C.6   调试参数     附录  D   对象查询语言(  OQL  )简介  D.1 SELECT  子句     D.2 FROM  子句     D.3 WHERE  子句     D.4   属性访问器     D.5 OQL  语言的  BNF  范式     附录  E JDK  历史版本轨迹  前言     Java  是目前用户最多、利用范围最广的软件开发技术之一。  Java  的技术体系重要由支持     Java  程序运行的假造机、提供各开发领域接口支持的  Java     API  、  Java  编程语言及很多第三方     Java  框架(如  Spring  、  Struts  等)构成。在国内,有关  Java API  、  Java  语言语法及第三方框架的     技术资料和书籍非常丰富,相比之下,有关  Java  假造机的资料却显得异常缺少。     这种状况在很大水平上是由  Java  开发技术本身的一个重要优点导致的:在假造机层面隐     藏了底层技术的复杂性以及呆板与操作系统的差别性。运行程序的物理呆板的环境千差万     别,而  Java  假造机则在千差万别的物理机上建立了统一的运行平台,实现了在恣意一台假造     机上编译的程序都能在任何一台假造机上正常运行。这一极大优势使得  Java  应用的开发比传     统  C/C++  应用的开发更高效和快捷,程序员可以把重要精力会合在具体业务逻辑上,而不是     物理硬件的兼容性上。在一般环境下,一个程序员只要了解了须要的  Java API  、  Java  语法,     以及学习适当的第三方开发框架,就已经基本能满足日常开发的须要了,假造机会在用户不     知不觉中完成对硬件平台的兼容及对内存等资源的管理工作。因此,了解假造机的运作并不     是一般开发人员必须掌握的知识。     然而,凡事都具备两面性。随着  Java  技术的不断发展,它被应用于越来越多的领域之     中。此中一些领域,如电力、金融、通讯等,对程序的性能、稳定性和可扩展性方面都有极     高的要求。程序很可能在  10  个人同时利用时完全正常,但是在  10000  个人同时利用时就会缓     慢、死锁,乃至瓦解。毫无疑问,要满足  10000  个人同时利用须要更高性能的物理硬件,但     是在绝大多数环境下,提升硬件效能无法等比例地提升程序的运作性能和并发本领,乃至可     能对程序运作状况完全没有任何改善。这里面有  Java  假造机的缘故原由:为了到达给所有硬件提     供同等的假造平台的目标,牺牲了一些与硬件相干的性能特性。更重要的是人为缘故原由:假如     开发人员不了解假造机一些技术特性的运行原理,就无法写出最适合假造机运行和自优化的     代码。     实在,目前商用的高性能  Java  假造机都提供了相称多的优化特性和调节本领,用于满足     应用程序在实际生产环境中对性能和稳定性的要求。假如只是为了入门学习,让程序在本身     的呆板上正常运行,那么这些特性可以说是可有可无的;假如用于生产开发,尤其是企业级     生产开发,就急迫须要开发人员中至少有一部分人对假造机的特性及调节方法具有很清晰的     认识,以是在  Java  开发体系中,对架构师、系统调优师、高级程序员等角色的需求一直都非     常大。学习假造机中各种自动运作特性的原理也成为了  Java  程序员成长门路上必然会接触到     的一课。本书可以使读者以一种相对轻松的方式学习假造机的运作原理,对  Java  程序员的成     长也有较大的资助。  第  2  版与第  1  版的区别     JDK 1.7  在  2011  年  7  月  28  日正式发布,相对于  2006  年发布的  JDK 1.6  ,新版的  JDK  有了很多     新的特性和改进。本书的第  2  版也相应地举行了修改和升级,把讲解的技术平台从  JDK 1.6  提     升至  JDK     1.7  。比方,增长了对  JDK     1.7  中最新的  G1  收集器,以及  JDK     1.7  中  JSR-292     InvokeDynamic  (对非  Java  语言的调用支持)的分析讲解等内容。     在第  1  版出版后,笔者收到了很多热心读者的反馈意见,部分读者提出  OpenJDK  开源已     久,第  1  版却很少有直接分析  OpenJDK  源码的内容,有点  “  视宝山而不见  ”  的感觉。因此,在     本书第  2  版中,笔者特殊加强了对这部分内容的讲解,此中在第  1  章中就介绍了如何分析、调     试  OpenJDK  源码等。在本书后续章节中,不少关于功能点的讲解都直接利用  OpenJDK  中的     HotSpot  源码大概  JIT  编译器生成的当地代码作为论据。     如何把  Java  假造机原理中很多理论性很强的知识、特性应用于实践开发,是本书贯穿始     终的主旨。由于笔者盼望在本书第  2  版中进一步加强知识的实践性,因此增长了很多对处理惩罚     JVM  常见标题技能的讲解,包罗如何分析  GC  日记、如何分析  JIT  编译器代码优化过程和生成     代码等。并且,在第  1  版的基础上,第  2  版中进一步增长了若干处理惩罚  JVM  标题标实践案例供读     者参考。     另外,本书第  2  版还修正了第  1  版中多处错误的、有歧义的和不完备的描述。有关勘误信     息,可以参考第  1  版的勘误页面(  http://icyfenix.iteye.com/blog/1119214  )。  本书面向的读者     (  1  )利用  Java  技术体系的中、高级开发人员     Java  假造机作为中、高级开发人员必须修炼的知识,有着较高的学习门槛,本书可作为     学习假造机的良好教材。     (  2  )系统调优师     系统调优师是近几年才鼓起的职业,本书中的大量案例、代码和调优实战将会对系统调     优师的日常工作有直接的资助。     (  3  )系统架构师     保障系统的性能、并发和伸缩等本领是系统架构师的重要职责之一,而这部分与假造机     的运作密不可分,本书可以作为他们制定应用系统底层框架的参考资料。  如何阅读本书     本书一共分为五个部分:走近  Java  、自动内存管理机制、假造机执行子系统、程序编译     与代码优化、高效并发。各部分基本上是互相独立的,没有必然的前后依赖关系,读者可以     从任何一个感爱好的专题开始阅读,但是每个部分中的各个章节间有先后顺序。     本书并没有假设读者在  Java  领域具备很专业的技术水平,因此在包管逻辑正确的条件     下,只管用通俗的语言和案例讲述假造机中与开发的关系最为密切的内容。当然,学习假造     机技术本身就须要读者有一定的基础,且本书的读者定位是中、高级程序员,因此本书假设     读者本身了解一些常用的开发框架、  Java API  和  Java  语法等基础知识。     笔者盼望读者在阅读本书的同时,把本书中的实践内容亲自验证一遍,此中用到的代码     清单可以从华章网站(  http://www.hzbook.com  )下载。     语言约定     本书在语言和技术上有如下约定:     本书中提到  HotSpot  、  JRockit  假造机、  WebLogic  服务器等产物的所有者时,仍然利用  Sun     和  BEA  公司的名称,实际上,  BEA  和  Sun  分别于  2008  年和  2009  年被  Oracle  公司收购,现在已经     不存在这两个商标了,但毫无疑问的是,它们都是在  Java  领域中做出过卓越贡献的、值得程     序员纪念的公司。     JDK  从  1.5  版本开始,在官方的正式文档与宣传资料中已经不再利用雷同  “JDK 1.5”  的名     称,只有程序员内部利用的开发版本号(  Developer Version  ,比方  java-version  的输出)才继     续相沿  1.5  、  1.6  和  1.7  的版本号,而公开版本号(  Product Version  )则改为  JDK 5  、  JDK 6  和  JDK     7  的定名方式,为了行文同等,本书所有场所统一采用开发版本号的定名方式。     由于版面关系,本书中的很多示例代码都没有遵照最优的代码编写风格,如利用的流没     有关闭流等,请读者在阅读时注意这一点。     假如没有特殊说明,本书中所有讨论都是以  Sun JDK 1.7  为技术平台的。不外假如有某个     特性在各个版本间的厘革较大,一般都会说明它在各个版本间的差别。  内容特色     第一部分 走近  Java     本书的第一部分为后文的讲解建立了良好的基础。只管了解  Java  技术的来龙去脉,以及     编译本身的  OpenJDK  对于读者理解  Java  假造机并不是必需的,但是这些准备过程可以为走近     Java  技术和  Java  假造机提供很好的引导。第一部分只有第  1  章:     第  1  章 介绍了  Java  技术体系的过去、现在和未来的一些发展趋势,并介绍了如何独立     地编译一个  OpenJDK 7  。     第二部分 自动内存管理机制     由于程序员把内存控制的权力交给了  Java  假造机,以是可以在编码的时候享受自动内存     管理的诸多优势,不外也正是这个缘故原由,一旦出现内存泄漏和溢出方面的标题,假如不了解     假造机是怎样利用内存的,那么排查错误将会成为一项异常艰难的工作。第二部分包罗第  2     ~  5  章:     第  2  章 讲解了假造机中内存是如何划分的,以及哪部分区域、什么样的代码和操作可     能导致内存溢出异常,并讲解了各个区域出现内存溢出异常的常见缘故原由。     第  3  章 分析了垃圾收集的算法和  JDK 1.7  中提供的几款垃圾收集器的特点及运作原理。     通过代码实例验证了  Java  假造机中自动内存分配及接纳的重要规则。     第  4  章 介绍了随  JDK  发布的  6  个命令行工具与两个可视化的故障处理惩罚工具的利用方法。     第  5  章 与读者分享了几个比较有代表性的实际案例,还准备了一个所有开发人员都     能  “  切身实战  ”  的练习,读者可通过实践来获得故障处理惩罚和调优的履历。     第三部分 假造机执行子系统     执行子系统是假造机中必不可少的组成部分,了解了假造机如何执行程序,才能写出更     良好的代码。第三部分包罗第  6  ~  9  章:     第  6  章 讲解了  Class  文件结构中的各个组成部分,以及每个部分的定义、数据结构和使     用方法,以实战的方式演示了  Class  文件的数据是如何存储和访问的。     第  7  章 介绍了类加载过程的  “  加载  ”  、  “  验证  ”  、  “  准备  ”  、  “  剖析  ”  和  “  初始化  ”5  个阶段中虚     拟机分别执行了哪些动作,还介绍了类加载器的工作原理及其对假造机的意义。     第  8  章 分析了假造机在执行代码时如何找到正确的方法,如何执行方法内的字节码,     以及执行代码时涉及的内存结构。     第  9  章 通过  4  个类加载及执行子系统的案例,分享了利用类加载器和处理惩罚字节码的一些     值得欣赏和鉴戒的思绪,并通过一个实战练习来加深对前面理论知识的理解。     第四部分 程序编译与代码优化  Java  程序从源码编译成字节码和从字节码编译成当地呆板码的这两个过程,合并起来其     实就等同于一个传统编译器所执行的编译过程。第四部分包罗第  10  ~  11  章:     第  10  章 分析了  Java  语言中泛型、主动装箱和拆箱、条件编译等多种语法糖的前因后     果,并通过实战演示了如何利用插入式注解处理惩罚器来实现一个查抄程序定名规范的编译器插     件。     第  11  章 讲解了假造机的热门探测方法、  HotSpot  的即时编译器、编译触发条件,以及     如何从假造机外部观察和分析  JIT  编译的数据和结果,此外,还讲解了几种常见的编译优化     技术。     第五部分 高效并发     Java  语言和假造机提供了原生的、美满的多线程支持,这使得它天生就适合开发多线程     并发的应用程序。不外我们不能期望系统来完成所有并发相干的处理惩罚,了解并发的内幕也是     成为一个高级程序员不可缺少的课程。第五部分包罗第  12  ~  13  章:     第  12  章 讲解了假造机  Java  内存模型的结构及操作,以及原子性、可见性和有序性在     Java  内存模型中的体现,介绍了先行发生原则的规则及利用,还了解了线程在  Java  语言中是     如何实现的。     第  13  章 介绍了线程安全涉及的概念和分类、同步实现的方式及假造机的底层运作原     理,并且介绍了假造机实现高效并发所采取的一系列锁优化措施。  参考资料     本书名为  “  深入理解  Java  假造机  ”  ,但要想深入理解假造机,仅凭一本书肯定是远远不够     的,读者可以通过以下信息找到更多关于  Java  假造机方面的资料。我在写作此书的时候,也     从下面这些参考资料中获得了很大的资助。     (  1  )书籍     《  The Java Virtual Machine Specification,Java SE 7 Edition  》    [1]     要学习假造机,无论如何都必须掌握  “Java  假造机规范  ”  。这本书的概念和细节描述与  Sun     的早期假造机(  Sun Classic VM  )高度吻合,不外,随着技术的发展,高性能假造机真正的     细节实现方式已经徐徐与假造机规范所描述的差距越来越大,假如只能选择一本参考书来了     解假造机,那我推荐这本书。此书的  Java SE 7  版在  2011  年  7  月出版发行,这是自  1999  年发布     的《  Java  假造机规范(第  2  版)》以来的第一次版本更新。笔者对  Java SE 7  版的全文举行了     翻译,并与原书一样在网上免费发布了全文  PDF     [2]    。     《  The Java Language Specification,Java SE 7 Edition  》    [3]     虽然假造机并不是  Java  语言专有的,但是了解  Java  语言的各种细节规定对理解假造机的     行为也是很有资助的,它与上一本《  Java  假造机规范》都是  Sun  官方出品的书籍,而且这本     书还是由  Java  之父  James Gosling  亲自执笔撰写的。这本书也与《  Java  假造机规范》一样,可     以在官方网站完全免费下载到全文  PDF  ,但暂时没有中文译本,《  Java  语言规范(第  3  版)》     于  2005  年  7  月由机械工业出版社引进出版。     《  Oracle JRockit The Definitive Guide  》     《  Oracle     JRockit  权威指南》,  2010  年  7  月出版,国内也没有(可能是尚未)引进这本     书,它是由  JRockit  的两位资深开发人员(此中一位还是  JRockit     Mission     Control  团队的     TeamLeader  )撰写的  JRockit  假造机高级利用指南。虽然  JRockit  的用户量可能不如  HotSpot  多,     但也是目前最流行的三大贸易假造机之一,并且不同假造机中的很多实现思绪都是可以对比     参照的。这本书是了解当代高性能假造机很好的参考资料。     《  Inside the Java 2 Virtual Machine,Second Edition  》     《深入  Java  假造机(第  2  版)》,  2000  年  1  月出版,  2003  年由机械工业出版社出版此中文     译本。在相称长的时间里,这本书是唯一的一本关于  Java  假造机的中文图书。     《  Java Performance  》     《  Java     Performance  》是  “The     Java”  系列(很多人都读过该系列中最出名的《  Effective     Java  》)图书中最新的一本,  2011  年  10  月出版,暂时没有中文版。这本书并非全部都围绕     Java  假造机(只有第  3  、  4  、  7  章直接与  Java  假造机相干),而是从操作系统到基于  Java  的上层     程序性能度量和调优的全面介绍,此中涉及  Java  假造机的内容具备一定的深度和可实践性。     (  2  )网站资源  高级语言假造机圈子:  http://hllvm.group.iteye.com/     里面有一些国内关于假造机的讨论,并不只限于  JVM  ,而是涉及对所有的高级语言假造     机(  High-Level Language Virtual Machine  )的讨论,但该网站建立在  ITEye  上,自然还是以讨     论  Java  假造机为主。圈主  RednaxelaFX  (莫枢)的博客(  http://rednaxelafx.iteye.com/  )是另外     一个非常有价值的假造机及编译原理等资料的分享园地。     HotSpot Internals  :  https://wikis.oracle.com/display/HotSpotInternals/Home     一个关于  OpenJDK  的  Wiki  网站,很多文章都由  JDK  的开发团队编写,更新较慢,但是仍     然有很高的参考价值。     The HotSpot Group  :  http://openjdk.java.net/groups/hotspot/     HotSpot  组群,包罗假造机开发、编译器、垃圾收集和运行时  4  个邮件组,此中有关于     HotSpot  假造机的最新讨论。     [1]  官方所在:  http://docs.oracle.com/javase/specs/jvms/se7/jvms7.pdf  。     [2]  官方所在:  http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf  。     [3]  中文译当所在:  http://icyfenix.iteye.com/blog/1256329  。  勘误和支持     在本书交稿的时候,我并不像想象中的那样高兴或放松,写作之时那种  “  小心翼翼、如     履薄冰  ”  的感觉依然萦绕在心头。在每一章、每一节落笔之时,我都在考虑如何才能把各个     知识点更有条理地讲述出来,同时也在担心会不会由于本身理解有毛病而误导了读者。由于     写作水平和写作时间所限,书中难免存在不妥之处,以是特地开通了一个读者邮箱     (  understandingjvm@gmail.com  )与各人交流,各人如有任何意见或建议接待与我接洽。相信     写书与写程序一样,作品一定都是不完美的,由于不完美,我们才有不断寻求完美的动力。     本书第  2  版的勘误,将会在作者的博客(  http://icyfenix.iteye.com/  )中发布。接待读者在     博客上留言。  致谢     首先要感谢我的家人,在本誊写作期间端赖他们对我的悉心照顾,才让我可以或许满身心地     投入到写作之中,而无后顾之忧。     同时要感谢我的工作单位远光软件,公司为我提供了宝贵的工作、学习和实践的环境,     书中的很多知识点都来自于工作中的实践;也感谢与我一起工作的同事们,非常荣幸能与你     们一起在这个富有豪情的团队中共同奋斗。     还要感谢  Oracle  公司假造机团队的莫枢,在百忙之中抽空审阅了本书,提出了很多宝贵     的建议和意见。     最后,感谢机械工业出版社华章公司的编辑,本书可以或许顺利出版离不开他们的敬业精力     和一丝不苟的工作态度。     周志明  第一部分 走近  Java     第  1  章 走近  Java  第  1  章 走近  Java     天下上并没有完美的程序,但我们并不因此而沮丧,由于写程序原来就是一个不断寻求     完美的过程。     1.1   概述     Java  不仅仅是一门编程语言,还是一个由一系列计算机软件和规范形成的技术体系,这     个技术体系提供了完备的用于软件开发和跨平台部署的支持环境,并广泛应用于嵌入式系     统、移动终端、企业服务器、大型机等各种场所,如图  1-1  所示。时至本日,  Java  技术体系已     经吸引了  900  多万软件开发者,这是全球最大的软件开发团队。利用  Java  的装备多达几十亿     台,此中包罗  11  亿多台个人计算机、  30  亿部移动电话及其他手持装备、数量众多的智能卡,     以及大量机顶盒、导航系统和其他装备    [1]    。     图   1-1 Java  技术的广泛应用     Java  能获得如此广泛的认可,除了它拥有一门结构严谨、面向对象的编程语言之外,还     有很多不可忽视的优点:它摆脱了硬件平台的束缚,实现了  “  一次编写,到处运行  ”  的理想;     它提供了一个相对安全的内存管理和访问机制,制止了绝大部分的内存泄露和指针越界问     题;它实现了热门代码检测和运行时编译及优化,这使得  Java  应用能随着运行时间的增长而     获得更高的性能;它有一套美满的应用程序接口,尚有无数来自贸易机构和开源社区的第三     方类库来资助它实现各种各样的功能  ……Java  所带来的这些好处使程序的开发服从得到了很     大的提升。作为一名  Java  程序员,在编写程序时除了恣意发挥  Java  的各种优势外,还应该去     了解和思索一下  Java  技术体系中这些技术特性是如何实现的。认识这些技术运作的本质,是     本身思索  “  程序这样写好欠好  ”  的基础和条件。当我们在利用一种技术时,假如不再依赖书籍  和他人就能得到这些标题标答案,那才算上升到了  “  不惑  ”  的境界。     本书将与读者一起分析  Java  技术中最重要的那些特性的实现原理。在本章中,我们将重     点介绍  Java  技术体系内容以及  Java  的历史、现在和未来的发展趋势。     [1]  这些数据是  Java  的广告词,它们来源于:  http://www.java.com/zh_CN/about/  。  1.2 Java  技术体系     从广义上讲,  Clojure  、  JRuby  、  Groovy  等运行于  Java  假造机上的语言及其相干的程序都     属于  Java  技术体系中的一员。假如仅从传统意义上来看,  Sun  官方所定义的  Java  技术体系包罗     以下几个组成部分:     Java  程序设计语言     各种硬件平台上的  Java  假造机     Class  文件格式     Java API  类库     来自贸易机构和开源社区的第三方  Java  类库     我们可以把  Java  程序设计语言、  Java  假造机、  Java     API  类库这三部分统称为  JDK  (  Java     Development Kit  ),  JDK  是用于支持  Java  程序开发的最小环境,在后面的内容中,为了讲解     方便,有一些地方会以  JDK  来代替整个  Java  技术体系。另外,可以把  Java     API  类库中的  Java     SE API  子集    [1]    和  Java  假造机这两部分统称为  JRE  (  Java Runtime Environment  ),  JRE  是支持  Java     程序运行的标准环境。图  1-2  展示了  Java  技术体系所包罗的内容,以及  JDK  和  JRE  所涵盖的范     围。  图   1-2 Java  技术体系所包罗的内容    [2]     以上是根据各个组成部分的功能来举行划分的,假如按照技术所服务的领域来划分,或     者说按照  Java  技术关注的重点业务领域来划分,  Java  技术体系可以分为  4  个平台,分别为:     Java Card  :支持一些  Java  小程序(  Applets  )运行在小内存装备(如智能卡)上的平台。     Java ME  (  Micro Edition  ):支持  Java  程序运行在移动终端(手机、  PDA  )上的平台,对     Java API  有所精简,并到场了针对移动终端的支持,这个版本以前称为  J2ME  。     Java SE  (  Standard Edition  ):支持面向桌面级应用(如  Windows  下的应用程序)的  Java     平台,提供了完备的  Java  核心  API  ,这个版本以前称为  J2SE  。     Java EE  (  Enterprise Edition  ):支持利用多层架构的企业应用(如  ERP  、  CRM  应用)的     Java  平台,除了提供  Java SE API  外,还对其做了大量的扩充    [3]    并提供了相干的部署支持,这     个版本以前称为  J2EE  。     [1]  JDK 1.7  的  Java SE API  范围:  http://download.oracle.com/javase/7/docs/api/  。     [2]  图片来源:  http://download.oracle.com/javase/7/docs/  。     [3]  这些扩展一般以  javax.*  作为包名,而以  java.*  为包名的包都是  Java SE API  的核心包,但由     于历史缘故原由,一部分曾经是扩展包的  API  后来进入了核心包,因此核心包中也包罗了不少     javax.*  的包名。  1.3 Java  发展史     从第一个  Java  版本诞生到现在已经有  18  年的时间了。沧海桑田一刹时,转眼  18  年过去     了,在图  1-3  所展示的时间线中,我们看到  JDK  已经发展到了  1.7  版。在这  18  年里还诞生了无     数和  Java  相干的产物、技术和标准。现在让我们走入时间隧道,从孕育  Java  语言的期间开     始,再来回首一下  Java  的发展轨迹和历史变迁。     图   1-3 Java  技术发展的时间线     1991  年  4  月,由  James Gosling  博士领导的绿色计划(  Green Project  )开始启动,此计划的     目标是开发一种可以或许在各种消耗性电子产物(如机顶盒、冰箱、收音机等)上运行的程序架     构。这个计划的产物就是  Java  语言的前身:  Oak  (橡树)。  Oak  当时在消耗品市场上并不算成     功,但随着  1995  年互联网潮水的鼓起,  Oak  敏捷找到了最适合本身发展的市场定位并蜕变成     为  Java  语言。     1995  年  5  月  23  日,  Oak  语言改名为  Java  ,并且在  SunWorld  大会上正式发布  Java 1.0  版本。     Java  语言第一次提出了  “Write Once,Run Anywhere”  的口号。     1996  年  1  月  23  日,  JDK 1.0  发布,  Java  语言有了第一个正式版本的运行环境。  JDK 1.0  提供     了一个纯解释执行的  Java  假造机实现(  Sun Classic VM  )。  JDK 1.0  版本的代表技术包罗:     Java  假造机、  Applet  、  AWT  等。     1996  年  4  月,  10  个最重要的操作系统供应商申明将在其产物中嵌入  Java  技术。同年  9  月,     已有约莫  8.3  万个网页应用了  Java  技术来制作。在  1996  年  5  月尾,  Sun  公司于美国旧金山举行了     首届  JavaOne  大会,从此  JavaOne  成为全天下数百万  Java  语言开发者每年一度的技术盛会。     1997  年  2  月  19  日,  Sun  公司发布了  JDK     1.1  ,  Java  技术的一些最基础的支持点(如  JDBC     等)都是在  JDK     1.1  版本中发布的,  JDK     1.1  版的技术代表有:  JAR  文件格式、  JDBC  、     JavaBeans  、  RMI  。  Java  语法也有了一定的发展,如内部类(  Inner     Class  )和反射     (  Reflection  )都是在这个时候出现的。  直到  1999  年  4  月  8  日,  JDK 1.1  一共发布了  1.1.0  ~  1.1.8  九个版本。从  1.1.4  之后,每个  JDK  版     本都有一个本身的名字(工程代号),分别为:  JDK 1.1.4-Sparkler  (宝石)、  JDK 1.1.5-     Pumpkin  (南瓜)、  JDK 1.1.6-Abigail  (阿比盖尔,女子名)、  JDK 1.1.7-Brutus  (布鲁图,古     罗马政治家和将军)和  JDK 1.1.8-Chelsea  (切尔西,城市名)。     1998  年  12  月  4  日,  JDK  迎来了一个里程碑式的版本  JDK 1.2  ,工程代号为  Playground  (竞技     场),  Sun  在这个版本中把  Java  技术体系拆分为  3  个方向,分别是面向桌面应用开发的     J2SE  (  Java 2 Platform,Standard Edition  )、面向企业级开发的  J2EE  (  Java 2 Platform,Enterprise     Edition  )和面向手机等移动终端开发的  J2ME  (  Java 2 Platform,Micro Edition  )。在这个版本     中出现的代表性技术非常多,如  EJB  、  Java Plug-in  、  Java IDL  、  Swing  等,并且这个版本中     Java  假造机第一次内置了  JIT  (  Just In Time  )编译器(  JDK 1.2  中曾并存过  3  个假造机,  Classic     VM  、  HotSpot VM  和  Exact VM  ,此中  Exact VM  只在  Solaris  平台出现过;后面两个假造机都是     内置  JIT  编译器的,而之前版本所带的  Classic VM  只能以外挂的形式利用  JIT  编译器)。在语     言和  API  级别上,  Java  添加了  strictfp  关键字与现在  Java  编码之中极为常用的一系列  Collections     聚集类。在  1999  年  3  月和  7  月,分别有  JDK 1.2.1  和  JDK 1.2.2  两个小版本发布。     1999  年  4  月  27  日,  HotSpot  假造机发布,  HotSpot  最初由一家名为  “Longview     Technologies”  的小公司开发,由于  HotSpot  的优异体现,这家公司在  1997  年被  Sun  公司收购     了。  HotSpot  假造机发布时是作为  JDK 1.2  的附加程序提供的,后来它成为了  JDK 1.3  及之后所     有版本的  Sun JDK  的默认假造机。     2000  年  5  月  8  日,工程代号为  Kestrel  (美洲红隼)的  JDK 1.3  发布,  JDK 1.3  相对于  JDK 1.2     的改进重要体现在一些类库上(如数学运算和新的  Timer API  等),  JNDI  服务从  JDK 1.3  开始     被作为一项平台级服务提供(以前  JNDI  仅仅是一项扩展),利用  CORBA IIOP  来实现  RMI  的     通讯协议,等等。这个版本还对  Java 2D  做了很多改进,提供了大量新的  Java 2D API  ,并且     新添加了  JavaSound  类库。  JDK 1.3  有  1  个修正版本  JDK 1.3.1  ,工程代号为  Ladybird  (瓢虫),     于  2001  年  5  月  17  日发布。     自从  JDK 1.3  开始,  Sun  维持了一个风俗:约莫每隔两年发布一个  JDK  的主版本,以动物     定名,期间发布的各个修正版本则以昆虫作为工程名称。     2002  年  2  月  13  日,  JDK 1.4  发布,工程代号为  Merlin  (灰背隼)。  JDK 1.4  是  Java  真正走向     成熟的一个版本,  Compaq  、  Fujitsu  、  SAS  、  Symbian  、  IBM  等著名公司都有到场乃至实现本身     独立的  JDK 1.4  。哪怕是在十多年后的今天,仍然有很多主流应用(  Spring  、  Hibernate  、  Struts     等)能直接运行在  JDK 1.4  之上,大概继承发布能运行在  JDK 1.4  上的版本。  JDK 1.4  同样发布     了很多新的技术特性,如正则表达式、异常链、  NIO  、日记类、  XML  剖析器和  XSLT  转换器     等。  JDK     1.4  有两个后续修正版:  2002  年  9  月  16  日发布的工程代号为  Grasshopper  (蚱蜢)的     JDK 1.4.1  与  2003  年  6  月  26  日发布的工程代号为  Mantis  (螳螂)的  JDK 1.4.2  。     2002  年前后还发生了一件与  Java  没有直接关系,但事实上对  Java  的发展历程影响很大的     事件,那就是微软公司的  .NET Framework  发布了。这个无论是技术实现上还是目标用户上都     与  Java  有很多相近之处的技术平台给  Java  带来了很多讨论、比较和竞争,  .NET  平台和  Java  平     台之间声势浩大的孰优孰劣的论战到目前为止都在继承。     2004  年  9  月  30  日,  JDK 1.5     [1]    发布,工程代号  Tiger  (老虎)。从  JDK 1.2  以来,  Java  在语法     层面上的变换一直很小,而  JDK 1.5  在  Java  语法易用性上做出了非常大的改进。比方,自动装  箱、泛型、动态注解、罗列、可变长参数、遍历循环(  foreach  循环)等语法特性都是在  JDK     1.5  中到场的。在假造机和  API  层面上,这个版本改进了  Java  的内存模型(  Java     Memory     Model,JMM  )、提供了  java.util.concurrent  并发包等。另外,  JDK     1.5  是官方声明可以支持     Windows 9x  平台的最后一个  JDK  版本。     2006  年  12  月  11  日,  JDK 1.6  发布,工程代号  Mustang  (野马)。在这个版本中,  Sun  终结了     从  JDK 1.2  开始已经有  8  年历史的  J2EE  、  J2SE  、  J2ME  的定名方式,启用  Java SE 6  、  Java EE     6  、  Java     ME     6  的定名方式。  JDK     1.6  的改进包罗:提供动态语言支持(通过内置  Mozilla     JavaScript Rhino  引擎实现)、提供编译  API  和微型  HTTP  服务器  API  等。同时,这个版本对  Java     假造机内部做了大量改进,包罗锁与同步、垃圾收集、类加载等方面的算法都有相称多的改     动。     在  2006  年  11  月  13  日的  JavaOne  大会上,  Sun  公司公布最终会将  Java  开源,并在随后的一年     多时间内,连续将  JDK  的各个部分在  GPL v2  (  GNU General Public License v2  )协议下公开了     源码,并建立了  OpenJDK  构造对这些源码举行独立管理。除了少少量的产权代码     (  Encumbered Code  ,这部分代码大多是  Sun  本身也无权限举行开源处理惩罚的)外,  OpenJDK  几     乎包罗了  Sun JDK  的全部代码,  OpenJDK  的质量主管曾经体现,在  JDK 1.7  中,  Sun JDK  和     OpenJDK  除了代码文件头的版权注释之外,代码基本上完全一样,以是  OpenJDK 7  与  Sun JDK     1.7  本质上就是同一套代码库开发的产物。     JDK 1.6  发布以后,由于代码复杂性的增长、  JDK  开源、开发  JavaFX  、经济危急及  Sun  收     购案等缘故原由,  Sun  在  JDK  发展以外的事情上耗费了很多资源,  JDK  的更新没有再维持两年发布     一个主版本的发展速率。  JDK 1.6  到目前为止一共发布了  37  个  Update  版本,最新的版本为  Java     SE 6 Update 37  ,于  2012  年  10  月  16  日发布。     2009  年  2  月  19  日,工程代号为  Dolphin  (海豚)的  JDK 1.7  完成了其第一个里程碑版本。根     据  JDK 1.7  的功能规划,一共设置了  10  个里程碑。最后一个里程碑版本原计划于  2010  年  9  月  9     日结束,但由于各种缘故原由,  JDK 1.7  最终无法按计划完成。     从  JDK 1.7  最开始的功能规划来看,它本应是一个包罗很多重要改进的  JDK  版本,此中的     Lambda  项目(  Lambda  表达式、函数式编程)、  Jigsaw  项目(假造机模块化支持)、动态语言     支持、  GarbageFirst  收集器和  Coin  项目(语言细节进化)等子项目对于  Java  业界都会产生深远     的影响。在  JDK 1.7  开发期间,  Sun  公司由于相继在技术竞争和贸易竞争中都陷入泥潭,公司     的股票市值跌至仅有高峰时期的  3%  ,已无力推动  JDK 1.7  的研发工作按正常计划举行。为了     尽快结束  JDK 1.7  长期  “  跳票  ”  的标题,  Oracle  公司收购  Sun  公司后不久便公布将实验  “B  计划  ”  ,     大幅裁剪了  JDK 1.7  预定目标,以便包管  JDK 1.7  的正式版可以或许于  2011  年  7  月  28  日准时发布。  “B     计划  ”  把不能按时完成的  Lambda  项目、  Jigsaw  项目和  Coin  项目标部分改进延迟到  JDK     1.8  之     中。最终,  JDK 1.7  的重要改进包罗:提供新的  G1  收集器(  G1  在发布时依然处于  Experimental     状态,直至  2012  年  4  月的  Update     4  中才正式  “  转正  ”  )、加强对非  Java  语言的调用支持(  JSR-     292  ,这项特性到目前为止依然没有完全实现定型)、升级类加载架构等。     到目前为止,  JDK 1.7  已经发布了  9  个  Update  版本,最新的  Java SE 7 Update 9  于  2012  年  10     月  16  日发布。从  Java SE 7 Update 4  起,  Oracle  开始支持  Mac OS X  操作系统,并在  Update 6  中达     到完全支持的水平,同时,在  Update 6  中还对  ARM  指令集架构提供了支持。至此,官方提供     的  JDK  可以运行于  Windows  (不含  Windows     9x  )、  Linux  、  Solaris  和  Mac     OS  平台上,支持     ARM  、  x86  、  x64  和  Sparc  指令集架构范例。  2009  年  4  月  20  日,  Oracle  公司公布正式以  74  亿美元的代价收购  Sun  公司,  Java  商标从此正     式归  Oracle  所有(  Java  语言本身并不属于哪间公司所有,它由  JCP  构造举行管理,只管  JCP  主     要是由  Sun  公司大概说  Oracle  公司所领导的)。由于此前  Oracle  公司已经收购了另外一家大型     的中间件企业  BEA  公司,在完成对  Sun  公司的收购之后,  Oracle  公司分别从  BEA  和  Sun  中取得     了目前三大贸易假造机的此中两个:  JRockit  和  HotSpot,Oracle  公司公布在未来  1  ~  2  年的时间     内,将把这两个良好的假造机互相取长补短,最终合二为一    [2]    。可以预见在不久的将     来,  Java  假造机技术将会产生相称巨大的厘革。     根据  Oracle  官方提供的信息,  JDK 1.8  的第一个正式版本将于  2013  年  9  月发布,  JDK 1.8  将     会提供在  JDK     1.7  中规划过,但最终未能在  JDK     1.7  中发布的特性,即  Lambda  表达式、     Jigsaw  (很不幸,随后  Oracle  公司又公布  Jigsaw  在  JDK     1.8  中依然无法完成,须要延至  JDK     1.9  )和  JDK 1.7  中未实现的一部分  Coin  等。     在  2011  年的  JavaOne  大会上,  Oracle  公司还提到了  JDK 1.9  的长远规划,盼望未来的  Java  虚     拟机可以或许管理数以  GB  计的  Java  堆,可以或许更高效地与当地代码集成,并且令  Java  假造机运行时     尽可能少人工干预,可以或许自动调节。     [1]  JDK  从  1.5  版本开始,官方在正式文档与宣传上已经不再利用雷同  JDK 1.5  的定名,只有在     程序员内部利用的开发版本号(  Developer Version  ,比方  java-version  的输出)中才继承相沿     1.5  、  1.6  、  1.7  的版本号,而公开版本号(  Product Version  )则改为  JDK 5  、  JDK 6  、  JDK 7  的命     名方式,本书为了行文同等,所有场所统一采用开发版本号的定名方式。     [2]  “HotRockit”  项目标相干介绍:  http://hirt.se/presentations/WhatToExpect.ppt  。  1.4 Java  假造机发展史     上一节我们从整个  Java  技术的角度观察了  Java  技术的发展,很多  Java  程序员都会潜意识     地把它与  Sun  公司的  HotSpot  假造机等同看待,大概尚有一些程序员会注意到  BEA     JRockit  和     IBM J9  ,但对  JVM  的认识不仅仅只有这些。     从  1996  年初  Sun  公司发布的  JDK 1.0  中所包罗的  Sun Classic VM  到今天,曾经涌现、湮灭过     很多或经典或良好或有特色的假造机实现,在这一节中,我们先临时把代码与技术放下,一     起来回首一下  Java  假造机家族的发展轨迹和历史变迁。     1.4.1 Sun Classic/Exact VM     以今天的视角来看,  Sun Classic VM  的技术可能很原始,这款假造机的使命也早已终     结。但仅凭它  “  天下上第一款商用  Java  假造机  ”  的头衔,就充足有让历史记着它的理由。     1996  年  1  月  23  日,  Sun  公司发布  JDK 1.0  ,  Java  语言首次拥有了商用的正式运行环境,这个     JDK  中所带的假造机就是  Classic VM  。这款假造机只能利用纯解释器方式来执行  Java  代码,     假如要利用  JIT  编译器,就必须举行外挂。但是假如外挂了  JIT  编译器,  JIT  编译器就完全接受     了假造机的执行系统,解释器便不再工作了。用户在这款假造机上执行  java-version  命令,将     会看到雷同下面这行输出:     java version"1.2.2"     Classic VM  (  build JDK-1.2.2-001  ,  green threads,sunwjit  )     此中的  “sunwjit”  就是  Sun  提供的外挂编译器,其他雷同的外挂编译器尚有  Symantec JIT  和     shuJIT  等。由于解释器和编译器不能配合工作,这就意味着假如要利用编译器执行,编译器     就不得不对每一个方法、每一行代码都举行编译,而无论它们执行的频率是否具有编译的价     值。基于程序相应时间的压力,这些编译器根本不敢应用编译耗时稍高的优化技术,因此这     个阶段的假造机即利用了  JIT  编译器输出当地代码,执行服从也和传统的  C/C++  程序有很大差     距,  “Java  语言很慢  ”  的形象就是在这时候开始在用户心中树立起来的。     Sun  的假造机团队努力去办理  Classic VM  所面临的各种标题,提升运行服从。在  JDK 1.2     时,曾在  Solaris  平台上发布过一款名为  Exact VM  的假造机,它的执行系统已经具备当代高性     能假造机的雏形:如两级即时编译器、编译器与解释器混合工作模式等。  Exact VM  因它利用     正确式内存管理(  Exact Memory Management  ,也可以叫  Non-Conservative/Accurate Memory     Management  )而得名,即假造机可以知道内存中某个位置的数据具体是什么范例。譬如内存     中有一个  32  位的整数  123456  ,它到底是一个  reference  范例指向  123456  的内存所在还是一个数     值为  123456  的整数,假造机将有本领分辨出来,这样才能在  GC  (垃圾收集)的时候正确判     断堆上的数据是否还可能被利用。由于利用了正确式内存管理,  Exact     VM  可以抛弃以前     Classic VM  基于  handler  的对象查找方式(缘故原由是举行  GC  后对象将可能会被移动位置,假如将     所在为  123456  的对象移动到  654321  ,在没有明白信息表明内存中哪些数据是  reference  的条件     下,假造机是不敢把内存中所有为  123456  的值改成  654321  的,以是要利用句柄来保持     reference  值的稳定),这样每次定位对象都少了一次间接查找的开销,提升执行性能。     虽然  Exact VM  的技术相对  Classic VM  来说先辈了很多,但是在贸易应用上只存在了很短     暂的时间就被更为良好的  HotSpot VM  所代替,乃至还没有来得及发布  Windows  和  Linux  平台下  的商用版本。而  Classic VM  的生命周期则相对长了很多,它在  JDK 1.2  之前是  Sun JDK  中唯一     的假造机,在  JDK 1.2  时,它与  HotSpot VM  并存,但默认利用的是  Classic VM  (用户可用  java    hotspot  参数切换至  HotSpot VM  ),而在  JDK 1.3  时,  HotSpot VM  成为默认假造机,但  Classic     VM  仍作为假造机的  “  备用选择  ”  发布(利用  java-classic  参数切换),直到  JDK     1.4  的时     候,  Classic VM  才完全退出商用假造机的历史舞台,与  Exact VM  一起进入了  Sun Labs Research     VM  之中。  1.4.2 Sun HotSpot VM     提起  HotSpot VM  ,相信所有  Java  程序员都知道,它是  Sun JDK  和  OpenJDK  中所带的假造     机,也是目前利用范围最广的  Java  假造机。但不一定所有人都知道的是,这个目前看起     来  “  血统纯正  ”  的假造机在最初并非由  Sun  公司开发,而是由一家名为  “Longview     Technologies”  的小公司设计的;乃至这个假造机最初并非是为  Java  语言而开发的,它来源于     Strongtalk VM  ,而这款假造机中相称多的技术又是来源于一款支持  Self  语言实现  “  到达  C  语言     50%  以上的执行服从  ”  的目标而设计的假造机,  Sun  公司注意到了这款假造机在  JIT  编译上有许     多良好的理念和实际效果,在  1997  年收购了  Longview Technologies  公司,从而获得了  HotSpot     VM  。     HotSpot     VM  既继承了  Sun  之前两款商用假造机的优点(如前面提到的正确式内存管     理),也有很多本身新的技术优势,如它名称中的  HotSpot  指的就是它的热门代码探测技术     (实在两个  VM  基本上是同时期的独立产物,  HotSpot  还稍早一些,  HotSpot  一开始就是正确式     GC  ,而  Exact VM  之中也有与  HotSpot  险些一样的热门探测。为了  Exact VM  和  HotSpot VM  哪个     成为  Sun  重要支持的  VM  产物,在  Sun  公司内部尚有过争论,  HotSpot  打败  Exact  并不能算技术上     的胜利),  HotSpot     VM  的热门代码探测本领可以通过执行计数器找出最具有编译价值的代     码,然后通知  JIT  编译器以方法为单位举行编译。假如一个方法被频仍调用,或方法中有用     循环次数很多,将会分别触发标准编译和  OSR  (栈上替换)编译动作。通过编译器与解释器     恰当地协同工作,可以在最优化的程序相应时间与最佳执行性能中取得均衡,而且无须等待     当地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于引入更多的代码     优化技术,输出质量更高的当地代码。     在  2006  年的  JavaOne  大会上,  Sun  公司公布最终会把  Java  开源,并在随后的一年,连续将     JDK  的各个部分(此中当然也包罗了  HotSpot VM  )在  GPL  协议下公开了源码,并在此基础上     建立了  OpenJDK  。这样,  HotSpot VM  便成为了  Sun JDK  和  OpenJDK  两个实现极度接近的  JDK  项     目标共同假造机。     在  2008  年和  2009  年,  Oracle  公司分别收购了  BEA  公司和  Sun  公司,这样  Oracle  就同时拥有     了两款良好的  Java  假造机:  JRockit VM  和  HotSpot VM  。  Oracle  公司公布在不久的未来(约莫     应在发布  JDK 8  的时候)会完成这两款假造机的整合工作,使之优势互补。整合的方式大抵     上是在  HotSpot  的基础上,移植  JRockit  的良好特性,譬如利用  JRockit  的垃圾接纳器与     MissionControl  服务,利用  HotSpot  的  JIT  编译器与混合的运行时系统。  1.4.3 Sun Mobile-Embedded VM/Meta-Circular VM     Sun  公司所研发的假造机可不仅有前面介绍的服务器、桌面领域的商用假造机,除此之     外,  Sun  公司面临移动和嵌入式市场,也发布过假造机产物,另外尚有一类假造机,在设计     之初就没抱有商用的目标,仅仅是用于研究、验证某种技术和观点,又大概是作为一些规范     的标准实现。这些假造机对于大部分不从事相干领域开发的  Java  程序员来说可能比较陌生。     Sun  公司发布的其他  Java  假造机有:     (  1  )  KVM     KVM  中的  K  是  “Kilobyte”  的意思,它夸大简朴、轻量、高度可移植,但是运行速率比较     慢。在  Android  、  iOS  等智能手机操作系统出现前曾经在手机平台上得到非常广泛的应用。     (  2  )  CDC/CLDC HotSpot Implementation     CDC/CLDC  全称是  Connected  (  Limited  )  Device Configuration  ,在  JSR-139/JSR-218  规范中     举行定义,它盼望在手机、电子书、  PDA  等装备上建立统一的  Java  编程接口,而  CDC-HI VM     和  CLDC-HI VM  则是它们的一组参考实现。  CDC/CLDC  是整个  Java ME  的重要支柱,但从目前     Android  和  iOS  二分天下的移动数字装备市场看来,在这个领域中,  Sun  的假造机所面临的局     面远不如服务器和桌面领域乐观。     (  3  )  Squawk VM     Squawk     VM  由  Sun  公司开发,运行于  Sun SPOT  (  Sun Small Programmable Object     Technology  ,一种手持的  WiFi  装备),也曾经运用于  Java Card  。这是一个  Java  代码比重很高     的嵌入式假造机实现,此中诸如类加载器、字节码验证器、垃圾收集器、解释器、编译器和     线程调度都是  Java  语言本身完成的,仅仅靠  C  语言来编写装备  I/O  和须要的当地代码。     (  4  )  JavaInJava     JavaInJava  是  Sun  公司于  1997  年~  1998  年间研发的一个实验室性子的假造机,从名字就可     以看出,它试图以  Java  语言来实现  Java  语言本身的运行环境,既所谓的  “  元循环  ”  (  Meta    Circular  ,是指利用语言自身来实现其运行环境)。它必须运行在另外一个宿主假造机之     上,内部没有  JIT  编译器,代码只能以解释模式执行。在  20  世纪末主流  Java  假造机都未能很好     办理性能标题标期间,开发这种项目,其执行速率可想而知。     (  5  )  Maxine VM     Maxine VM  和上面的  JavaInJava  非常相似,它也是一个险些全部以  Java  代码实现(只有用     于启动  JVM  的加载器利用  C  语言编写)的元循环  Java  假造机。这个项目于  2005  年开始,到现     在仍然在发展之中,比起  JavaInJava,Maxine VM  就显得  “  靠谱  ”  很多,它有先辈的  JIT  编译器和     垃圾收集器(但没有解释器),可在宿主模式或独立模式下执行,其执行服从已经接近了     HotSpot Client VM  的水平。  1.4.4 BEA JRockit/IBM J9 VM     前面介绍了  Sun  公司的各种假造机,除了  Sun  公司以外,其他构造、公司也研发过不少虚     拟机实现,此中规模最大、最著名的就是  BEA  和  IBM  公司了。     JRockit     VM  曾经号称  “  天下上速率最快的  Java  假造机  ”  (广告词,貌似  J9 VM  也这样说     过),它是  BEA  公司在  2002  年从  Appeal Virtual Machines  公司收购的假造机。  BEA  公司将其发     展为一款专门为服务器硬件和服务器端应用场景高度优化的假造机,由于专注于服务器端应     用,它可以不太关注程序启动速率,因此  JRockit  内部不包罗剖析器实现,全部代码都靠即时     编译器编译后执行。除此之外,  JRockit  的垃圾收集器和  MissionControl  服务套件等部分的实     现,在众多  Java  假造机中也一直处于领先水平。     IBM J9 VM  并不是  IBM  公司唯一的  Java  假造机,不外是目前其主力发展的  Java  假造机。     IBM J9 VM  原来是内部开发代号,正式名称是  “IBM Technology for Java Virtual Machine”  ,简     称  IT4J  ,只是这个名字太拗口了一点,遍及水平不如  J9  。  J9 VM  最初是由  IBM Ottawa  实验室     一个名为  SmallTalk  的假造机扩展而来的,当时这个假造机有一个  bug  是由  8k  值定义错误引起     的,工程师花了很长时间终于发现并办理了这个错误,此后这个版本的假造机就称为  K8  了,     后来扩展出支持  Java  的假造机就被称为  J9  了。与  BEA JRockit  专注于服务器端应用不同,  IBM     J9  的市场定位与  Sun HotSpot  比较接近,它是一款设计上从服务器端到桌面应用再到嵌入式都     全面考虑的多用途假造机,  J9  的开发目标是作为  IBM  公司各种  Java  产物的执行平台,它的主     要市场是和  IBM  产物(如  IBM WebSphere  等)搭配以及在  IBM AIX  和  z/OS  这些平台上部署  Java     应用。  1.4.5 Azul VM/BEA Liquid VM     我们平时所提及的  “  高性能  Java  假造机  ”  一般是指  HotSpot  、  JRockit  、  J9  这类在通用平台上     运行的商用假造机,但实在  Azul VM  和  BEA Liquid VM  这类特定硬件平台专有的假造机才     是  “  高性能  ”  的武器。     Azul VM  是  Azul Systems  公司在  HotSpot  基础上举行大量改进,运行于  Azul Systems  公司的     专有硬件  Vega  系统上的  Java  假造机,每个  Azul VM  实例都可以管理至少数十个  CPU  和数百  GB     内存的硬件资源,并提供在巨大内存范围内实现可控的  GC  时间的垃圾收集器、为专有硬件     优化的线程调度等良好特性。在  2010  年,  Azul Systems  公司开始从硬件转向软件,发布了自     己的  Zing JVM  ,可以在通用  x86  平台上提供接近于  Vega  系统的特性。     Liquid VM  即是现在的  JRockit VE  (  Virtual Edition  ),它是  BEA  公司开发的,可以直接运     行在自家  Hypervisor  系统上的  JRockit VM  的假造化版本,  Liquid VM  不须要操作系统的支持,     大概说它本身本身实现了一个专用操作系统的须要功能,如文件系统、网络支持等。由假造     机越过通用操作系统直接控制硬件可以获得很多好处,如在线程调度时,不须要再举行内核     态  /  用户态的切换等,这样可以最大限度地发挥硬件的本领,提升  Java  程序的执行性能。  1.4.6 Apache Harmony/Google Android Dalvik VM     这节介绍的  Harmony VM  和  Dalvik VM  只能称做  “  假造机  ”  ,而不能称做  “Java  假造机  ”  ,但     是这两款假造机(以及所代表的技术体系)对最近几年的  Java  天下产生了非常大的影响和挑     战,乃至有些悲观的评论家以为成熟的  Java  生态系统有瓦解的可能。     Apache Harmony  是一个  Apache  软件基金会旗下以  Apache License  协议开源的实际兼容于     JDK 1.5  和  JDK 1.6  的  Java  程序运行平台,这个介绍相称拗口。它包罗本身的假造机和  Java  库,     用户可以在上面运行  Eclipse  、  Tomcat  、  Maven  等常见的  Java  程序,但是它没有通过  TCK  认     证,以是我们不得不消那么一长串拗口的语言来介绍它,而不能用一句  “Apache  的  JDK”  来说     明。假如一个公司要公布本身的运行平台  “  兼容于  Java  语言  ”  ,那就必须要通过     TCK  (  Technology Compatibility Kit  )的兼容性测试。  Apache  基金会曾要求  Sun  公司提供  TCK  的     利用授权,但是一直遭到拒绝,直到  Oracle  公司收购了  Sun  公司之后,两边关系越闹越僵,最     终导致  Apache  愤然退出  JCP  (  Java Community Process  )构造,这是目前为止  Java  社区最严重     的一次  “  分裂  ”  。     在  Sun  将  JDK  开源形成  OpenJDK  之后,  Apache Harmony  开源的优势被极大地削弱,乃至连     Harmony  项目标最大到场者  IBM  公司也公布辞去  Harmony  项目管理主席的职位,并到场     OpenJDK  项目标开发。虽然  Harmony  没有颠末真正大规模的贸易运用,但是它的很多代码     (基本上是  Java  库部分的代码)被吸纳进  IBM  的  JDK 7  实现及  Google Android SDK  之中,尤其     是对  Android  的发展起到了很大的推动作用。     说到  Android  ,这个时下最热门的移动数码装备平台在最近几年间的发展过程中所取得     的成果已经远远超越了  Java ME  在过去十多年所获得的成果,  Android  让  Java  语言真正走进了     移动数码装备领域,只是走的并非  Sun  公司原来想象的那一条路。     Dalvik VM  是  Android  平台的核心组成部分之一,它的名字来源于冰岛一个名为  Dalvik  的     小渔村。  Dalvik VM  并不是一个  Java  假造机,它没有遵照  Java  假造机规范,不能直接执行  Java     的  Class  文件,利用的是寄存器架构而不是  JVM  中常见的栈架构。但是它与  Java  又有着千丝万     缕的接洽,它执行的  dex  (  Dalvik Executable  )文件可以通过  Class  文件转化而来,利用  Java  语     法编写应用程序,可以直接利用大部分的  Java API  等。目前  Dalvik VM  随着  Android  一起处于     迅猛发展阶段,在  Android 2.2  中已提供即时编译器实现,在执行性能上有了很大的提高。  1.4.7 Microsoft JVM  及其他     在十几年的  Java  假造机发展过程中,除去上面介绍的那些被大规模贸易应用过的  Java  虚     拟机外,尚有很多假造机是不为人知的大概曾经  “  壮丽  ”  过但最终湮灭的。我们以此中微软公     司的  JVM  为例来介绍一下。     大概  Java  程序员听起来可能会觉得惊讶,微软公司曾经是  Java  技术的铁杆支持者(也必     须承认,与  Sun  公司争取  Java  的控制权,令  Java  从跨平台技术变为绑定在  Windows  上的技术是     微软公司的重要目标)。在  Java  语言诞生的初期(  1996  年~  1998  年,以  JDK     1.2  发布为分     界),它的重要应用之一是在欣赏器中运行  Java     Applets  程序,微软公司为了在  IE3  中支持     Java Applets  应用而开发了本身的  Java  假造机,虽然这款假造机只有  Windows  平台的版本,却     是当时  Windows  下性能最好的  Java  假造机,它在  1997  年和  1998  年连续两年获得了《  PC     Magazine  》杂志的  “  编辑选择奖  ”  。但好景不长,在  1997  年  10  月,  Sun  公司正式以侵犯商标、不     正当竞争等罪名控告微软公司,在随后对微软公司的垄断调查之中,这款假造机也曾作为证     据之一被呈送法庭。这场官司的结果是微软公司赔偿  2000  万美金给  Sun  公司(最终微软公司     因垄断赔偿给  Sun  公司的总金额高达  10  亿美元),承诺制止其  Java  假造机的发展,并逐步在     产物中移除  Java  假造机相干功能。具有讽刺意味的是,到最后在  Windows XP SP3  中  Java  假造     机被完全抹去的时候,  Sun  公司却又到处登报盼望微软公司不要这样做    [1]    。  Windows     XP  高级     产物司理  Jim     Cullinan  称:  “  我们耗费了  3  年的时间和  Sun  打官司,当时他们试图阻止我们在     Windows  中支持  Java  ,现在我们这样做了,可他们又在抱怨,这太具有讽刺意味了。  ”     我们试想一下,假如当年  Sun  公司没有告状微软公司,微软公司继承保持着对  Java  技术     的热情,那  Java  的天下会变得怎么样呢?  .NET  技术是否会发展起来?但历史是没有假设的。     其他在本节中没有介绍到的  Java  假造机尚有(当然,应该尚有很多笔者所不知道的):     JamVM.     cacaovm.     SableVM.     Kaffe.     Jelatine JVM.     NanoVM.     MRP.     Moxie JVM.     Jikes RVM.     [1]  Sun  公司在《纽约时报》、《圣约瑟贸易消息》和《华尔街周刊》上刊登了整页的广告,     在广告词中  Sun  公司招呼消耗者  “  要求微软公司继承在其  Windows XP  系统包罗  Java  平台  ”  。  1.5   展望  Java  技术的未来     在  2005  年,  Java  语言诞生  10  周年的  SunOne  技术大会上,  Java  语言之父  James Gosling  做了     一场题为  “Java  技术下一个十年  ”  的演讲。笔者不具备  James     Gosling  博士那样高屋建瓴的视     角,这里仅从  Java  平台中几个新生的但已经开始显现出发达之势的技术发展点来看一下后续     1  ~  2  个  JDK  版本内的一些很有盼望的技术重点。     1.5.1   模块化     模块化是办理应用系统与技术平台越来越复杂、越来越庞大标题标一个重要途径。无论     是开发人员还是产物最终用户,都不盼望为了系统中一小块的功能而不得不下载、安装、部     署及维护整套庞大的系统。站在整个软件工业化的高度来看,模块化是建立各种功能的标准     件的条件。最近几年  OSGi  技术的敏捷发展、各个厂商在  JCP  中对模块化规范的猛烈斗争    [1]    ,     都能充实说明模块化技术的急迫和重要。     在未来的  Java  平台中,很可能会对模块化提出语法层面的支持。早在  2007  年,  Sun  公司     就提出过  JSR-277  :  Java  模块系统(  Java Module System  ),试图建立  Java  平台的模块化标准,     但受挫于以  IBM  公司为主导提交的  JSR-291  :  Java     SE  动态组件支持(  Dynamic     Component     Support for Java SE  ,这实际就是  OSGi R4.1  )。由于模块化规范主导权的重要性,  Sun  公司不     能担当一个无法由它控制的规范,在整个  Java SE 6  期间都拒绝把任何模块化技术内置到  JDK     之中。在  Java SE 7  发展初期,  Sun  公司再次提交了一个新的规范请求文档  JSR-294  :  Java  编程     语言中的改进模块性支持(  Improved Modularity Support in the Java Programming Language  ),     只管这个  JSR  仍然没有通过,但是  Sun  公司已经独立于  JCP  专家组在  OpenJDK  里建立了一个名     为  Jigsaw  (拼图)的子项目来推动这个规范在  Java  平台中变化为具体的实现。  Java  的模块化     之争目前还没有结束,  OSGi  已经发布到  R5.0  版本,而  Jigsaw  从  Java 7  延迟至  Java 8  ,在  2012  年     7  月又不得不公布推迟到  Java 9  中发布,从这点看来,  Sun  在这场战争中处于劣势,但无论胜     利者是哪一方,  Java  模块化已经成为一项无法拦截的厘革潮水。     [1]  假如读者对  Java  模块化之争感爱好,可以阅读笔者的另外一本书《深入理解  OSGi  :     Equinox  原理、应用与最佳实践》的第  1  章。  1.5.2   混合语言     当单一的  Java  开发已经无法满足当前软件的复杂需求时,越来越多基于  Java  假造机的语     言开发被应用到软件项目中,  Java  平台上的多语言混合编程正成为主流,每种语言都可以针     对本身善于的方面更好地办理标题。试想一下,在一个项目之中,并行处理惩罚用  Clojure  语言编     写,展示层利用  JRuby/Rails  ,中间层则是  Java  ,每个应用层都将利用不同的编程语言来完     成,而且,接口对每一层的开发者都是透明的,各种语言之间的交互不存在任何困难,就像     利用本身语言的原生  API  一样方便    [1]    ,由于它们最终都运行在一个假造机之上。     在最近的几年里,  Clojure  、  JRuby  、  Groovy  等新生语言的利用人数不断增长,而运行在     Java  假造机(  JVM  )之上的语言数量也在敏捷膨胀,图  1-4  中枚举了此中的一部分。这两点证     明混合编程在我们身边已经有所应用并被广泛认可。通过特定领域的语言去办理特定领域的     标题是当前软件开发应对日趋复杂的项目需求的一个方向。     图   1-4   可以运行在  JVM  之上的语言    [2]  除了催生出大量的新语言外,很多已经有很长历史的程序语言也出现了基于  Java  假造机     实现的版本,这样使得混合编程对很多以前利用其他语言的  “  老  ”  程序员也具备相称大的吸引     力,软件企业投入了大量资本的现有代码资产也能很好地掩护起来。表  1-1  中枚举了常见语     言的  JVM  实现版本。     对这些运行于  Java  假造机之上、  Java  之外的语言,来自系统级的、底层的支持正在敏捷     增强,以  JSR-292  为核心的一系列项目和功能改进(如  Da Vinci Machine  项目、  Nashorn  引擎、     InvokeDynamic  指令、  java.lang.invoke  包等),推动  Java  假造机从  “Java  语言的假造机  ”  向  “  多语     言假造机  ”  的方向发展。     [1]  在同一个假造机上运行的其他语言与  Java  之间的交互一般都比较轻易,但非  Java  语言之间     的交互一般都比较烦琐。  dynalang  项目(  http://dynalang.sourceforge.net/  )就是为了办理这个问     题而出现的。     [2]  图片来源:  http://www.Slideshare.net/josebetomex/oow-2009-towards-a-universal-vm  。  1.5.3   多核并行     如今,  CPU  硬件的发展方向已经从高频率变化为多核心,随着多核期间的到临,软件开     发越来越关注并行编程的领域。早在  JDK 1.5  就已经引入  java.util.concurrent  包实现了一个粗粒     度的并发框架。而  JDK 1.7  中到场的  java.util.concurrent.forkjoin  包则是对这个框架的一次重要     扩充。  Fork/Join  模式是处理惩罚并行编程的一个经典方法,如图  1-5  所示。虽然不能办理所有的问     题,但是在此模式的适用范围之内,可以或许轻松地利用多个  CPU  核心提供的计算资源来协作完     成一个复杂的计算任务。通过利用  Fork/Join  模式,我们可以或许更加顺畅地过渡到多核期间。     图   1-5 Fork/Join  模式表示图    [1]     在  Java 8  中,将会提供  Lambda  支持,这将会极大改善目前  Java  语言不适合函数式编程的     近况(目前  Java  语言利用函数式编程并不是不可以,只是会显得很臃肿),函数式编程的一     个重要优点就是这样的程序自然地适合并行运行,这对  Java  语言在多核期间继承保持主流语     言的地位有很大资助。     另外,在并行计算中必须提及的尚有  OpenJDK  的子项目  Sumatra     [2]    ,目前显卡的算术运算     本领、并行本领已经远远凌驾了  CPU  ,在图形领域以外发掘显卡的潜力是近几年计算机发展     的方向之一,比方  C  语言的  CUDA  。  Sumatra  项目就是为  Java  提供利用  GPU  (  Graphics     Processing Units  )和  APU  (  Accelerated Processing Units  )运算本领的工具,以后它将会直接提     供  Java  语言层面的  API  ,大概为  Lambda  和其他  JVM  语言提供底层的并行运算支持。     在  JDK  外围,也出现了专为满足并行计算需求的计算框架,如  Apache  的  Hadoop     Map/Reduce  ,这是一个简朴易懂的并行框架,可以或许运行在由上千个商用呆板组成的大型集群     上,并且能以一种可靠的容错方式并行处理惩罚  TB  级别的数据集。另外,还出现了诸如  Scala  、     Clojure  及  Erlang  等天生就具备并行计算本领的语言。     [1]  图片来源:  http://www.ibm.com/developerworks/cn/java/j-lo-forkjoin/  。     [2]  Sumatra  项目主页:  http://openjdk.java.net/projects/sumatra/  。  1.5.4   进一步丰富语法     Java 5  曾经对  Java  语法举行了一次扩充,这次扩充到场了自动装箱、泛型、动态注解、     罗列、可变长参数、遍历循环等语法,使得  Java  语言的精确性和易用性有了很大的进步。在     Java 7  (由于进度压力,很多改进已推迟至  Java 8  )中,对  Java  语法举行了另一次大规模的扩     充。  Sun  (已被  Oracle  收购)专门为改进  Java  语法在  OpenJDK  中建立了  Coin  子项目    [1]    来统一处     理对  Java  语法的细节修改,如二进制数的原生支持、在  switch  语句中支持字符串、  “  <>  ”  操     作符、异常处理惩罚的改进、简化变长参数方法调用、面向资源的  try-catch-finally  语句等都是在     Coin  项目之中提交的内容。     除了  Coin  项目之外,在  JSR-335  (  Lambda Expressions for the Java TM Programming     Language  )中定义的  Lambda  表达式    [2]    也将对  Java  的语法和语言风俗产生很大的影响,面向函数     方式的编程可能会成为主流。     [1]  Coin  项目主页:  http://wikis.sun.com/display/ProjectCoin/Home  。     [2]  Lambda  项目主页:  http://openjdk.java.net/projects/lambda/  。  1.5.5 64  位假造机     在几年之前,主流的  CPU  就开始支持  64  位架构了。  Java  假造机也在很早之前就推出了支     持  64  位系统的版本。但  Java  程序运行在  64  位假造机上须要付出比较大的额外代价:首先是内     存标题,由于指针膨胀和各种数据范例对齐补白的缘故原由,运行于  64  位系统上的  Java  应用须要     消耗更多的内存,通常要比  32  位系统额外增长  10%  ~  30%  的内存消耗;其次,多个机构的测     试结果显示,  64  位假造机的运行速率在各个测试项中险些全面掉队于  32  位假造机,两者约莫     有  15%  左右的性能差距。     但是在  Java EE  方面,企业级应用常常须要利用凌驾  4GB  的内存,对于  64  位假造机的需求     黑白常急迫的,但由于上述缘故原由,很多企业应用都仍然选择利用假造集群等方式继承在  32  位     假造机中举行部署。  Sun  也注意到了这些标题,并做出了一些改善,在  JDK 1.6 Update 14  之     后,提供了普通对象指针压缩功能(  -XX  :  +UseCompressedOops  ,这个参数不建议显式设     置,建议维持默认由假造机的  Ergonomics  机制自动开启),在执行代码时,动态植入压缩指     令以节省内存消耗,但是开启压缩指针会增长执行代码数量,由于所有在  Java  堆里的、指向     Java  堆内对象的指针都会被压缩,这些指针的访问就须要更多的代码才可以实现,而且并不     只是读写字段才受影响,在实例方法调用、子范例查抄等操作中也受影响,由于对象实例指     向对象范例的引用也被压缩了。随着硬件的进一步发展,计算机终究会完全过渡到  64  位的时     代,这是一件毫无疑问的事情,主流的假造机应用也终究会从  32  位发展至  64  位,而假造机对     64  位的支持也将会进一步美满。  1.6   实战:本身编译  JDK     想要一探  JDK  内部的实现机制,最便捷的路径之一就是本身编译一套  JDK  ,通过阅读和     跟踪调试  JDK  源码去了解  Java  技术体系的原理,虽然门槛会高一点,但肯定会比阅读各种书     籍、文章更加贴近本质。另外,  JDK  中的很多底层方法都是当地化(  Native  )的,须要跟踪     这些方法的运作或对  JDK  举行  Hack  的时候,都须要本身编译一套  JDK  。     现在网络上有不少开源的  JDK  实现可以供我们选择,如  Apache Harmony  、  OpenJDK  等。     考虑到  Sun  系列的  JDK  是现在利用得最广泛的  JDK  版本,笔者选择了  OpenJDK  举行这次编译实     战。     1.6.1   获取  JDK  源码     首先要先明白  OpenJDK  和  Sun/OracleJDK  之间,以及  OpenJDK 6  、  OpenJDK 7  、  OpenJDK     7u  和  OpenJDK 8  等项目之间是什么关系,这有助于确定接下来编译要利用的  JDK  版本和源码     分支。     从前面介绍的  Java  发展史中我们了解到  OpenJDK  是  Sun  在  2006  年末把  Java  开源而形成的项     目,这里的  “  开源  ”  是通常意义上的源码开放形式,即源码是可被复用的,比方  IcedTea     [1]    、     UltraViolet     [2]    都是从  OpenJDK  源码衍生出的发行版。但假如仅从  “  开源  ”  字面意义(开放可阅读     的源码)上看,实在  Sun  自  JDK 1.5  之后就开始以  Java Research License  (  JRL  )的形式公布过     Java  源码,重要用于研究人员阅读(  JRL  允许证的开放源码至  JDK 1.6 Update 23  为止)。把这     些  JRL  允许证形式的  Sun/OracleJDK  源码和对应版本的  OpenJDK  源码举行比较,发现除了文件     头的版权注释之外,别的代码基本上都是雷同的,只有字体渲染部分存在一点差别,  Oracle     JDK  采用了贸易实现,而  OpenJDK  利用的是开源的  FreeType  。当然,  “  雷同  ”  是建立在两者共     有的组件基础上的,  Oracle JDK  中还会存在一些  Open JDK  没有的、商用闭源的功能,比方从     JRockit  移植改造而来的  Java Flight Recorder  。预计以后  JRockit  的  MissionControl  移植到  HotSpot     之后,也会以  Oracle JDK  专有、闭源的形式提供。     Oracle  的项目发布司理  Joe Darcy  在  OSCON 2011  上对两者关系的介绍    [3]    也证明了  OpenJDK     7  和  Oracle JDK 7  在程序上黑白常接近的,两者共用了大量雷同的代码(如图  1-6  所示,注意     图中提示了两者共同代码的占比要远高于图形上看到的比例),以是我们编译的  OpenJDK  ,     基本上可以以为性能、功能和执行逻辑上都和官方的  Oracle JDK  是同等的。  图   1-6 OpenJDK  和  Oracle JDK  之间的关系     再来看一下  OpenJDK 6  、  OpenJDK 7  、  OpenJDK 7u  和  OpenJDK 8  这几个项目之间的关系,     从图  1-7  (依然是从  Joe Darcy  的  OSCON 2011  演示稿中截取的图片)来看,  OpenJDK 7  是始于     JDK 6  时期,当时  JDK 6  和  JDK 6 Update 1  已经发布,  JDK 7  已经开始研发了,以是  OpenJDK 7     是直接基于正在研发的  JDK 7  源码建立的。但考虑到  OpenJDK 7  的状况在当时还不适合实际生     产部署,因此在  OpenJDK 7 Build 20  的基础上建立了  OpenJDK 6  分支,剥离掉  JDK 7  新功能的     代码,形成一个可以通过  TCK 6  测试的独立分支。     图   1-7 OpenJDK 6  、  OpenJDK 7  、  OpenJDK 7u  、  OpenJDK 8  之间的关系  2012  年  7  月,  JDK 7  正式发布,在  OpenJDK  中也同步建立了  OpenJDK 7 Update  项目对  JDK 7     举行更新升级,以及  OpenJDK 8  项目开始下一个  JDK  大版本的研发。按照开发风俗,新的功     能或  Bug  修复通常是在最新分支上举行的,当功能或修复在最新分支上稳定之后会同步到其     他老版本的维护分支上。     OpenJDK 6  、  OpenJDK 7  、  OpenJDK 7u  和  OpenJDK 8  的源码都可以在它们相应的网页上找     到,在本次编译实践中,笔者选用的项目是  OpenJDK 7u  ,版本为  7u6  。     获取  OpenJDK  源码有两种方式,此中一种是通过  Mercurial  代码版本管理工具从  Repository     中直接取得源码(  Repository  所在:  http://hg.openjdk.java.net/jdk7u/jdk7u  ),获取过程如以下     代码所示。     hg clone http://hg.openjdk.java.net/jdk7u/jdk7u-dev     cd jdk7u-dev     chmod 755 get_source.sh     ./get_source.sh     这是最直接的方式,从版本管理中看变更轨迹比看  Release Note  效果更好。但不敷之处     是速率太慢,虽然代码总容量只有  300 MB  左右,但是文件数量太多,在笔者的网络下全部     复制到当地须要数小时。另外,考虑到  Mercurial  不如  Git  、  SVN  、  ClearCase  或  CVS  之类的版本     控制工具那样遍及,对于一般读者,建议采用第二种方式,即直接下载官方打包好的源码     包,读者可以从  Source Bundle Releases  页面(所在:  http://jdk7.java.net/source.html  )取得打包     好的源码,到当地直接解压即可。一般来说,源码包大概一至两个月左右会更新一次,虽然     不够及时,但比起从  Mercurial  复制代码的确方便和快捷很多。笔者下载的是  OpenJDK     7     Update 6 Build b21  版源码包,  2012  年  8  月  28  日发布,大概  99MB  ,解压后约为  339MB  。     [1]  IcedTea  :  http://icedtea.classpath.org/wiki/Main_Page  。     [2]  UltraViolet  :  https://www.reservoir.com/  ?  q=uvform/form  。     [3]  全文所在:  https://blogs.oracle.com/darcy/resource/OSCON/oscon2011_OpenJDKState.pdf  。  1.6.2   系统需求     假如可能,笔者建议只管在  Linux  、  MacOS  或  Solaris  上构建  OpenJDK  ,这要比在  Windows     平台上轻易得多,本章实战中笔者将以  Ubuntu 10.10  和  MacOS X 10.8.2  为例举行构建。假如读     者一定要在  Windows  平台上完成编译,可参考本书附录  A  ,该附录是本书第一版中介绍如何     在  Windows  下编译  OpenJDK     6  的例子,原有的部分内容现在已颠末时了(比方安装  Plug  部     分),但还是有一定参考意义,因此笔者没有把它删除掉,而是移到附录之中。     无论在什么平台下举行编译,都建议读者认真阅读一遍源码中的  README-builds.html  文     档(无论在  OpenJDK  网站上还是在下载的源码包中都有这份文档),由于编译过程中须要注     意的细节非常多。虽然不至于像文档上所描述的  “Building the source code for the JDK requires     a high level of technical expertise.Sun provides the source code primarily for technical experts who     want to conduct research.  (编译  JDK  须要很高的专业技术,  Sun  提供  JDK  源码是为了技术专家进     行研究之用)  ”  那么夸张,但是假如读者是第一次编译,那有可能会在一些小标题上耗费许     多时间。     在本次编译中采用的是  64  位操作系统,编译的也是  64  位的  OpenJDK  ,假如须要编译  32  位     版本,那建议在  32  位操作系统上举行。在官方文档上写到编译  OpenJDK  至少须要  512MB  的内     存和  600MB  的磁盘空间。  512MB  的内存大概能凑合利用,不外  600MB  的磁盘空间估计仅是指     存放  OpenJDK  源码所需的空间,要完成编译,  600MB  肯定是无论如何都不够的,光输出的编     译结果就有近  3GB  (由于有很多中间文件,以及会编译出不同优化级别(  Product  、  Debug  、     FastDebug  等)的假造机),建议读者至少包管  5GB  以上的空余磁盘。     对系统的最后一点要求就是所有的文件,包罗源码和依赖项目,都不要放在包罗中文的     目次里面,这样做不是一定不可以,只是没有须要给本身找贫苦。  1.6.3   构建编译环境     在  MacOS     [1]    和  Linux  上构建  OpenJDK  编译环境比较简朴(相对于  Windows  来说),对于  Mac     OS  ,须要安装最新版本的  XCode  和  Command Line Tools for XCode  ,在  Apple Developer  网站     (  https://developer.apple.com/  )上可以免费下载,这两个  SDK  包提供了  OpenJDK  所需的编译     器以及  Makefile  中用到的外部命令。另外,还要准备一个  6u14  以上版本的  JDK  ,由于  OpenJDK     的各个组成部分(  Hotspot  、  JDK API  、  JAXWS  、  JAXP……  )有的是利用  C++  编写的,更多的     代码则是利用  Java  自身实现的,因此编译这些  Java  代码须要用到一个可用的  JDK  ,官方称这     个  JDK  为  “Bootstrap JDK”  。假如编译  OpenJDK 7  ,  Bootstrap JDK  必须利用  JDK6 Update 14  或之     后的版本,笔者选用的是  JDK7 Update 4  。最后须要下载一个  1.7.1  以上版本的  Apache Ant  ,用     于执行  Java  编译代码中的  Ant  脚本。     对于  Linux  来说,所须要准备的依赖与  Mac OS  差不多,  Bootstrap JDK  和  Ant  都是一样的,     在  Mac OS  中  GCC  编译器来源于  XCode SDK  ,而  Ubuntu  中  GCC  应该是默认安装好的,须要确保     版本为  4.3  以上,假如没有找到  GCC  ,安装  binutils  即可,在  Ubuntu 10.10  下编译  OpenJDK 7u4  所     需的依赖可以利用以下命令一次安装完成。     sudo apt-get install build-essential gawk m4 openjdk-6-jdk     libasound2-dev libcups2-dev libxrender-dev xorg-dev xutils-dev     x11proto-print-dev binutils libmotif3 libmotif-dev ant     [1]  注意,只有在  OpenJDK 7u4  和之后的版本才能编译出  Mac OS  系统下的  JDK  包,之前的版本     虽然在源码和编译脚本中也包罗了  Mac OS  目次,但是尚未美满。  1.6.4   举行编译     现在须要下载的编译环境和依赖项目都准备齐备了,最后我们还须要对系统的环境变量     做一些简朴设置以便编译可以或许顺利通过。  OpenJDK  在编译时读取的环境变量有很多,但大多     都有默认值,必须设置的只有两个:  LANG  和  ALT_BOOTDIR  ,前者是设定语言选项,必须     设置为:     export LANG=C     否则,在编译结束前的验证阶段会出现一个  HashTable  内的空指针异常。另外一个     ALT_BOOTDIR  参数是前面提到的  Bootstrap JDK  ,在  Mac OS  上笔者设为以下路径,其他操作     系统读者对应调解即可。     export ALT_BOOTDIR=/Library/Java/JavaVirtualMachines/jdk1.7.0_04.jdk/Contents/Home     另外,假如读者之前设置了  JAVA_HOME  和  CLASSPATH  两个环境变量,在编译之前必     须取消,否则在  Makefile  脚本中查抄到有这两个变量存在,会有警告提示。     unset JAVA_HOME     unset CLASSPATH     其他环境变量笔者就不再逐一介绍了,代码清单  1-1  给出笔者本身常用的编译  Shell  脚     本,读者可以参考变量注释中的内容。     代码清单  1-1   环境变量设置     #  语言选项,这个必须设置,否则编译好后会出现一个  HashTable  的  NPE  错     export LANG=C     #Bootstrap JDK  的安装路径。必须设置     export ALT    _    BOOTDIR=/Library/Java/JavaVirtualMachines/jdk1.7.0    _    04.jdk/Contents/Home     #  答应自动下载依赖     export ALLOW    _    DOWNLOADS=true     #  并行编译的线程数,设置为和  CPU  内核数量同等即可     export HOTSPOT_BUILD_JOBS=6     export ALT    _    PARALLEL    _    COMPILE    _    JOBS=6     #  比较本次  build  出来的映像与先前版本的差别。这对我们来说没有意义,     #  必须设置为  false  ,否则  sanity  查抄会报缺少先前版本  JDK  的映像的错误提示。     #  假如已经设置  dev  大概  DEV_ONLY=true  ,这个不显式设置也行     export SKIP    _    COMPARE    _    IMAGES=true     #  利用预编译头文件,不加这个编译会更慢一些     export USE    _    PRECOMPILED    _    HEADER=true     #  要编译的内容     export BUILD_LANGTOOLS=true     #export BUILD_JAXP=false     #export BUILD_JAXWS=false     #export BUILD_CORBA=false     export BUILD_HOTSPOT=true     export BUILD    _    JDK=true     #  要编译的版本     #export SKIP_DEBUG_BUILD=false     #export SKIP_FASTDEBUG_BUILD=true     #export DEBUG    _    NAME=debug     #  把它设置为  false  可以避开  javaws  和欣赏器  Java  插件之类的部分的  build     BUILD    _    DEPLOY=false     #  把它设置为  false  就不会  build  出安装包。由于安装包里有些奇怪的依赖,     #  但即便不  build  出它也已经能得到完备的  JDK  映像,以是还是别  build  它好了     BUILD    _    INSTALL=false     #  编译结果所存放的路径     export ALT    _    OUTPUTDIR=/Users/IcyFenix/Develop/JVM/jdkBuild/openjdk    _    7u4/build     #  这两个环境变量必须去掉,否则会有很诡异的事情发生(我没有具体查过这些  "  诡异的     #  事情  "  ,  Makefile  脚本查抄到有这  2  个变量就会提示警告)     unset JAVA_HOME     unset CLASSPATH     make 2  >&  1|tee $ALT_OUTPUTDIR/build.log     全部设置结束之后,可以输入  make sanity  来查抄我们前面所做的设置是否全部正确。如     果统统顺利,那么几秒钟之后会有雷同代码清单  1-2  所示的输出。     代码清单  1-2 make sanity  查抄  ~  /Develop/JVM/jdkBuild/openjdk_7u4$make sanity     Build Machine Information  :     build machine=IcyFenix-RMBP.local     Build Directory Structure  :     CWD=/Users/IcyFenix/Develop/JVM/jdkBuild/openjdk_7u4     TOPDIR=.     LANGTOOLS_TOPDIR=./langtools     JAXP_TOPDIR=./jaxp     JAXWS_TOPDIR=./jaxws     CORBA_TOPDIR=./corba     HOTSPOT_TOPDIR=./hotspot     JDK_TOPDIR=./jdk     Build Directives  :     BUILD_LANGTOOLS=true     BUILD_JAXP=true     BUILD_JAXWS=true     BUILD_CORBA=true     BUILD_HOTSPOT=true     BUILD_JDK=true     DEBUG_CLASSFILES=     DEBUG    _    BINARIES=     ……  因篇幅关系,中间省略了大量的输出内容  ……     OpenJDK-specific settings  :     FREETYPE_HEADERS_PATH=/usr/X11R6/include     ALT_FREETYPE_HEADERS_PATH=     FREETYPE_LIB_PATH=/usr/X11R6/lib     ALT_FREETYPE_LIB_PATH=     Previous JDK Settings  :     PREVIOUS_RELEASE_PATH=USING-PREVIOUS_RELEASE_IMAGE     ALT_PREVIOUS_RELEASE_PATH=     PREVIOUS_JDK_VERSION=1.6.0     ALT_PREVIOUS_JDK_VERSION=     PREVIOUS_JDK_FILE=     ALT_PREVIOUS_JDK_FILE=     PREVIOUS_JRE_FILE=     ALT_PREVIOUS_JRE_FILE=     PREVIOUS_RELEASE_IMAGE=/Library/Java/JavaVirtualMachines/jdk1.7.0_04.jdk/Contents/Home     ALT_PREVIOUS_RELEASE_IMAGE=     Sanity check passed.     Makefile  的  Sanity  查抄过程输出了编译所需的所有环境变量,假如看到  “Sanity     check     passed.”  ,说明查抄过程通过了,可以输入  “make”  执行整个  OpenJDK  编译(  make  不加参数,     默认编译  make all  ),笔者利用  Core i7 3720QM/16GB RAM  的  MacBook  呆板,启动  6  条编译线     程,全量编译整个  OpenJDK  大概需  20  分钟,编译结束后,将输出雷同下面的日记清单所示内     容。假如读者之前已经全量编译过,只修改了少量文件,增量编译可以在数十秒内完成。     #--Build times----------     Target all_product_build     Start 2012-12-13 17  :  12  :  19     End 2012-12-13 17  :  31  :  07     00  :  01  :  19 corba     00  :  01  :  15 hotspot     00  :  00  :  14 jaxp     00  :  7  :  21 jaxws     00  :  8  :  11 jdk     00  :  00  :  28 langtools     00  :  18  :  48 TOTAL     -------------------------     编译完成之后,进入  OpenJDK  源码下的  build/j2sdk-image  目次(大概  build-debug  、  build    fastdebug  这两个目次),这是整个  JDK  的完备编译结果,复制到  JAVA_HOME  目次,就可以     作为一个完备的  JDK  利用,编译出来的假造机,在  -version  命令中带有用户的呆板名。     >  ./java-version     openjdk version"1.7.0-internal-fastdebug"     O p e n J D K R u n t i m e E n v i r o n m e n t  (  b u i l d 1.7.0-i n t e r n a l-f a s t d e b u g-icyfenix_2012_12_24_15_57-b00  )     OpenJDK 64-Bit Server VM  (  build 23.0-b21-fastdebug,mixed mode  )     在大多数时候,假如我们并不关心  JDK  中  HotSpot  假造机以外的内容,只想单独编译     HotSpot  假造机的话(比方调试假造机时,每次改动程序都执行整个  OpenJDK  的  Makefile  ,速     度肯定受不了),那么利用  hotspot/make  目次下的  Makefile  举行替换即可,其他参数设置与前     面是同等的,这时候假造机的输出结果存放在  build/hotspot/outputdir/bsd_amd64_compiler2  目     录    [1]    中,进入后可以见到以下几个目次。     0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17  :  24 debug     0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17  :  24 fastdebug     0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17  :  25 generated     0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17  :  24 jvmg     0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17  :  24 optimized  0 drwxr-xr-x 584 IcyFenix staff 19K 12 13 17  :  25 product     0 drwxr-xr-x 15 IcyFenix staff 510B 12 13 17  :  24 profiled     这些目次对应了不同的优化级别,优化级别越高,性能自然就越好,但是输出代码与源     码的差距就越大,难于调试,具体哪个目次有内容,取决于  make  命令后面的参数。     在编译结束之后、运行假造机之前,还要手工编辑目次下的  env.sh  文件,这个文件由编     译脚本自动产生,用于设置假造机的环境变量,里面已经发布了  “JAVA_HOME  、     CLASSPATH  、  HOTSPOT_BUILD_USER”3  个环境变量,还须要增长一     个  “LD_LIBRARY_PATH”  ,内容如下:     LD_LIBRARY_PATH=.  :  ${JAVA_HOME}/jre/lib/amd64/native_threads  :  ${JAVA_HOME}/jre/lib/amd64  :     export LD_LIBRARY_PATH     然后执行以下命令启动假造机(这时的启动器名为  gamma  ),输出版本号。     ../env.sh     ./gamma-version     Using java runtime at  :  /Library/Java/JavaVirtualMachines/jdk1.7.0_04.jdk/Contents/Home/jre     java version"1.7.0_04"     Java  (  TM  )  SE Runtime Environment  (  build 1.7.0_04-b21  )     OpenJDK 64-Bit Server VM  (  build 23.0-b21  ,  mixed mode  )     看到本身编译的假造机成功运行起来,很有成就感吧!     [1]  在不同呆板上,最后一个目次名称会有所差别,  bsd  体现  Mac     OS  系统(内核为     FreeBSD  ),  amd64  体现是  64  位  JDK  (  32  位是  x86  ),  compiler2  体现是  Server VM  (  Client VM  表     示是  compiler1  )。  1.6.5   在  IDE  工具中举行源码调试     在阅读  OpenJDK  源码的过程中,常常须要运行、调试程序来资助理解。我们现在已经可     以编译出一个调试版本  HotSpot  假造机,禁用优化,并带有符号信息,这样就可以利用  GDB     来举行调试了。据笔者了解,很多对假造机了解比较深的开发人员确实就是直接利用  GDB  加     VIM  编辑器来开发、修改  HotSpot  的,不外相信大部分读者更倾向于在  IDE  环境而不是纯文本     的  GDB  下阅读、跟踪  HotSpot  源码,因此这节就简朴介绍一下  “  如安在  IDE  中举行  HotSpot  源码     调试  ”  。     首先,到  NetBeans  网站(  http://netbeans.org/  )上下载最新版的  NetBeans  ,下载时选择支持     C/C++  开发的那个版本。安装后,新建一个项目,选择  “  基于现有源代码的  C/C++  项目  ”  ,在     源码文件夹中填入  OpenJDK  目次下  hotspot  目次的路径,在下面的单选按钮中选择  “  定制  ”  ,如     图  1-8  所示,然后单击  “  下一步  ”  按钮。     图   1-8   在  NetBeans  中创建  HotSpot  项目(  1  )     接着,在  “  指定构建代码的方法  ”  中选择  “  利用现有的  makefile”  ,并填入  Makefile  文件的路     径(在  hotspot/make  目次下),如图  1-9  所示。单击  “  下一步  ”  按钮,将  “  构建命令  ”  修改为以下  内容:     ${MAKE}-f Makefile clean jvmg     ALT_BOOTDIR=/Library/Java/JavaVirtualMachines/jdk1.7.0_04.jdk/Contents/Home ARCH_DATA_MODEL=64 LANG=C     图   1-9   在  NetBeans  中创建  HotSpot  项目(  2  )     OpenJDK     7u4  源码  Makefile  在终端运行时能正确获取到系统指令集架构为  64  位,但在     NetBeans  中却没有取得正确的值,误以为是  32  位,因此这里必须利用  ARCH_DATA_MODEL     参数明白指定为  64  位。另外两个参数  ALT_BOOTDIR  和  LANG  的作用前面已经介绍过。单     击  “  完成  ”  按钮,  HotSpot  项目就这样导入到  NetBeans  中了。     不外,这时候  HotSpot  还运行不起来,由于  NetBeans  根本不知道编译出来的结果放在哪     里、哪个程序是假造机的入口等,这些内容都须要明白告知  NetBeans  。在  HotSpot  工程上单击     右键,在弹出的快捷菜单中选择  “  属性  ”  ,在弹出的对话框中找到  “  运行  ”  选项,设置运行命令     为:     /Users/IcyFenix/Develop/JVM/jdkBuild/openjdk_7u4/hotspot/build/bsd/bsd_amd64_compiler2/jvmg/gamma Queens  上面的  Queens  是  Makefile  脚本自动产生的一段解八皇后标题标  Java  程序,用于测试假造     机,这里笔者直接拿来用了,读者完全可以将它替换为本身的  Java  程序。     读者在调试  Java  代码执行时,假如要跟踪具体  Java  代码在假造机中是如何执行的,大概     会觉得无从动手,由于目前在  HotSpot  主流的操作系统上,都采用模板解释器来执行字节     码,它与  JIT  编译器一样,最终执行的汇编代码都是运行期间产生的,无法直接设置断点,     以是  HotSpot  增长了以下参数来方便开发人员调试解释器。     -XX  :  +TraceBytecodes-XX  :  StopInterpreterAt=  <  n  >     这组参数的作用是当遇到序号为<  n  >的字节码指令时,便会制止程序执行,进入断点     调试。在调试解释器部分代码时,把这两个参数加到  gamma  后面即可。     最后,还须要在  “  环境  ”  窗口中设置环境变量,也就是前面  env.sh  脚本所设置的那几个环     境变量,如图  1-10  所示。     图   1-10   在  NetBeans  中创建  HotSpot  项目(  3  )     完成以上配置之后,一个可修改、编译、调试的  HotSpot  工程就完全建立起来了,启动  器的执行入口是  java.c  的  main  ()方法,读者可以设置断点单步跟踪,如图  1-11  所示。     图   1-11   在  NetBeans  中创建  HotSpot  项目(  4  )     由于  HotSpot  的源码比较长,  C/C++  文件数量也很多,为了便于读者阅读,以是代码清单     1-3  给出了各个目次中代码的重要用途,供读者参考。     代码清单  1-3 HotSpot  源码结构    [1]  [1]  该目次结构由  RednaxelaFX  整理:  http://hllvm.group.iteye.com/group/topic/26998  。  1.7   本章小结     本章介绍了  Java  技术体系的过去、现在以及未来的一些发展趋势,并通过实战介绍了如     何本身来独立编译一个  OpenJDK 7  。作为全书的弁言部分,本章建立了后文研究所必需的环     境。在了解  Java  技术的来龙去脉后,后面章节将分为  4  部分去介绍  Java  在内存管理、  Class  文     件结构与执行引擎、编译器优化及多线程并发方面的实现原理。  第二部分 自动内存管理机制     第  2  章   Java  内存区域与内存溢出异常     第  3  章 垃圾收集器与内存分配计谋     第  4  章 假造机性能监控与故障处理惩罚工具     第  5  章 调优案例分析与实战  第  2  章   Java  内存区域与内存溢出异常     Java  与  C++  之间有一堵由内存动态分配和垃圾收集技术所围成的  “  高墙  ”  ,墙表面的人想     进去,墙里面的人却想出来。     2.1   概述     对于从事  C  、  C++  程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权力     的  “  皇帝  ”  又是从事最基础工作的  “  劳动人民  ”——  既拥有每一个对象的  “  所有权  ”  ,又担负着每     一个对象生命开始到终结的维护责任。     对于  Java  程序员来说,在假造机自动内存管理机制的资助下,不再须要为每一个  new  操     作去写配对的  delete/free  代码,不轻易出现内存泄漏和内存溢出标题,由假造机管理内存这     统统看起来都很美好。不外,也正是由于  Java  程序员把内存控制的权力交给了  Java  假造机,     一旦出现内存泄漏和溢出方面的标题,假如不了解假造机是怎样利用内存的,那么排查错误     将会成为一项异常艰难的工作。     本章是第二部分的第  1  章,笔者将从概念上介绍  Java  假造机内存的各个区域,讲解这些     区域的作用、服务对象以及此中可能产生的标题,这是翻越假造机内存管理这堵围墙的第一     步。  2.2   运行时数据区域     Java  假造机在执行  Java  程序的过程中会把它所管理的内存划分为若干个不同的数据区     域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着假造机历程的启动而     存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《  Java  假造机规范(  Java     SE 7  版)》的规定,  Java  假造机所管理的内存将会包罗以下几个运行时数据区域,如图  2-1  所     示。     图   2-1 Java  假造机运行时数据区     2.2.1   程序计数器     程序计数器(  Program Counter Register  )是一块较小的内存空间,它可以看作是当前线     程所执行的字节码的行号指示器。在假造机的概念模型里(仅是概念模型,各种假造机可能     会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选     取下一条须要执行的字节码指令,分支、循环、跳转、异常处理惩罚、线程规复等基础功能都需     要依赖这个计数器来完成。     由于  Java  假造机的多线程是通过线程轮番切换并分配处理惩罚器执行时间的方式来实现的,     在任何一个确定的时刻,一个处理惩罚器(对于多核处理惩罚器来说是一个内核)都只会执行一条线     程中的指令。因此,为了线程切换后能规复到正确的执行位置,每条线程都须要有一个独立     的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为  “  线程私     有  ”  的内存。     假如线程正在执行的是一个  Java  方法,这个计数器记录的是正在执行的假造机字节码指     令的所在;假如正在执行的是  Native  方法,这个计数器值则为空(  Undefined  )。此内存区域     是唯逐一个在  Java  假造机规范中没有规定任何  OutOfMemoryError  环境的区域。  2.2.2 Java  假造机栈     与程序计数器一样,  Java  假造机栈(  Java Virtual Machine Stacks  )也是线程私有的,它的     生命周期与线程雷同。假造机栈描述的是  Java  方法执行的内存模型:每个方法在执行的同时     都会创建一个栈帧(  Stack Frame     [1]    )用于存储局部变量表、操作数栈、动态链接、方法出口     等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在假造机栈中入栈到出     栈的过程。     常常有人把  Java  内存区分为堆内存(  Heap  )和栈内存(  Stack  ),这种分法比较粗     糙,  Java  内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最     关注的、与对象内存分配关系最密切的内存区域是这两块。此中所指的  “  堆  ”  笔者在后面会专     门讲述,而所指的  “  栈  ”  就是现在讲的假造机栈,大概说是假造机栈中局部变量表部分。     局部变量表存放了编译期可知的各种基本数据范例(  boolean  、  byte  、  char  、  short  、  int  、     float  、  long  、  double  )、对象引用(  reference  范例,它不等同于对象本身,可能是一个指向对     象起始所在的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相干的位置)和     returnAddress  范例(指向了一条字节码指令的所在)。     此中  64  位长度的  long  和  double  范例的数据会占用  2  个局部变量空间(  Slot  ),别的的数据     范例只占用  1  个。局部变量表所需的内存空间在编译期间完身分配,当进入一个方法时,这     个方法须要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变     量表的巨细。     在  Java  假造机规范中,对这个区域规定了两种异常状况:假如线程请求的栈深度大于虚     拟机所答应的深度,将抛出  StackOverflowError  异常;假如假造机栈可以动态扩展(当前大部     分的  Java  假造机都可动态扩展,只不外  Java  假造机规范中也答应固定长度的假造机栈),如     果扩展时无法申请到充足的内存,就会抛出  OutOfMemoryError  异常。     [1]  栈帧是方法运行时的基础数据结构,在本书的第  8  章中会对帧举行具体讲解。  2.2.3   当地方法栈     当地方法栈(  Native Method Stack  )与假造机栈所发挥的作用黑白常相似的,它们之间     的区别不外是假造机栈为假造机执行  Java  方法(也就是字节码)服务,而当地方法栈则为虚     拟机利用到的  Native  方法服务。在假造机规范中对当地方法栈中方法利用的语言、利用方式     与数据结构并没有逼迫规定,因此具体的假造机可以自由实现它。乃至有的假造机(譬如     Sun HotSpot  假造机)直接就把当地方法栈和假造机栈合二为一。与假造机栈一样,当地方法     栈区域也会抛出  StackOverflowError  和  OutOfMemoryError  异常。  2.2.4 Java  堆     对于大多数应用来说,  Java  堆(  Java Heap  )是  Java  假造机所管理的内存中最大的一块。     Java  堆是被所有线程共享的一块内存区域,在假造机启动时创建。此内存区域的唯一目标就     是存放对象实例,险些所有的对象实例都在这里分配内存。这一点在  Java  假造机规范中的描     述是:所有的对象实例以及数组都要在堆上分配    [1]    ,但是随着  JIT  编译器的发展与逃逸分析技     术逐渐成熟,栈上分配、标量替换    [2]    优化技术将会导致一些微妙的厘革发生,所有的对象都     分配在堆上也徐徐变得不是那么  “  绝对  ”  了。     Java  堆是垃圾收集器管理的重要区域,因此很多时候也被称做  “GC  堆  ”  (  Garbage     Collected Heap  ,幸好国内没翻译成  “  垃圾堆  ”  )。从内存接纳的角度来看,由于现在收集器基     本都采用分代收集算法,以是  Java  堆中还可以细分为:新生代和老年代;再细致一点的有     Eden  空间、  From Survivor  空间、  To Survivor  空间等。从内存分配的角度来看,线程共享的     Java  堆中可能划分出多个线程私有的分配缓冲区(  Thread Local Allocation Buffer,TLAB  )。不     过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划     分的目标是为了更好地接纳内存,大概更快地分配内存。在本章中,我们仅仅针对内存区域     的作用举行讨论,  Java  堆中的上述各个区域的分配、接纳等细节将是第  3  章的主题。     根据  Java  假造机规范的规定,  Java  堆可以处于物理上不连续的内存空间中,只要逻辑上     是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定巨细的,也可以是     可扩展的,不外当前主流的假造机都是按照可扩展来实现的(通过  -Xmx  和  -Xms  控制)。如     果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出  OutOfMemoryError  异     常。     [1]  Java  假造机规范中的原文:  The heap is the runtime data area from which memory for all class     instances and arrays is allocated  。     [2]  逃逸分析与标量替换的相干内容,拜见第  11  章相干内容。  2.2.5   方法区     方法区(  Method Area  )与  Java  堆一样,是各个线程共享的内存区域,它用于存储已被虚     拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然  Java  假造机规     范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做  Non-Heap  (非堆),目标应     该是与  Java  堆区分开来。     对于风俗在  HotSpot  假造机上开发、部署程序的开发者来说,很多人都更愿意把方法区     称为  “  永世代  ”  (  Permanent Generation  ),本质上两者并不等价,仅仅是由于  HotSpot  假造机的     设计团队选择把  GC  分代收集扩展至方法区,大概说利用永世代来实现方法区而已,这样     HotSpot  的垃圾收集器可以像管理  Java  堆一样管理这部分内存,可以或许省去专门为方法区编写内     存管理代码的工作。对于其他假造机(如  BEA JRockit  、  IBM J9  等)来说是不存在永世代的概     念的。原则上,如何实现方法区属于假造机实现细节,不受假造机规范束缚,但利用永世代     来实现方法区,现在看来并不是一个好主意,由于这样更轻易遇到内存溢出标题(永世代     有  -XX  :  MaxPermSize  的上限,  J9  和  JRockit  只要没有触碰到历程可用内存的上限,比方  32  位系     统中的  4GB  ,就不会出现标题),而且有少少数方法(比方  String.intern  ())会因这个缘故原由     导致不同假造机下有不同的体现。因此,对于  HotSpot  假造机,根据官方发布的路线图信     息,现在也有放弃永世代并逐步改为采用  Native Memory  来实现方法区的规划了    [1]    ,在目前已     经发布的  JDK 1.7  的  HotSpot  中,已经把原来放在永世代的字符串常量池移出。     Java  假造机规范对方法区的限定非常宽松,除了和  Java  堆一样不须要连续的内存和可以     选择固定巨细大概可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个     区域是比较少出现的,但并非数据进入了方法区就如永世代的名字一样  “  永世  ”  存在了。这区     域的内存接纳目标重要是针对常量池的接纳和对范例的卸载,一般来说,这个区域的回     收  “  成绩  ”  比较难以令人满意,尤其是范例的卸载,条件相称苛刻,但是这部分区域的接纳确     实是须要的。在  Sun  公司的  BUG  列表中,曾出现过的若干个严重的  BUG  就是由于低版本的     HotSpot  假造机对此区域未完全接纳而导致内存泄漏。     根据  Java  假造机规范的规定,当方法区无法满足内存分配需求时,将抛出     OutOfMemoryError  异常。     [1]  JEP 122-Remove the Permanent Generation  :  http://openjdk.java.net/jeps/122  。  2.2.6   运行时常量池     运行时常量池(  Runtime Constant Pool  )是方法区的一部分。  Class  文件中除了有类的版     本、字段、方法、接口等描述信息外,尚有一项信息是常量池(  Constant Pool Table  ),用于     存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常     量池中存放。     Java  假造机对  Class  文件每一部分(自然也包罗常量池)的格式都有严格规定,每一个字     节用于存储哪种数据都必须符合规范上的要求才会被假造机认可、装载和执行,但对于运行     时常量池,  Java  假造机规范没有做任何细节的要求,不同的提供商实现的假造机可以按照自     己的须要来实现这个内存区域。不外,一般来说,除了保存  Class  文件中描述的符号引用外,     还会把翻译出来的直接引用也存储在运行时常量池中    [1]    。     运行时常量池相对于  Class  文件常量池的另外一个重要特性是具备动态性,  Java  语言并不     要求常量一定只有编译期才能产生,也就是并非预置入  Class  文件中常量池的内容才能进入方     法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较     多的便是  String  类的  intern  ()方法。     既然运行时常量池是方法区的一部分,自然受到方法区内存的限定,当常量池无法再申     请到内存时会抛出  OutOfMemoryError  异常。     [1]  关于  Class  文件格式和符号引用等概念可拜见第  6  章。  2.2.7   直接内存     直接内存(  Direct Memory  )并不是假造机运行时数据区的一部分,也不是  Java  假造机规     范中定义的内存区域。但是这部分内存也被频仍地利用,而且也可能导致  OutOfMemoryError     异常出现,以是我们放到这里一起讲解。     在  JDK 1.4  中新到场了  NIO  (  New Input/Output  )类,引入了一种基于通道(  Channel  )与缓     冲区(  Buffer  )的  I/O  方式,它可以利用  Native  函数库直接分配堆外内存,然后通过一个存储     在  Java  堆中的  DirectByteBuffer  对象作为这块内存的引用举行操作。这样能在一些场景中显著     提高性能,由于制止了在  Java  堆和  Native  堆中来回复制数据。     显然,本机直接内存的分配不会受到  Java  堆巨细的限定,但是,既然是内存,肯定还是     会受到本机总内存(包罗  RAM  以及  SWAP  区大概分页文件)巨细以及处理惩罚器寻址空间的限     制。服务器管理员在配置假造机参数时,会根据实际内存设置  -Xmx  等参数信息,但常常忽略     直接内存,使得各个内存区域总和大于物理内存限定(包罗物理的和操作系统级的限定),     从而导致动态扩展时出现  OutOfMemoryError  异常。  2.3 HotSpot  假造机对象探秘     介绍完  Java  假造机的运行时数据区之后,我们大抵知道了假造机内存的概况,读者了解     了内存中放了些什么后,大概就会想更进一步了解这些假造机内存中的数据的其他细节,譬     如它们是如何创建、如何布局以及如何访问的。对于这样涉及细节的标题,必须把讨论范围     限定在具体的假造机和会合在某一个内存区域上才有意义。基于实用优先的原则,笔者以常     用的假造机  HotSpot  和常用的内存区域  Java  堆为例,深入探讨  HotSpot  假造机在  Java  堆中对象分     配、布局和访问的全过程。     2.3.1   对象的创建     Java  是一门面向对象的编程语言,在  Java  程序运行过程中无时无刻都有对象被创建出     来。在语言层面上,创建对象(比方克隆、反序列化)通常仅仅是一个  new  关键字而已,而     在假造机中,对象(文中讨论的对象限于普通  Java  对象,不包罗数组和  Class  对象等)的创建     又是怎样一个过程呢?     假造机遇到一条  new  指令时,首先将去查抄这个指令的参数是否能在常量池中定位到一     个类的符号引用,并且查抄这个符号引用代表的类是否已被加载、剖析和初始化过。假如没     有,那必须先执行相应的类加载过程,本书第  7  章将探讨这部分内容的细节。     在类加载查抄通事后,接下来假造机将为新生对象分配内存。对象所需内存的巨细在类     加载完成后便可完全确定(如何确定将在  2.3.2  节中介绍),为对象分配空间的任务等同于把     一块确定巨细的内存从  Java  堆中划分出来。假设  Java  堆中内存是绝对规整的,所有用过的内     存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配     内存就仅仅是把那个指针向空闲空间那里挪动一段与对象巨细相等的距离,这种分配方式称     为  “  指针碰撞  ”  (  Bump the Pointer  )。假如  Java  堆中的内存并不是规整的,已利用的内存和空     闲的内存相互交错,那就没有办法简朴地举行指针碰撞了,假造机就必须维护一个列表,记     录上哪些内存块是可用的,在分配的时候从列表中找到一块充足大的空间划分给对象实例,     并更新列表上的记录,这种分配方式称为  “  空闲列表  ”  (  Free     List  )。选择哪种分配方式由     Java  堆是否规整决定,而  Java  堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决     定。因此,在利用  Serial  、  ParNew  等带  Compact  过程的收集器时,系统采用的分配算法是指针     碰撞,而利用  CMS  这种基于  Mark-Sweep  算法的收集器时,通常采用空闲列表。     除如何划分可用空间之外,尚有另外一个须要考虑的标题是对象创建在假造机中黑白常     频仍的行为,纵然是仅仅修改一个指针所指向的位置,在并发环境下也并不是线程安全的,     可能出现正在给对象  A  分配内存,指针还没来得及修改,对象  B  又同时利用了原来的指针来     分配内存的环境。办理这个标题有两种方案,一种是对分配内存空间的动作举行同步处理惩罚     ——  实际上假造机采用  CAS  配上失败重试的方式包管更新操作的原子性;另一种是把内存分     配的动作按照线程划分在不同的空间之中举行,即每个线程在  Java  堆中预先分配一小块内     存,称为当地线程分配缓冲(  Thread Local Allocation Buffer,TLAB  )。哪个线程要分配内     存,就在哪个线程的  TLAB  上分配,只有  TLAB  用完并分配新的  TLAB  时,才须要同步锁定。     假造机是否利用  TLAB  ,可以通过  -XX  :  +/-UseTLAB  参数来设定。     内存分配完成后,假造机须要将分配到的内存空间都初始化为零值(不包罗对象头),     假如利用  TLAB  ,这一工作过程也可以提前至  TLAB  分配时举行。这一步操作包管了对象的实  例字段在  Java  代码中可以不赋初始值就直接利用,程序能访问到这些字段的数据范例所对应     的零值。     接下来,假造机要对对象举行须要的设置,比方这个对象是哪个类的实例、如何才能找     到类的元数据信息、对象的哈希码、对象的  GC  分代年龄等信息。这些信息存放在对象的对     象头(  Object Header  )之中。根据假造机当前的运行状态的不同,如是否启用偏向锁等,对     象头会有不同的设置方式。关于对象头的具体内容,稍后再做具体介绍。     在上面工作都完成之后,从假造机的视角来看,一个新的对象已经产生了,但从  Java  程     序的视角来看,对象创建才刚刚开始  ——  <  init  >方法还没有执行,所有的字段都还为零。     以是,一般来说(由字节码中是否跟随  invokespecial  指令所决定),执行  new  指令之后会接着     执行<  init  >方法,把对象按照程序员的意愿举行初始化,这样一个真正可用的对象才算完     全产生出来。     下面的代码清单  2-1  是  HotSpot  假造机  bytecodeInterpreter.cpp  中的代码片段(这个解释器实     现很少有机会实际利用,由于大部分平台上都利用模板解释器;当代码通过  JIT  编译器执行     时差别就更大了。不外,这段代码用于了解  HotSpot  的运作过程是没有什么标题标)。     代码清单  2-1 HotSpot  解释器的代码片段     //  确保常量池中存放的是已解释的类     if  (!  constants-  >  tag    _    at  (  index  )  .is    _    unresolved    _    klass  ())  {     //  断言确保是  klassOop  和  instanceKlassOop  (这部分下一节介绍)     oop entry=  (  klassOop  )  *constants-  >  obj_at_addr  (  index  );     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  ();     //  确保对象所属范例已经颠末初始化阶段     if  (  ik-  >  is_initialized  ()&&  ik-  >  can_be_fastpath_allocated  ())     {     //  取对象长度     size_t obj_size=ik-  >  size_helper  ();     oop result=NULL  ;     //  记录是否须要将对象所有字段置零值     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  ;     /*cmpxchg  是  x86  中的  CAS  指令,这里是一个  C++  方法,通过  CAS  方式分配空间,假如并发失败,     转到  retry  中重试,直至成功分配为止  */     if  (  new_top  <  =*Universe  :  heap  ()  -  >  end_addr  ())  {     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  );     UPDATE_PC_AND_TOS_AND_CONTINUE  (  3  ,  1  );     }     }     }  2.3.2   对象的内存布局     在  HotSpot  假造机中,对象在内存中存储的布局可以分为  3  块区域:对象头(  Header  )、     实例数据(  Instance Data  )和对齐添补(  Padding  )。     HotSpot  假造机的对象头包罗两部分信息,第一部分用于存储对象自身的运行时数据,     如哈希码(  HashCode  )、  GC  分代年龄、锁状态标志、线程持有的锁、偏向线程  ID  、偏向时     间戳等,这部分数据的长度在  32  位和  64  位的假造机(未开启压缩指针)中分别为  32bit  和     64bit  ,官方称它为  “Mark Word”  。对象须要存储的运行时数据很多,实在已经超出了  32  位、     64  位  Bitmap  结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储     本钱,考虑到假造机的空间服从,  Mark Word  被设计成一个非固定的数据结构以便在极小的     空间内存储只管多的信息,它会根据对象的状态复用本身的存储空间。比方,在  32  位的     HotSpot  假造机中,假如对象处于未被锁定的状态下,那么  Mark Word  的  32bit  空间中的  25bit  用     于存储对象哈希码,  4bit  用于存储对象分代年龄,  2bit  用于存储锁标志位,  1bit  固定为  0  ,而在     其他状态(轻量级锁定、重量级锁定、  GC  标记、可偏向)下对象的存储内容见表  2-1  。     对象头的另外一部分是范例指针,即对象指向它的类元数据的指针,假造机通过这个指     针来确定这个对象是哪个类的实例。并不是所有的假造机实现都必须在对象数据上保留范例     指针,换句话说,查找对象的元数据信息并不一定要颠末对象本身,这点将在  2.3.3  节讨论。     另外,假如对象是一个  Java  数组,那在对象头中还必须有一块用于记录数组长度的数据,因     为假造机可以通过普通  Java  对象的元数据信息确定  Java  对象的巨细,但是从数组的元数据中     却无法确定数组的巨细。     代码清单  2-2  为  HotSpot  假造机  markOop.cpp  中的代码(注释)片段,它描述了  32bit  下  Mark     Word  的存储状态。     代码清单  2-2 markOop.cpp  片段     //Bit-format of an object header  (  most significant first,big endian layout below  ):     //32 bits  :     //--------     //hash  :  25------------  >  |age  :  4 biased_lock  :  1 lock  :  2  (  normal object  )     //JavaThread*  :  23 epoch  :  2 age  :  4 biased_lock  :  1 lock  :  2  (  biased object  )     //size  :  32------------------------------------------  >  |  (  CMS free block  )     //PromotedObject*  :  29----------  >  |promo_bits  :  3-----  >  |  (  CMS promoted object  )     接下来的实例数据部分是对象真正存储的有用信息,也是在程序代码中所定义的各种类     型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都须要记录起来。这部分  的存储顺序会受到假造机分配计谋参数(  FieldsAllocationStyle  )和字段在  Java  源码中定义顺     序的影响。  HotSpot  假造机默认的分配计谋为  longs/doubles  、  ints  、  shorts/chars  、     bytes/booleans  、  oops  (  Ordinary Object Pointers  ),从分配计谋中可以看出,雷同宽度的字段     总是被分配到一起。在满足这个条件条件的环境下,在父类中定义的变量会出现在子类之     前。假如  CompactFields  参数值为  true  (默以为  true  ),那么子类之中较窄的变量也可能会插入     到父类变量的空隙之中。     第三部分对齐添补并不是必然存在的,也没有特殊的含义,它仅仅起着占位符的作用。     由于  HotSpot VM  的自动内存管理系统要求对象起始所在必须是  8  字节的整数倍,换句话说,     就是对象的巨细必须是  8  字节的整数倍。而对象头部分正好是  8  字节的倍数(  1  倍大概  2  倍),     因此,当对象实例数据部分没有对齐时,就须要通过对齐添补来补全。  2.3.3   对象的访问定位     建立对象是为了利用对象,我们的  Java  程序须要通过栈上的  reference  数据来操作堆上的     具体对象。由于  reference  范例在  Java  假造机规范中只规定了一个指向对象的引用,并没有定     义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,以是对象访问方式也是     取决于假造机实现而定的。目前主流的访问方式有利用句柄和直接指针两种。     假如利用句柄访问的话,那么  Java  堆中将会划分出一块内存来作为句柄池,  reference  中     存储的就是对象的句柄所在,而句柄中包罗了对象实例数据与范例数据各自的具体所在信     息,如图  2-2  所示。     图   2-2   通过句柄访问对象     假如利用直接指针访问,那么  Java  堆对象的布局中就必须考虑如何放置访问范例数据的     相干信息,而  reference  中存储的直接就是对象所在,如图  2-3  所示。  图   2-3   通过直接指针访问对象     这两种对象访问方式各有优势,利用句柄来访问的最大好处就是  reference  中存储的是稳     定的句柄所在,在对象被移动(垃圾收集时移动对象黑白常普遍的行为)时只会改变句柄中     的实例数据指针,而  reference  本身不须要修改。     利用直接指针访问方式的最大好处就是速率更快,它节省了一次指针定位的时间开销,     由于对象的访问在  Java  中非常频仍,因此这类开销集腋成裘后也是一项非常可观的执行成     本。就本书讨论的重要假造机  Sun HotSpot  而言,它是利用第二种方式举行对象访问的,但从     整个软件开发的范围来看,各种语言和框架利用句柄来访问的环境也非常常见。  2.4   实战:  OutOfMemoryError  异常     在  Java  假造机规范的描述中,除了程序计数器外,假造机内存的其他几个运行时区域都     有发生  OutOfMemoryError  (下文称  OOM  )异常的可能,本节将通过若干实例来验证异常发生     的场景(代码清单  2-3  ~代码清单  2-9  的几段简朴代码),并且会初步介绍几个与内存相干的     最基本的假造机参数。     本节内容的目标有两个:第一,通过代码验证  Java  假造机规范中描述的各个运行时区域     存储的内容;第二,盼望读者在工作中遇到实际的内存溢出异常时,能根据异常的信息快速     判定是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这     些异常后该如何处理惩罚。     下文代码的开头都注释了执行时所须要设置的假造机启动参数(注释中  “VM Args”  后面     跟着的参数),这些参数对实验的结果有直接影响,读者调试代码的时候千万不要忽略。如     果读者利用控制台命令来执行程序,那直接跟在  Java  命令之后誊写就可以。假如读者利用     Eclipse IDE  ,则可以参考图  2-4  在  Debug/Run  页签中的设置。     图   2-4   在  Eclipse  的  Debug  页签中设置假造机参数  下文的代码都是基于  Sun  公司的  HotSpot  假造机运行的,对于不同公司的不同版本的假造     机,参数和程序运行的结果可能会有所差别。     2.4.1 Java  堆溢出     Java  堆用于存储对象实例,只要不断地创建对象,并且包管  GC Roots  到对象之间有可达     路径来制止垃圾接纳机制清除这些对象,那么在对象数量到达最大堆的容量限定后就会产生     内存溢出异常。     代码清单  2-3  中代码限定  Java  堆的巨细为  20MB  ,不可扩展(将堆的最小值  -Xms  参数与最     大值  -Xmx  参数设置为一样即可制止堆自动扩展),通过参数  -XX  :     +HeapDumpOnOutOfMemoryError  可以让假造机在出现内存溢出异常时  Dump  出当前的内存堆     转储快照以便事后举行分析    [1]    。     代码清单  2-3 Java  堆内存溢出异常测试     /**     *VM Args  :  -Xms20m-Xmx20m-XX  :  +HeapDumpOnOutOfMemoryError     *@author zzm     */     public class HeapOOM{     static class OOMObject{     }     public static void main  (  String[]args  )  {     List  <  OOMObject  >  list=new ArrayList  <  OOMObject  >();     while  (  true  )  {     list.add  (  new OOMObject  ());     }     }     }     运行结果:     java.lang.OutOfMemoryError  :  Java heap space     Dumping heap to java_pid3404.hprof……     Heap dump file created[22045981 bytes in 0.663 secs]     Java  堆内存的  OOM  异常是实际应用中常见的内存溢出异常环境。当出现  Java  堆内存溢出     时,异常堆栈信息  “java.lang.OutOfMemoryError”  会跟着进一步提示  “Java heap space”  。     要办理这个区域的异常,一般的本领是先通过内存映像分析工具(如  Eclipse     Memory     Analyzer  )对  Dump  出来的堆转储快照举行分析,重点是确认内存中的对象是否是须要的,也     就是要先分清楚到底是出现了内存泄漏(  Memory     Leak  )还是内存溢出(  Memory     Overflow  )。图  2-5  显示了利用  Eclipse Memory Analyzer  打开的堆转储快照文件。  图   2-5   利用  Eclipse Memory Analyzer  打开的堆转储快照文件     假如是内存泄露,可进一步通过工具检察泄露对象到  GC Roots  的引用链。于是就能找到     泄露对象是通过怎样的路径与  GC Roots  相干联并导致垃圾收集器无法自动接纳它们的。掌握     了泄露对象的范例信息及  GC     Roots  引用链的信息,就可以比较正确地定位出泄露代码的位     置。     假如不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当查抄虚     拟机的堆参数(  -Xmx  与  -Xms  ),与呆板物理内存对比看是否还可以调大,从代码上查抄是     否存在某些对象生命周期过长、持有状态时间过长的环境,实验减少程序运行期的内存消     耗。     以上是处理惩罚  Java  堆内存标题标简朴思绪,处理惩罚这些标题所须要的知识、工具与履历是后     面  3  章的主题。     [1]  关于堆转储快照文件分析方面的内容,可拜见第  4  章。  2.4.2   假造机栈和当地方法栈溢出     由于在  HotSpot  假造机中并不区分假造机栈和当地方法栈,因此,对于  HotSpot  来说,虽     然  -Xoss  参数(设置当地方法栈巨细)存在,但实际上是无效的,栈容量只由  -Xss  参数设定。     关于假造机栈和当地方法栈,在  Java  假造机规范中描述了两种异常:     假如线程请求的栈深度大于假造机所答应的最大深度,将抛出  StackOverflowError  异常。     假如假造机在扩展栈时无法申请到充足的内存空间,则抛出  OutOfMemoryError  异常。     这里把异常分成两种环境,看似更加严谨,但却存在着一些互相重叠的地方:当栈空间     无法继承分配时,到底是内存太小,还是已利用的栈空间太大,其本质上只是对同一件事情     的两种描述而已。     在笔者的实验中,将实验范围限定于单线程中的操作,实验了下面两种方法均无法让虚     拟机产生  OutOfMemoryError  异常,实验的结果都是获得  StackOverflowError  异常,测试代码如     代码清单  2-4  所示。     利用  -Xss  参数减少栈内存容量。结果:抛出  StackOverflowError  异常,异常出现时输出的     堆栈深度相应缩小。     定义了大量的当地变量,增大此方法帧中当地变量表的长度。结果:抛出     StackOverflowError  异常时输出的堆栈深度相应缩小。     代码清单  2-4   假造机栈和当地方法栈  OOM  测试(仅作为第  1  点测试程序)     /**     *VM Args  :  -Xss128k     *@author zzm     */     public class JavaVMStackSOF{     private int stackLength=1  ;     public void stackLeak  ()  {     stackLength++  ;     stackLeak  ();     }     public static void main  (  String[]args  )  throws Throwable{     JavaVMStackSOF oom=new JavaVMStackSOF  ();     try{     oom.stackLeak  ();     }catch  (  Throwable e  )  {     System.out.println  (  "stack length  :  "+oom.stackLength  );     throw e  ;     }     }     }     运行结果:     stack length  :  2402     Exception in thread"main"java.lang.StackOverflowError     at org.fenixsoft.oom.VMStackSOF.leak  (  VMStackSOF.java  :  20  )     at org.fenixsoft.oom.VMStackSOF.leak  (  VMStackSOF.java  :  21  )     at org.fenixsoft.oom.VMStackSOF.leak  (  VMStackSOF.java  :  21  )     ……  后续异常堆栈信息省略     实验结果表明:在单个线程下,无论是由于栈帧太大还是假造机栈容量太小,当内存无     法分配的时候,假造机抛出的都是  StackOverflowError  异常。     假如测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常,如     代码清单  2-5  所示。但是这样产生的内存溢出异常与栈空间是否充足大并不存在任何接洽,  大概正确地说,在这种环境下,为每个线程的栈分配的内存越大,反而越轻易产生内存溢出     异常。     实在缘故原由不难理解,操作系统分配给每个历程的内存是有限定的,譬如  32  位的  Windows     限定为  2GB  。假造机提供了参数来控制  Java  堆和方法区的这两部分内存的最大值。剩余的内     存为  2GB  (操作系统限定)减去  Xmx  (最大堆容量),再减去  MaxPermSize  (最大方法区容     量),程序计数器消耗内存很小,可以忽略掉。假如假造机历程本身耗费的内存不计算在     内,剩下的内存就由假造机栈和当地方法栈  “  瓜分  ”  了。每个线程分配到的栈容量越大,可以     建立的线程数量自然就越少,建立线程时就越轻易把剩下的内存耗尽。     这一点读者须要在开发多线程的应用时特殊注意,出现  StackOverflowError  异常时有错误     堆栈可以阅读,相对来说,比较轻易找到标题标所在。而且,假如利用假造机默认参数,栈     深度在大多数环境下(由于每个方法压入栈的帧巨细并不是一样的,以是只能说在大多数情     况下)到达  1000  ~  2000  完全没有标题,对于正常的方法调用(包罗递归),这个深度应该完     全够用了。但是,假如是建立过多线程导致的内存溢出,在不能减少线程数大概更换  64  位虚     拟机的环境下,就只能通过减少最大堆和减少栈容量来换取更多的线程。假如没有这方面的     处理惩罚履历,这种通过  “  减少内存  ”  的本领来办理内存溢出的方式会比较难以想到。     代码清单  2-5   创建线程导致内存溢出异常     /**     *VM Args  :  -Xss2M  (这时候不妨设置大些)     *@author zzm     */     public class JavaVMStackOOM{     private void dontStop  ()  {     while  (  true  )  {     }     }     public void stackLeakByThread  ()  {     while  (  true  )  {     Thread thread=new Thread  (  new Runnable  ()  {     @Override     public void run  ()  {     dontStop  ();     }     }  );     thread.start  ();     }     }     public static void main  (  String[]args  )  throws Throwable{     JavaVMStackOOM oom=new JavaVMStackOOM  ();     oom.stackLeakByThread  ();     }     }     注意 特殊提示一下,假如读者要实验运行上面这段代码,记得要先保存当前的工作。     由于在  Windows  平台的假造机中,  Java  的线程是映射到操作系统的内核线程上的    [1]    ,因此上述     代码执行时有较大的风险,可能会导致操作系统假死。     运行结果:     Exception in thread"main"java.lang.OutOfMemoryError  :  unable to create new native thread     [1]  关于假造机线程实现方面的内容可以参考本书第  12  章。  2.4.3   方法区和运行时常量池溢出     由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起举行。前     面提到  JDK 1.7  开始逐步  “  去永世代  ”  的事情,在此就以测试代码观察一下这件事对程序的实际     影响。     String.intern  ()是一个  Native  方法,它的作用是:假如字符串常量池中已经包罗一个等     于此  String  对象的字符串,则返回代表池中这个字符串的  String  对象;否则,将此  String  对象包     含的字符串添加到常量池中,并且返回此  String  对象的引用。在  JDK 1.6  及之前的版本中,由     于常量池分配在永世代内,我们可以通过  -XX  :  PermSize  和  -XX  :  MaxPermSize  限定方法区大     小,从而间接限定此中常量池的容量,如代码清单  2-6  所示。     代码清单  2-6   运行时常量池导致的内存溢出异常     /**     *VM Args  :  -XX  :  PermSize=10M-XX  :  MaxPermSize=10M     *@author zzm     */     public class RuntimeConstantPoolOOM{     public static void main  (  String[]args  )  {     //  利用  List  保持着常量池引用,制止  Full GC  接纳常量池行为     List  <  String  >  list=new ArrayList  <  String  >();     //10MB  的  PermSize  在  integer  范围内充足产生  OOM  了     int i=0  ;     while  (  true  )  {     list.add  (  String.valueOf  (  i++  )  .intern  ());     }     }     }     运行结果:     Exception in thread"main"java.lang.OutOfMemoryError  :  PermGen space     at java.lang.String.intern  (  Native Method  )     at org.fenixsoft.oom.RuntimeConstantPoolOOM.main  (  RuntimeConstantPoolOOM.java  :  18  )     从运行结果中可以看到,运行时常量池溢出,在  OutOfMemoryError  后面跟随的提示信息     是  “PermGen     space”  ,说明运行时常量池属于方法区(  HotSpot  假造机中的永世代)的一部     分。     而利用  JDK 1.7  运行这段程序就不会得到雷同的结果,  while  循环将一直举行下去。关于     这个字符串常量池的实现标题,还可以引申出一个更有意思的影响,如代码清单  2-7  所示。     代码清单  2-7 String.intern  ()返回引用的测试     public class RuntimeConstantPoolOOM{     public static void main  (  String[]args  )  {     public static void main  (  String[]args  )  {     String str1=new StringBuilder  (  "  计算机  "  )  .append  (  "  软件  "  )  .toString  ();     System.out.println  (  str1.intern  ()  ==str1  );     String str2=new StringBuilder  (  "ja"  )  .append  (  "va"  )  .toString  ();     System.out.println  (  str2.intern  ()  ==str2  );     }     }     }     这段代码在  JDK 1.6  中运行,会得到两个  false  ,而在  JDK 1.7  中运行,会得到一个  true  和一     个  false  。产生差别的缘故原由是:在  JDK 1.6  中,  intern  ()方法会把首次遇到的字符串实例复制     到永世代中,返回的也是永世代中这个字符串实例的引用,而由  StringBuilder  创建的字符串     实例在  Java  堆上,以是必然不是同一个引用,将返回  false  。而  JDK     1.7  (以及部分其他假造     机,比方  JRockit  )的  intern  ()实现不会再复制实例,只是在常量池中记录首次出现的实例  引用,因此  intern  ()返回的引用和由  StringBuilder  创建的那个字符串实例是同一个。对  str2  比     较返回  false  是由于  “java”  这个字符串在执行  StringBuilder.toString  ()之前已经出现过,字符串     常量池中已经有它的引用了,不符合  “  首次出现  ”  的原则,而  “  计算机软件  ”  这个字符串则是首     次出现的,因此返回  true  。     方法区用于存放  Class  的相干信息,如类名、访问修饰符、常量池、字段描述、方法描述     等。对于这些区域的测试,基本的思绪是运行时产生大量的类去填满方法区,直到溢出。虽     然直接利用  Java SE API  也可以动态产生类(如反射时的  GeneratedConstructorAccessor  和动态     署理等),但在本次实验中操作起来比较贫苦。在代码清单  2-8  中,笔者借助  CGLib     [1]    直接操     作字节码运行时生成了大量的动态类。     值得特殊注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经     常会出现在实际应用中:当前的很多主流框架,如  Spring  、  Hibernate  ,在对类举行增强时,     都会利用到  CGLib  这类字节码技术,增强的类越多,就须要越大的方法区来包管动态生成的     Class  可以加载入内存。另外,  JVM  上的动态语言(比方  Groovy  等)通常都会持续创建类来实     现语言的动态性,随着这类语言的流行,也越来越轻易遇到与代码清单  2-8  相似的溢进场     景。     代码清单  2-8   借助  CGLib  使方法区出现内存溢出异常     /**     *VM Args  :  -XX  :  PermSize=10M-XX  :  MaxPermSize=10M     *@author zzm     */     public class JavaMethodAreaOOM{     public static void main  (  String[]args  )  {     while  (  true  )  {     Enhancer enhancer=new Enhancer  ();     enhancer.setSuperclass  (  OOMObject.class  );     enhancer.setUseCache  (  false  );     enhancer.setCallback  (  new MethodInterceptor  ()  {     public Object intercept  (  Object obj,Method method,Object[]args,MethodProxy proxy  )  throws Throwable{     return proxy.invokeSuper  (  obj,args  );     }     }  );     enhancer.create  ();     }     }     static class OOMObject{     }     }     运行结果:     Caused by  :  java.lang.OutOfMemoryError  :  PermGen space     at java.lang.ClassLoader.defineClass1  (  Native Method  )     at java.lang.ClassLoader.defineClassCond  (  ClassLoader.java  :  632  )     at java.lang.ClassLoader.defineClass  (  ClassLoader.java  :  616  )     ……8 more     方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器接纳掉,判定条件是     比较苛刻的。在常常动态生成大量  Class  的应用中,须要特殊注意类的接纳状况。这类场景除     了上面提到的程序利用了  CGLib  字节码增强和动态语言之外,常见的尚有:大量  JSP  或动态产     生  JSP  文件的应用(  JSP  第一次运行时须要编译为  Java  类)、基于  OSGi  的应用(纵然是同一个     类文件,被不同的加载器加载也会视为不同的类)等。     [1]  CGLib  开源项目:  http://cglib.sourceforge.net/  。  2.4.4   本机直接内存溢出     DirectMemory  容量可通过  -XX  :  MaxDirectMemorySize  指定,假如不指定,则默认与  Java     堆最大值(  -Xmx  指定)一样,代码清单  2-9  越过了  DirectByteBuffer  类,直接通过反射获取     Unsafe  实例举行内存分配(  Unsafe  类的  getUnsafe  ()方法限定了只有引导类加载器才会返回     实例,也就是设计者盼望只有  rt.jar  中的类才能利用  Unsafe  的功能)。由于,虽然利用     DirectByteBuffer  分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申     请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方     法是  unsafe.allocateMemory  ()。     代码清单  2-9   利用  unsafe  分配本机内存     /**     *VM Args  :  -Xmx20M-XX  :  MaxDirectMemorySize=10M     *@author zzm     */     public class DirectMemoryOOM{     private static final int_1MB=1024*1024  ;     public static void main  (  String[]args  )  throws Exception{     Field unsafeField=Unsafe.class.getDeclaredFields  ()  [0]  ;     unsafeField.setAccessible  (  true  );     Unsafe unsafe=  (  Unsafe  )  unsafeField.get  (  null  );     while  (  true  )  {     unsafe.allocateMemory  (  _1MB  );     }     }     }     运行结果:     Exception in thread"main"java.lang.OutOfMemoryError     at sun.misc.Unsafe.allocateMemory  (  Native Method  )     at org.fenixsoft.oom.DMOOM.main  (  DMOOM.java  :  20  )     由  DirectMemory  导致的内存溢出,一个显着的特性是在  Heap Dump  文件中不会看见显着     的异常,假如读者发现  OOM  之后  Dump  文件很小,而程序中又直接或间接利用了  NIO  ,那就     可以考虑查抄一下是不是这方面的缘故原由。  2.5   本章小结     通过本章的学习,我们明白了假造机中的内存是如何划分的,哪部分区域、什么样的代     码和操作可能导致内存溢出异常。虽然  Java  有垃圾收集机制,但内存溢出异常离我们仍然并     不遥远,本章只是讲解了各个区域出现内存溢出异常的缘故原由,第  3  章将具体讲解  Java  垃圾收     集机制为了制止内存溢出异常的出现都做了哪些努力。  第  3  章 垃圾收集器与内存分配计谋     Java  与  C++  之间有一堵由内存动态分配和垃圾收集技术所围成的  “  高墙  ”  ,墙表面的人想     进去,墙里面的人却想出来。     3.1   概述     说起垃圾收集(  Garbage Collection,GC  ),大部分人都把这项技术当做  Java  语言的伴生产     物。事实上,  GC  的历史比  Java  长远,  1960  年诞生于  MIT  的  Lisp  是第一门真正利用内存动态分     配和垃圾收集技术的语言。当  Lisp  还在胚胎时期时,人们就在思索  GC  须要完成的  3  件事情:     哪些内存须要接纳?     什么时候接纳?     如何接纳?     颠末半个多世纪的发展,目前内存的动态分配与内存接纳技术已经相称成熟,统统看起     来都进入了  “  自动化  ”  期间,那为什么我们还要去了解  GC  和内存分配呢?答案很简朴:当需     要排查各种内存溢出、内存泄漏标题时,当垃圾收集成为系统到达更高并发量的瓶颈时,我     们就须要对这些  “  自动化  ”  的技术实施须要的监控和调节。     把时间从半个多世纪以前拨回到现在,回到我们熟悉的  Java  语言。第  2  章介绍了  Java  内存     运行时区域的各个部分,此中程序计数器、假造机栈、当地方法栈  3  个区域随线程而生,随     线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个     栈帧中分配多少内存基本上是在类结构确定下来时就已知的(只管在运行期会由  JIT  编译器     举行一些优化,但在本章基于概念模型的讨论中,大要上可以以为是编译期可知的),因此     这几个区域的内存分配和接纳都具备确定性,在这几个区域内就不须要过多考虑接纳的问     题,由于方法结束大概线程结束时,内存自然就跟随着接纳了。而  Java  堆和方法区则不一     样,一个接口中的多个实现类须要的内存可能不一样,一个方法中的多个分支须要的内存也     可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配     和接纳都是动态的,垃圾收集器所关注的是这部分内存,本章后续讨论中的  “  内存  ”  分配与回     收也仅指这一部分内存。  3.2   对象已死吗     在堆里面存放着  Java  天下中险些所有的对象实例,垃圾收集器在对堆举行接纳前,第一     件事情就是要确定这些对象之中哪些还  “  存活  ”  着,哪些已经  “  死去  ”  (即不可能再被任何途径     利用的对象)。     3.2.1   引用计数算法     很多教科书判定对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有     一个地方引用它时,计数器值就加  1  ;当引用失效时,计数器值就减  1  ;任何时刻计数器为  0     的对象就是不可能再被利用的。作者口试过很多的应届生和一些有多年工作履历的开发人     员,他们对于这个标题给予的都是这个答案。     客观地说,引用计数算法(  Reference Counting  )的实现简朴,判定服从也很高,在大部     分环境下它都是一个不错的算法,也有一些比较著名的应用案例,比方微软公司的     COM  (  Component Object Model  )技术、利用  ActionScript 3  的  FlashPlayer  、  Python  语言和在游     戏脚本领域被广泛应用的  Squirrel  中都利用了引用计数算法举行内存管理。但是,至少主流     的  Java  假造机里面没有选用引用计数算法来管理内存,此中最重要的缘故原由是它很难懂决对象     之间相互循环引用的标题。     举个简朴的例子,请看代码清单  3-1  中的  testGC  ()方法:对象  objA  和  objB  都有字段     instance  ,赋值令  objA.instance=objB  及  objB.instance=objA  ,除此之外,这两个对象再无任何引     用,实际上这两个对象已经不可能再被访问,但是它们由于互相引用着对方,导致它们的引     用计数都不为  0  ,于是引用计数算法无法通知  GC  收集器接纳它们。     代码清单  3-1   引用计数算法的缺陷     /**     *testGC  ()方法执行后,  objA  和  objB  会不会被  GC  呢?     *@author zzm     */     public class ReferenceCountingGC{     public Object instance=null  ;     private static final int_1MB=1024*1024  ;     /**     *  这个成员属性的唯一意义就是占点内存,以便能在  GC  日记中看清楚是否被接纳过     */     private byte[]bigSize=new byte[2*_1MB]  ;     public static void testGC  ()  {     ReferenceCountingGC objA=new ReferenceCountingGC  ();     ReferenceCountingGC objB=new ReferenceCountingGC  ();     objA.instance=objB  ;     objB.instance=objA  ;     objA=null  ;     objB=null  ;     //  假设在这行发生  GC,objA  和  objB  是否能被接纳?     System.gc  ();     }     }     运行结果:     [F u l l G C  (  S y s t e m  )  [T e n u r e d  :  0 K-  >  2 1 0 K  (  1 0 2 4 0 K  ),  0.0 1 4 9 1 4 2 s e c s]4603K-  >  210K  (  19456K  ),  [Perm  :  2999K-  >     2999K  (  21248K  )  ]  ,  0.0150007 secs][Times  :  user=0.01 sys=0.00  ,  real=0.02 secs]     Heap     def new generation total 9216K,used 82K[0x00000000055e0000  ,  0x0000000005fe0000  ,  0x0000000005fe0000  )     Eden space 8192K  ,  1%used[0x00000000055e0000  ,  0x00000000055f4850  ,  0x0000000005de0000  )     from space 1024K  ,  0%used[0x0000000005de0000  ,  0x0000000005de0000  ,  0x0000000005ee0000  )     to space 1024K  ,  0%used[0x0000000005ee0000  ,  0x0000000005ee0000  ,  0x0000000005fe0000  )     tenured generation total 10240K,used 210K[0x0000000005fe0000  ,  0x00000000069e0000  ,  0x00000000069e0000  )     the space 10240K  ,  2%used[0x0000000005fe0000  ,  0x0000000006014a18  ,  0x0000000006014c00  ,  0x00000000069e0000  )     compacting perm gen total 21248K,used 3016K[0x00000000069e0000  ,  0x0000000007ea0000  ,  0x000000000bde0000  )     the space 21248K  ,  14%used[0x00000000069e0000  ,  0x0000000006cd2398  ,  0x0000000006cd2400  ,  0x0000000007ea0000  )     No shared spaces configured.  从运行结果中可以清楚看到,  GC  日记中包罗  “4603K-  >  210K”  ,意味着假造机并没有因     为这两个对象互相引用就不接纳它们,这也从侧面说明假造机并不是通过引用计数算法来判     断对象是否存活的。  3.2.2   可达性分析算法     在主流的商用程序语言(  Java  、  C#  ,乃至包罗前面提到的古老的  Lisp  )的主流实现中,     都是称通过可达性分析(  Reachability Analysis  )来判定对象是否存活的。这个算法的基本思     路就是通过一系列的称为  “GC Roots”  的对象作为起始点,从这些节点开始向下搜刮,搜刮所     走过的路径称为引用链(  Reference Chain  ),当一个对象到  GC Roots  没有任何引用链相连     (用图论的话来说,就是从  GC Roots  到这个对象不可达)时,则证明此对象是不可用的。如     图  3-1  所示,对象  object 5  、  object 6  、  object 7  虽然互相有关联,但是它们到  GC Roots  是不可达     的,以是它们将会被判定为是可接纳的对象。     图   3-1   可达性分析算法判定对象是否可接纳     在  Java  语言中,可作为  GC Roots  的对象包罗下面几种:     假造机栈(栈帧中的当地变量表)中引用的对象。     方法区中类静态属性引用的对象。     方法区中常量引用的对象。     当地方法栈中  JNI  (即一般说的  Native  方法)引用的对象。  3.2.3   再谈引用     无论是通过引用计数算法判定对象的引用数量,还是通过可达性分析算法判定对象的引     用链是否可达,判定对象是否存活都与  “  引用  ”  有关。在  JDK 1.2  以前,  Java  中的引用的定义很     传统:假如  reference  范例的数据中存储的数值代表的是另外一块内存的起始所在,就称这块     内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用     大概没有被引用两种状态,对于如何描述一些  “  食之无味,弃之可惜  ”  的对象就显得无能为     力。我们盼望能描述这样一类对象:当内存空间还充足时,则能保留在内存之中;假如内存     空间在举行垃圾收集后还黑白常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这     样的应用场景。     在  JDK     1.2  之后,  Java  对引用的概念举行了扩充,将引用分为强引用(  Strong     Reference  )、软引用(  Soft     Reference  )、弱引用(  Weak     Reference  )、虚引用(  Phantom     Reference  )  4  种,这  4  种引用强度依次逐渐削弱。     强引用就是指在程序代码之中普遍存在的,雷同  “Object obj=new Object  ()  ”  这类的引     用,只要强引用还存在,垃圾收集器永远不会接纳掉被引用的对象。     软引用是用来描述一些尚有用但并非必需的对象。对于软引用关联着的对象,在系统将     要发生内存溢出异常之前,将会把这些对象列进接纳范围之中举行第二次接纳。假如这次回     收还没有充足的内存,才会抛出内存溢出异常。在  JDK 1.2  之后,提供了  SoftReference  类来实     现软引用。     弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的     对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否充足,     都会接纳掉只被弱引用关联的对象。在  JDK     1.2  之后,提供了  WeakReference  类来实现弱引     用。     虚引用也称为幽灵引用大概幻影引用,它是最弱的一种引用关系。一个对象是否有虚引     用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一     个对象设置虚引用关联的唯一目标就是能在这个对象被收集器接纳时收到一个系统通知。在     JDK 1.2  之后,提供了  PhantomReference  类来实现虚引用。  3.2.4   生存还是死亡     纵然在可达性分析算法中不可达的对象,也并非是  “  非死不可  ”  的,这时候它们暂时处     于  “  缓刑  ”  阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:假如对象在举行可达     性分析后发现没有与  GC Roots  相连接的引用链,那它将会被第一次标记并且举行一次筛选,     筛选的条件是此对象是否有须要执行  finalize  ()方法。当对象没有覆盖  finalize  ()方法,或     者  finalize  ()方法已经被假造机调用过,假造机将这两种环境都视为  “  没有须要执行  ”  。     假如这个对象被判定为有须要执行  finalize  ()方法,那么这个对象将会放置在一个叫做     F-Queue  的队列之中,并在稍后由一个由假造机自动建立的、低优先级的  Finalizer  线程去执行     它。这里所谓的  “  执行  ”  是指假造机会触发这个方法,但并不承诺会等待它运行结束,这样做     的缘故原由是,假如一个对象在  finalize  ()方法中执行迟钝,大概发生了死循环(更极端的情     况),将很可能会导致  F-Queue  队列中其他对象永世处于等待,乃至导致整个内存接纳系统     瓦解。  finalize  ()方法是对象逃脱死避难运的最后一次机会,稍后  GC  将对  F-Queue  中的对象     举行第二次小规模的标记,假如对象要在  finalize  ()中成功救济本身  ——  只要重新与引用链     上的任何一个对象建立关联即可,譬如把本身(  this  关键字)赋值给某个类变量大概对象的     成员变量,那在第二次标记时它将被移除出  “  即将接纳  ”  的聚集;假如对象这时候还没有逃     脱,那基本上它就真的被接纳了。从代码清单  3-2  中我们可以看到一个对象的  finalize  ()被     执行,但是它仍然可以存活。     代码清单  3-2   一次对象自我救济的演示     /**     *  此代码演示了两点:     *1.  对象可以在被  GC  时自我救济。     *2.  这种自救的机会只有一次,由于一个对象的  finalize  ()方法最多只会被系统自动调用一次     *@author zzm     */     public class FinalizeEscapeGC{     public static FinalizeEscapeGC SAVE_HOOK=null  ;     public void isAlive  ()  {     System.out.println  (  "yes,i am still alive  :)  "  );     }     @Override     protected void finalize  ()  throws Throwable{     super.finalize  ();     System.out.println  (  "finalize mehtod executed  !  "  );     FinalizeEscapeGC.SAVE_HOOK=this  ;     }     public static void main  (  String[]args  )  throws Throwable{     SAVE HOOK=new FinalizeEscapeGC  ();     //  对象第一次成功救济本身     _     SAVE_HOOK=null  ;     System.gc  ();     //  由于  finalize  方法优先级很低,以是暂停  0.5  秒以等待它     Thread.sleep  (  500  );     if  (  SAVE_HOOK  !  =null  )  {     SAVE_HOOK.isAlive  ();     }else{     System.out.println  (  "no,i am dead  :(  "  );     }     //  下面这段代码与上面的完全雷同,但是这次自救却失败了     SAVE_HOOK=null  ;     System.gc  ();     //  由于  finalize  方法优先级很低,以是暂停  0.5  秒以等待它     Thread.sleep  (  500  );     if  (  SAVE_HOOK  !  =null  )  {     SAVE_HOOK.isAlive  ();     }else{     System.out.println  (  "no,i am dead  :(  "  );     }     }     }     运行结果:     finalize mehtod executed  !     yes,i am still alive  :)     no,i am dead  :(     从代码清单  3-2  的运行结果可以看出,  SAVE_HOOK  对象的  finalize  ()方法确实被  GC  收     集器触发过,并且在被收集前成功逃脱了。  另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃     脱成功,一次失败,这是由于任何一个对象的  finalize  ()方法都只会被系统自动调用一次,     假如对象面临下一次接纳,它的  finalize  ()方法不会被再次执行,因此第二段代码的自救行     动失败了。     须要特殊说明的是,上面关于对象死亡时  finalize  ()方法的描述可能带有悲情的艺术色     彩,笔者并不鼓励各人利用这种方法来救济对象。相反,笔者建议各人只管制止利用它,因     为它不是  C/C++  中的析构函数,而是  Java  刚诞生时为了使  C/C++  程序员更轻易担当它所做出的     一个妥协。它的运行代价高昂,不确定性大,无法包管各个对象的调用顺序。有些教材中描     述它适合做  “  关闭外部资源  ”  之类的工作,这完全是对这个方法用途的一种自我安慰。     finalize  ()能做的所有工作,利用  try-finally  大概其他方式都可以做得更好、更及时,以是笔     者建议各人完全可以忘掉  Java  语言中有这个方法的存在。  3.2.5   接纳方法区     很多人以为方法区(大概  HotSpot  假造机中的永世代)是没有垃圾收集的,  Java  假造机规     范中确实说过可以不要求假造机在方法区实现垃圾收集,而且在方法区中举行垃圾收集     的  “  性价比  ”  一般比较低:在堆中,尤其是在新生代中,通例应用举行一次垃圾收集一般可以     接纳  70%  ~  95%  的空间,而永世代的垃圾收集服从远低于此。     永世代的垃圾收集重要接纳两部分内容:废弃常量和无用的类。接纳废弃常量与接纳     Java  堆中的对象非常雷同。以常量池中字面量的接纳为例,假如一个字符串  “abc”  已经进入了     常量池中,但是当前系统没有任何一个  String  对象是叫做  “abc”  的,换句话说,就是没有任何     String  对象引用常量池中的  “abc”  常量,也没有其他地方引用了这个字面量,假如这时发生内     存接纳,而且须要的话,这个  “abc”  常量就会被系统清理出常量池。常量池中的其他类(接     口)、方法、字段的符号引用也与此雷同。     判定一个常量是否是  “  废弃常量  ”  比较简朴,而要判定一个类是否是  “  无用的类  ”  的条件则     相对苛刻很多。类须要同时满足下面  3  个条件才能算是  “  无用的类  ”  :     该类所有的实例都已经被接纳,也就是  Java  堆中不存在该类的任何实例。     加载该类的  ClassLoader  已经被接纳。     该类对应的  java.lang.Class  对象没有在任何地方被引用,无法在任何地方通过反射访问该     类的方法。     假造机可以对满足上述  3  个条件的无用类举行接纳,这里说的仅仅是  “  可以  ”  ,而并不是     和对象一样,不利用了就必然会接纳。是否对类举行接纳,  HotSpot  假造机提供了  -Xnoclassgc     参数举行控制,还可以利用  -verbose  :  class  以及  -XX  :  +TraceClassLoading  、  -XX  :     +TraceClassUnLoading  检察类加载和卸载信息,此中  -verbose  :  class  和  -XX  :     +TraceClassLoading  可以在  Product  版的假造机中利用,  -XX  :  +TraceClassUnLoading  参数须要     FastDebug  版的假造机支持。     在大量利用反射、动态署理、  CGLib  等  ByteCode  框架、动态生成  JSP  以及  OSGi  这类频仍     自定义  ClassLoader  的场景都须要假造机具备类卸载的功能,以包管永世代不会溢出。  3.3   垃圾收集算法     由于垃圾收集算法的实现涉及大量的程序细节,而且各个平台的假造机操作内存的方法     又各不雷同,因此本节不计划过多地讨论算法的实现,只是介绍几种算法的思想及其发展过     程。     3.3.1   标记  -  清除算法     最基础的收集算法是  “  标记  -  清除  ”  (  Mark-Sweep  )算法,犹如它的名字一样,算法分     为  “  标记  ”  和  “  清除  ”  两个阶段:首先标记出所有须要接纳的对象,在标记完成后统一接纳所有     被标记的对象,它的标记过程实在在前一节讲述对象标记判定时已经介绍过了。之以是说它     是最基础的收集算法,是由于后续的收集算法都是基于这种思绪并对其不敷举行改进而得到     的。它的重要不敷有两个:一个是服从标题,标记和清除两个过程的服从都不高;另一个是     空间标题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程     序运行过程中须要分配较大对象时,无法找到充足的连续内存而不得不提前触发另一次垃圾     收集动作。标记  —  清除算法的执行过程如图  3-2  所示。     图   3-2 “  标记  -  清除  ”  算法表示图  3.3.2   复制算法     为了办理服从标题,一种称为  “  复制  ”  (  Copying  )的收集算法出现了,它将可用内存按容     量划分为巨细相等的两块,每次只利用此中的一块。当这一块的内存用完了,就将还存活着     的对象复制到另外一块上面,然后再把已利用过的内存空间一次清理掉。这样使得每次都是     对整个半区举行内存接纳,内存分配时也就不消考虑内存碎片等复杂环境,只要移动堆顶指     针,按顺序分配内存即可,实现简朴,运行高效。只是这种算法的代价是将内存缩小为了原     来的一半,未免太高了一点。复制算法的执行过程如图  3-3  所示。     图   3-3   复制算法表示图     现在的贸易假造机都采用这种收集算法来接纳新生代,  IBM  公司的专门研究表明,新生     代中的对象  98%  是  “  朝生夕死  ”  的,以是并不须要按照  1:1  的比例来划分内存空间,而是将内存     分为一块较大的  Eden  空间和两块较小的  Survivor  空间,每次利用  Eden  和此中一块  Survivor     [1]    。     当接纳时,将  Eden  和  Survivor  中还存活着的对象一次性地复制到另外一块  Survivor  空间上,最     后清理掉  Eden  和刚才用过的  Survivor  空间。  HotSpot  假造机默认  Eden  和  Survivor  的巨细比例是     8:1  ,也就是每次新生代中可用内存空间为整个新生代容量的  90%  (  80%+10%  ),只有  10%     的内存会被  “  浪费  ”  。当然,  98%  的对象可接纳只是一般场景下的数据,我们没有办法包管每     次接纳都只有不多于  10%  的对象存活,当  Survivor  空间不够用时,须要依赖其他内存(这里     指老年代)举行分配包管(  Handle Promotion  )。     内存的分配包管就好比我们去银行借款,假如我们信誉很好,在  98%  的环境下都能按时     归还,于是银行可能会默认我们下一次也能按时按量地归还贷款,只须要有一个包管人能保     证假如我不能还款时,可以从他的账户扣钱,那银行就以为没有风险了。内存的分配包管也     一样,假如另外一块  Survivor  空间没有充足空间存放上一次新生代收集下来的存活对象时,     这些对象将直接通过分配包管机制进入老年代。关于对新生代举行分配包管的内容,在本章  稍后在讲解垃圾收集器执行规则时还会再具体讲解。     [1]  这里须要说明一下,在  HotSpot  中的这种分代方式从最初就是这种布局,与  IBM  的研究并     没有什么实际接洽。本书枚举  IBM  的研究只是为了说明这种分代布局的意义所在。  3.3.3   标记  -  整理算法     复制收集算法在对象存活率较高时就要举行较多的复制操作,服从将会变低。更关键的     是,假如不想浪费  50%  的空间,就须要有额外的空间举行分配包管,以应对被利用的内存中     所有对象都  100%  存活的极端环境,以是在老年代一般不能直接选用这种算法。     根据老年代的特点,有人提出了另外一种  “  标记  -  整理  ”  (  Mark-Compact  )算法,标记过程     仍然与  “  标记  -  清除  ”  算法一样,但后续步骤不是直接对可接纳对象举行清理,而是让所有存     活的对象都向一端移动,然后直接清理掉端边界以外的内存,  “  标记  -  整理  ”  算法的表示图如     图  3-4  所示。     图   3-4 “  标记  -  整理  ”  算法表示图  3.3.4   分代收集算法     当前贸易假造机的垃圾收集都采用  “  分代收集  ”  (  Generational Collection  )算法,这种算     法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把  Java  堆     分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代     中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只须要付     出少量存活对象的复制本钱就可以完成收集。而老年代中由于对象存活率高、没有额外空间     对它举行分配包管,就必须利用  “  标记  —  清理  ”  大概  “  标记  —  整理  ”  算法来举行接纳。  3.4 HotSpot  的算法实现     3.2   节和  3.3  节从理论上介绍了对象存活判定算法和垃圾收集算法,而在  HotSpot  假造机     上实现这些算法时,必须对算法的执行服从有严格的考量,才能包管假造机高效运行。     3.4.1   罗列根节点     从可达性分析中从  GC Roots  节点找引用链这个操作为例,可作为  GC Roots  的节点重要在     全局性的引用(比方常量或类静态属性)与执行上下文(比方栈帧中的当地变量表)中,现     在很多应用仅仅方法区就有数百兆,假如要逐个查抄这里面的引用,那么必然会消耗很多时     间。     另外,可达性分析对执行时间的敏感还体现在  GC  停顿上,由于这项分析工作必须在一     个能确保同等性的快照中举行  ——  这里  “  同等性  ”  的意思是指在整个分析期间整个执行系统看     起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断厘革的情     况,该点不满足的话分析结果正确性就无法得到包管。这点是导致  GC  举行时必须停顿所有     Java  执行线程(  Sun  将这件事情称为  “Stop The World”  )的此中一个重要缘故原由,纵然是在号称     (险些)不会发生停顿的  CMS  收集器中,罗列根节点时也是必须要停顿的。     由于目前的主流  Java  假造机利用的都是正确式  GC  (这个概念在第  1  章介绍  Exact     VM  对     Classic VM  的改进时讲过),以是当执行系统停顿下来后,并不须要一个不漏地查抄完所有     执行上下文和全局的引用位置,假造机应当是有办法直接得知哪些地方存放着对象引用。在     HotSpot  的实现中,是利用一组称为  OopMap  的数据结构来到达这个目标的,在类加载完成的     时候,  HotSpot  就把对象内什么偏移量上是什么范例的数据计算出来,在  JIT  编译过程中,也     会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,  GC  在扫描时就可以直接得知     这些信息了。下面的代码清单  3-3  是  HotSpot Client VM  生成的一段  String.hashCode  ()方法的     当地代码,可以看到在  0x026eb7a9  处的  call  指令有  OopMap  记录,它指明了  EBX  寄存器和栈中     偏移量为  16  的内存区域中各有一个普通对象指针(  Ordinary Object Pointer  )的引用,有用范     围为从  call  指令开始直到  0x026eb730  (指令流的起始位置)  +142  (  OopMap  记录的偏移     量)  =0x026eb7be  ,即  hlt  指令为止。     代码清单  3-3 String.hashCode  ()方法编译后的当地代码     [Verified Entry Point]     0x026eb730  :  mov%eax  ,  -0x8000  (  %esp  )     ……    ;  ImplicitNullCheckStub slow case     0x026eb7a9  :  call 0x026e83e0     ;  OopMap{ebx=Oop[16]=Oop off=142}     ;  *caload     ;  -java.lang.String  :  hashCode@48  (  line 1489  )     ;  {runtime_call}     0x026eb7ae  :  push$0x83c5c18     ;  {external_word}     0x026eb7b3  :  call 0x026eb7b8     0x026eb7b8  :  pusha     0x026eb7b9  :  call 0x0822bec0  ;  {runtime_call}     0x026eb7be  :  hlt  3.4.2   安全点     在  OopMap  的协助下,  HotSpot  可以快速且正确地完成  GC Roots  罗列,但一个很现实的问     题随之而来:可能导致引用关系厘革,大概说  OopMap  内容厘革的指令非常多,假如为每一     条指令都生成对应的  OopMap  ,那将会须要大量的额外空间,这样  GC  的空间本钱将会变得很     高。     实际上,  HotSpot  也的确没有为每条指令都生成  OopMap  ,前面已经提到,只是在  “  特定的     位置  ”  记录了这些信息,这些位置称为安全点(  Safepoint  ),即程序执行时并非在所有地方都     能停顿下来开始  GC  ,只有在到达安全点时才能暂停。  Safepoint  的选定既不能太少以致于让     GC  等待时间太长,也不能过于频仍以致于过分增大运行时的负荷。以是,安全点的选定基     本上是以程序  “  是否具有让程序长时间执行的特性  ”  为标准举行选定的  ——  由于每条指令执行     的时间都非常短暂,程序不太可能由于指令流长度太长这个缘故原由而过长时间运行,  “  长时间     执行  ”  的最显着特性就是指令序列复用,比方方法调用、循环跳转、异常跳转等,以是具有     这些功能的指令才会产生  Safepoint  。     对于  Sefepoint  ,另一个须要考虑的标题是如安在  GC  发生时让所有线程(这里不包罗执行     JNI  调用的线程)都  “  跑  ”  到最近的安全点上再停顿下来。这里有两种方案可供选择:争先式     制止(  Preemptive Suspension  )和主动式制止(  Voluntary Suspension  ),此中争先式制止不需     要线程的执行代码主动去配合,在  GC  发生时,首先把所有线程全部制止,假如发现有线程     制止的地方不在安全点上,就规复线程,让它  “  跑  ”  到安全点上。现在险些没有假造机实现采     用争先式制止来暂停线程从而相应  GC  事件。     而主动式制止的思想是当  GC  须要制止线程的时候,不直接对线程操作,仅仅简朴地设     置一个标志,各个线程执行时主动去轮询这个标志,发现制止标志为真时就本身制止挂起。     轮询标志的地方和安全点是重合的,另外再加上创建对象须要分配内存的地方。下面代码清     单  3-4  中的  test  指令是  HotSpot  生成的轮询指令,当须要暂停线程时,假造机把  0x160100  的内存     页设置为不可读,线程执行到  test  指令时就会产生一个自陷异常信号,在预先注册的异常处     理器中暂停线程实现等待,这样一条汇编指令便完成安全点轮询和触发线程制止。     代码清单  3-4   轮询指令     0x01b6d627  :  call 0x01b2b210  ;  OopMap{[60]=Oop off=460}     ;  *invokeinterface size     ;  -Client1  :  main@113  (  line 23  )     ;  {virtual_call}     0x01b6d62c  :  nop     ;  OopMap{[60]=Oop off=461}     ;  *if_icmplt     ;  -Client1  :  main@118  (  line 23  )     0x01b6d62d  :  test%eax  ,  0x160100  ;  {poll}     0x01b6d633  :  mov 0x50  (  %esp  ),  %esi     0x01b6d637  :  cmp%eax  ,  %esi  3.4.3   安全区域     利用  Safepoint  似乎已经完美地办理了如何进入  GC  的标题,但实际环境却并不一定。     Safepoint  机制包管了程序执行时,在不太长的时间内就会遇到可进入  GC  的  Safepoint  。但是,     程序  “  不执行  ”  的时候呢?所谓的程序不执行就是没有分配  CPU  时间,典型的例子就是线程处     于  Sleep  状态大概  Blocked  状态,这时候线程无法相应  JVM  的制止请求,  “  走  ”  到安全的地方去     制止挂起,  JVM  也显然不太可能等待线程重新被分配  CPU  时间。对于这种环境,就须要安全     区域(  Safe Region  )来办理。     安全区域是指在一段代码片段之中,引用关系不会发生厘革。在这个区域中的恣意地方     开始  GC  都是安全的。我们也可以把  Safe Region  看做是被扩展了的  Safepoint  。     在线程执行到  Safe Region  中的代码时,首先标识本身已经进入了  Safe Region  ,那样,当     在这段时间里  JVM  要发起  GC  时,就不消管标识本身为  Safe Region  状态的线程了。在线程要离     开  Safe Region  时,它要查抄系统是否已经完成了根节点罗列(大概是整个  GC  过程),假如完     成了,那线程就继承执行,否则它就必须等待直到收到可以安全离开  Safe     Region  的信号为     止。     到此,笔者简要地介绍了  HotSpot  假造机如何去发起内存接纳的标题,但是假造机如何     具体地举行内存接纳动作仍然未涉及,由于内存接纳如何举行是由假造机所采用的  GC  收集     器决定的,而通常假造机中每每不止有一种  GC  收集器。下面继承来看  HotSpot  中有哪些  GC  收     集器。  3.5   垃圾收集器     假如说收集算法是内存接纳的方法论,那么垃圾收集器就是内存接纳的具体实现。  Java     假造机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚     拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据本身的应     用特点和要求组合出各个年代所利用的收集器。这里讨论的收集器基于  JDK 1.7 Update 14  之     后的  HotSpot  假造机(在这个版本中正式提供了商用的  G1  收集器,之前  G1  仍处于实验状     态),这个假造机包罗的所有收集器如图  3-5  所示。     图   3-5 HotSpot  假造机的垃圾收集器    [1]     图  3-5  展示了  7  种作用于不同分代的收集器,假如两个收集器之间存在连线,就说明它们     可以搭配利用。假造机所处的区域,则体现它是属于新生代收集器还是老年代收集器。接下     来笔者将逐一介绍这些收集器的特性、基本原理和利用场景,并重点分析  CMS  和  G1  这两款     相对复杂的收集器,了解它们的部分运作细节。     在介绍这些收集器各自的特性之前,我们先来明白一个观点:虽然我们是在对各个收集     器举行比较,但并非为了挑选出一个最好的收集器。由于直到现在为止还没有最好的收集器     出现,更加没有万能的收集器,以是我们选择的只是对具体应用最合适的收集器。这点不需     要多加解释就能证明:假如有一种放之四海皆准、任何场景下都适用的完美收集器存在,那     HotSpot  假造机就没须要实现那么多不同的收集器了。     3.5.1 Serial  收集器  Serial  收集器是最基本、发展历史最久长的收集器,曾经(在  JDK 1.3.1  之前)是假造机     新生代收集的唯一选择。各人看名字就会知道,这个收集器是一个单线程的收集器,但它     的  “  单线程  ”  的意义并不仅仅说明它只会利用一个  CPU  或一条收集线程去完成垃圾收集工作,     更重要的是在它举行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。  “Stop     The World”  这个名字大概听起来很酷,但这项工作实际上是由假造机在后台自动发起和自动     完成的,在用户不可见的环境下把用户正常工作的线程全部停掉,这对很多应用来说都是难     以担当的。读者不妨试想一下,要是你的计算机每运行一个小时就会暂停相应  5  分钟,你会     有什么样的心情?图  3-6  表示了  Serial/Serial Old  收集器的运行过程。     图   3-6 Serial/Serial Old  收集器运行表示图     对于  “Stop The World”  带给用户的不良体验,假造机的设计者们体现完全理解,但也表     示非常委屈:  “  你妈妈在给你扫除房间的时候,肯定也会让你老诚实实地在椅子上大概房间     外待着,假如她一边扫除,你一边乱扔纸屑,这房间还能扫除完?  ”  这确实是一个合情公道     的矛盾,虽然垃圾收集这项工作听起来和扫除房间属于一个性子的,但实际上肯定还要比打     扫房间复杂得多啊!     从  JDK 1.3  开始,一直到现在最新的  JDK 1.7  ,  HotSpot  假造机开发团队为消除大概减少工     作线程因内存接纳而导致停顿的努力一直在举行着,从  Serial  收集器到  Parallel  收集器,再到     Concurrent Mark Sweep  (  CMS  )乃至  GC  收集器的最前沿成果  Garbage First  (  G1  )收集器,我     们看到了一个个越来越良好(也越来越复杂)的收集器的出现,用户线程的停顿时间在不断     缩短,但是仍然没有办法完全消除(这里暂不包罗  RTSJ  中的收集器)。寻找更良好的垃圾收     集器的工作仍在继承!     写到这里,笔者似乎已经把  Serial  收集器描述成一个  “  老而无用、食之无味弃之可惜  ”  的     鸡肋了,但实际上到现在为止,它依然是假造机运行在  Client  模式下的默认新生代收集器。     它也有着优于其他收集器的地方:简朴而高效(与其他收集器的单线程比),对于限定单个     CPU  的环境来说,  Serial  收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最     高的单线程收集服从。在用户的桌面应用场景中,分配给假造机管理的内存一般来说不会很     大,收集几十兆乃至一两百兆的新生代(仅仅是新生代利用的内存,桌面应用基本上不会再     大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频仍发生,这点     停顿是可以担当的。以是,  Serial  收集器对于运行在  Client  模式下的假造机来说是一个很好的     选择。     [1]  图片来源:  http://blogs.sun.com/jonthecollector/entry/our_collectors  。  3.5.2 ParNew  收集器     ParNew  收集器实在就是  Serial  收集器的多线程版本,除了利用多条线程举行垃圾收集之     外,别的行为包罗  Serial  收集器可用的所有控制参数(比方:  -XX  :  SurvivorRatio  、  -XX  :     PretenureSizeThreshold  、  -XX  :  HandlePromotionFailure  等)、收集算法、  Stop The World  、对     象分配规则、接纳计谋等都与  Serial  收集器完全一样,在实现上,这两种收集器也共用了相     当多的代码。  ParNew  收集器的工作过程如图  3-7  所示。     图   3-7 ParNew/Serial Old  收集器运行表示图     ParNew  收集器除了多线程收集之外,其他与  Serial  收集器相比并没有太多创新之处,但     它却是很多运行在  Server  模式下的假造机中首选的新生代收集器,此中有一个与性能无关但     很重要的缘故原由是,除了  Serial  收集器外,目前只有它能与  CMS  收集器配合工作。在  JDK 1.5  时     期,  HotSpot  推出了一款在强交互应用中险些可以为有划期间意义的垃圾收集器  ——CMS  收     集器(  Concurrent Mark Sweep  ,本节稍后将具体介绍这款收集器),这款收集器是  HotSpot  虚     拟机中第一款真正意义上的并发(  Concurrent  )收集器,它第一次实现了让垃圾收集线程与     用户线程(基本上)同时工作,用前面那个例子的话来说,就是做到了在你的妈妈扫除房间     的时候你还能一边往地上扔纸屑。     不幸的是,  CMS  作为老年代的收集器,却无法与  JDK     1.4.0  中已经存在的新生代收集器     Parallel Scavenge  配合工作    [1]    ,以是在  JDK 1.5  中利用  CMS  来收集老年代的时候,新生代只能选     择  ParNew  大概  Serial  收集器中的一个。  ParNew  收集器也是利用  -XX  :  +UseConcMarkSweepGC     选项后的默认新生代收集器,也可以利用  -XX  :  +UseParNewGC  选项来逼迫指定它。     ParNew  收集器在单  CPU  的环境中绝对不会有比  Serial  收集器更好的效果,乃至由于存在     线程交互的开销,该收集器在通过超线程技术实现的两个  CPU  的环境中都不能百分之百地保     证可以超越  Serial  收集器。当然,随着可以利用的  CPU  的数量的增长,它对于  GC  时系统资源     的有用利用还是很有好处的。它默认开启的收集线程数与  CPU  的数量雷同,在  CPU  非常多     (譬如  32  个,现在  CPU  动辄就  4  核加超线程,服务器凌驾  32  个逻辑  CPU  的环境越来越多了)的     环境下,可以利用  -XX  :  ParallelGCThreads  参数来限定垃圾收集的线程数。     注意 从  ParNew  收集器开始,后面还会接触到几款并发和并行的收集器。在各人可能     产生疑惑之前,有须要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,     在评论垃圾收集器的上下文语境中,它们可以解释如下。     ●  并行(  Parallel  ):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状     态。  ●  并发(  Concurrent  ):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能     会瓜代执行),用户程序在继承运行,而垃圾收集程序运行于另一个  CPU  上。     [1]  Parallel Scavenge  收集器及后面提到的  G1  收集器都没有利用传统的  GC  收集器代码框架,而     另外独立实现,别的几种收集器则共用了部分的框架代码,具体内容可参考:     http://blogs.sun.com/jonthecollector/entry/our_collectors  。  3.5.3 Parallel Scavenge  收集器     Parallel Scavenge  收集器是一个新生代收集器,它也是利用复制算法的收集器,又是并行     的多线程收集器  ……  看上去和  ParNew  都一样,那它有什么特殊之处呢?     Parallel Scavenge  收集器的特点是它的关注点与其他收集器不同,  CMS  等收集器的关注点     是尽可能地缩短垃圾收集时用户线程的停顿时间,而  Parallel Scavenge  收集器的目标则是到达     一个可控制的吞吐量(  Throughput  )。所谓吞吐量就是  CPU  用于运行用户代码的时间与  CPU  总     消耗时间的比值,即吞吐量  =  运行用户代码时间  /  (运行用户代码时间  +  垃圾收集时间),虚     拟机总共运行了  100  分钟,此中垃圾收集花掉  1  分钟,那吞吐量就是  99%  。     停顿时间越短就越适合须要与用户交互的程序,良好的相应速率能提升用户体验,而高     吞吐量则可以高服从地利用  CPU  时间,尽快完成程序的运算任务,重要适合在后台运算而不     须要太多交互的任务。     Parallel Scavenge  收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集     停顿时间的  -XX  :  MaxGCPauseMillis  参数以及直接设置吞吐量巨细的  -XX  :  GCTimeRatio  参     数。     MaxGCPauseMillis  参数答应的值是一个大于  0  的毫秒数,收集器将尽可能地包管内存回     收耗费的时间不凌驾设定值。不外各人不要以为假如把这个参数的值设置得稍小一点就能使     得系统的垃圾收集速率变得更快,  GC  停顿时间缩短是以牺牲吞吐量和新生代空间来换取     的:系统把新生代调小一些,收集  300MB  新生代肯定比收集  500MB  快吧,这也直接导致垃圾     收集发生得更频仍一些,原来  10  秒收集一次、每次停顿  100  毫秒,现在变成  5  秒收集一次、每     次停顿  70  毫秒。停顿时间的确在降落,但吞吐量也降下来了。     GCTimeRatio  参数的值应当是一个大于  0  且小于  100  的整数,也就是垃圾收集时间占总时     间的比率,相称于是吞吐量的倒数。假如把此参数设置为  19  ,那答应的最大  GC  时间就占总     时间的  5%  (即  1/  (  1+19  )),默认值为  99  ,就是答应最大  1%  (即  1/  (  1+99  ))的垃圾收集     时间。     由于与吞吐量关系密切,  Parallel Scavenge  收集器也常常称为  “  吞吐量优先  ”  收集器。除上     述两个参数之外,  Parallel Scavenge  收集器尚有一个参数  -XX  :  +UseAdaptiveSizePolicy  值得关     注。这是一个开关参数,当这个参数打开之后,就不须要手工指定新生代的巨细(  -Xmn  )、     Eden  与  Survivor  区的比例(  -XX  :  SurvivorRatio  )、晋升老年代对象年龄(  -XX  :     PretenureSizeThreshold  )等细节参数了,假造机会根据当前系统的运行环境收集性能监控信     息,动态调解这些参数以提供最合适的停顿时间大概最大的吞吐量,这种调节方式称为  GC     自适应的调节计谋(  GC Ergonomics  )    [1]    。假如读者对于收集器运作原来不太了解,手工优化     存在困难的时候,利用  Parallel Scavenge  收集器配合自适应调节计谋,把内存管理的调优任务     交给假造机去完成将是一个不错的选择。只须要把基本的内存数据设置好(如  -Xmx  设置最大     堆),然后利用  MaxGCPauseMillis  参数(更关注最大停顿时间)或  GCTimeRatio  (更关注吞     吐量)参数给假造机设立一个优化目标,那具体细节参数的调节工作就由假造机完成了。自     适应调节计谋也是  Parallel Scavenge  收集器与  ParNew  收集器的一个重要区别。     [1]  官方介绍:  http://download.oracle.com/javase/1.5.0/docs/guide/vm/gc-ergonomics.html  。  3.5.4 Serial Old  收集器     Serial     Old  是  Serial  收集器的老年代版本,它同样是一个单线程收集器,利用  “  标记  -  整     理  ”  算法。这个收集器的重要意义也是在于给  Client  模式下的假造机利用。假如在  Server  模式     下,那么它重要尚有两大用途:一种用途是在  JDK 1.5  以及之前的版本中与  Parallel Scavenge     收集器搭配利用    [1]    ,另一种用途就是作为  CMS  收集器的后备预案,在并发收集发生  Concurrent     Mode Failure  时利用。这两点都将在后面的内容中具体讲解。  Serial Old  收集器的工作过程如     图  3-8  所示。     图   3-8 Serial/Serial Old  收集器运行表示图     [1]  须要说明一下,  Parallel Scavenge  收集器架构中本身有  PS MarkSweep  收集器来举行老年代     收集,并非直接利用了  Serial Old  收集器,但是这个  PS MarkSweep  收集器与  Serial Old  的实现     非常接近,以是在官方的很多资料中都是直接以  Serial Old  代替  PS MarkSweep  举行讲解,这     里笔者也采用这种方式。  3.5.5 Parallel Old  收集器     Parallel Old  是  Parallel Scavenge  收集器的老年代版本,利用多线程和  “  标记  -  整理  ”  算法。     这个收集器是在  JDK 1.6  中才开始提供的,在此之前,新生代的  Parallel Scavenge  收集器一直     处于比较尴尬的状态。缘故原由是,假如新生代选择了  Parallel     Scavenge  收集器,老年代除了     Serial Old  (  PS MarkSweep  )收集器外别无选择(还记得上面说过  Parallel Scavenge  收集器无     法与  CMS  收集器配合工作吗?)。由于老年代  Serial     Old  收集器在服务端应用性能上的  “  拖     累  ”  ,利用了  Parallel Scavenge  收集器也未必能在整体应用上获得吞吐量最大化的效果,由于     单线程的老年代收会合无法充实利用服务器多  CPU  的处理惩罚本领,在老年代很大而且硬件比较     高级的环境中,这种组合的吞吐量乃至还不一定有  ParNew  加  CMS  的组合  “  给力  ”  。     直到  Parallel     Old  收集器出现后,  “  吞吐量优先  ”  收集器终于有了比较名副实在的应用组     合,在注重吞吐量以及  CPU  资源敏感的场所,都可以优先考虑  Parallel Scavenge  加  Parallel Old     收集器。  Parallel Old  收集器的工作过程如图  3-9  所示。     图   3-9 Parallel Scavenge/Parallel Old  收集器运行表示图  3.5.6 CMS  收集器     CMS  (  Concurrent Mark Sweep  )收集器是一种以获取最短接纳停顿时间为目标的收集     器。目前很大一部分的  Java  应用会合在互联网站大概  B/S  系统的服务端上,这类应用尤其重     视服务的相应速率,盼望系统停顿时间最短,以给用户带来较好的体验。  CMS  收集器就非常     符合这类应用的需求。     从名字(包罗  “Mark Sweep”  )上就可以看出,  CMS  收集器是基于  “  标记  —  清除  ”  算法实现     的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为  4  个步骤,包罗:     初始标记(  CMS initial mark  )     并发标记(  CMS concurrent mark  )     重新标记(  CMS remark  )     并发清除(  CMS concurrent sweep  )     此中,初始标记、重新标记这两个步骤仍然须要  “Stop The World”  。初始标记仅仅只是     标记一下  GC Roots  能直接关联到的对象,速率很快,并发标记阶段就是举行  GC RootsTracing     的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继承运作而导致标记产生变     动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远     比并发标记的时间短。     由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起     工作,以是,从总体上来说,  CMS  收集器的内存接纳过程是与用户线程一起并发执行的。通     过图  3-10  可以比较清楚地看到  CMS  收集器的运作步骤中并发和须要停顿的时间。     图   3-10 Concurrent Mark Sweep  收集器运行表示图     CMS  是一款良好的收集器,它的重要优点在名字上已经体现出来了:并发收集、低停     顿,  Sun  公司的一些官方文档中也称之为并发低停顿收集器(  Concurrent     Low     Pause     Collector  )。但是  CMS  还远达不到完美的水平,它有以下  3  个显着的缺点:     CMS  收集器对  CPU  资源非常敏感。实在,面向并发设计的程序都对  CPU  资源比较敏感。     在并发阶段,它虽然不会导致用户线程停顿,但是会由于占用了一部分线程(大概说  CPU  资     源)而导致应用程序变慢,总吞吐量会低落。  CMS  默认启动的接纳线程数是(  CPU  数量     +3  )  /4  ,也就是当  CPU  在  4  个以上时,并发接纳时垃圾收集线程不少于  25%  的  CPU  资源,并且  随着  CPU  数量的增长而降落。但是当  CPU  不敷  4  个(譬如  2  个)时,  CMS  对用户程序的影响就     可能变得很大,假如原来  CPU  负载就比较大,还分出一半的运算本领去执行收集器线程,就     可能导致用户程序的执行速率忽然低落了  50%  ,实在也让人无法担当。为了应付这种环境,     假造机提供了一种称为  “  增量式并发收集器  ”  (  Incremental Concurrent Mark Sweep/i-CMS  )的     CMS  收集器变种,所做的事情和单  CPU  年代  PC  机操作系统利用抢占式来模拟多任务机制的思     想一样,就是在并发标记、清理的时候让  GC  线程、用户线程瓜代运行,只管减少  GC  线程的     独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,     也就是速率降落没有那么显着。实践证明,增量时的  CMS  收集器效果很一般,在目前版本     中,  i-CMS  已经被声明为  “deprecated”  ,即不再提倡用户利用。     CMS  收集器无法处理惩罚浮动垃圾(  Floating     Garbage  ),可能出现  “Concurrent     Mode     Failure”  失败而导致另一次  Full GC  的产生。由于  CMS  并发清理阶段用户线程还在运行着,伴     随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,  CMS  无法     在当次收会合处理惩罚掉它们,只好留待下一次  GC  时再清理掉。这一部分垃圾就称为  “  浮动垃     圾  ”  。也是由于在垃圾收集阶段用户线程还须要运行,那也就还须要预留有充足的内存空间     给用户线程利用,因此  CMS  收集器不能像其他收集器那样等到老年代险些完全被填满了再进     行收集,须要预留一部分空间提供并发收集时的程序运作利用。在  JDK     1.5  的默认设置     下,  CMS  收集器当老年代利用了  68%  的空间后就会被激活,这是一个偏守旧的设置,假如在     应用中老年代增长不是太快,可以适当调高参数  -XX  :  CMSInitiatingOccupancyFraction  的值来     提高触发百分比,以便低落内存接纳次数从而获取更好的性能,在  JDK 1.6  中,  CMS  收集器     的启动阈值已经提升至  92%  。要是  CMS  运行期间预留的内存无法满足程序须要,就会出现一     次  “Concurrent Mode Failure”  失败,这时假造机将启动后备预案:临时启用  Serial Old  收集器来     重新举行老年代的垃圾收集,这样停顿时间就很长了。以是说参数  -XX  :  CM     SInitiatingOccupancyFraction  设置得太高很轻易导致大量  “Concurrent Mode Failure”  失败,性能     反而低落。     尚有最后一个缺点,在本节开头说过,  CMS  是一款基于  “  标记  —  清除  ”  算法实现的收集     器,假如读者对前面这种算法介绍尚有印象的话,就可能想到这意味着收集结束时会有大量     空间碎片产生。空间碎片过多时,将会给大对象分配带来很大贫苦,每每会出现老年代尚有     很大空间剩余,但是无法找到充足大的连续空间来分配当前对象,不得不提前触发一次  Full     GC  。为了办理这个标题,  CMS  收集器提供了一个  -XX  :  +UseCMSCompactAtFullCollection  开     关参数(默认就是开启的),用于在  CMS  收集器顶不住要举行  FullGC  时开启内存碎片的合并     整理过程,内存整理的过程是无法并发的,空间碎片标题没有了,但停顿时间不得不变长。     假造机设计者还提供了另外一个参数  -XX  :  CMSFullGCsBeforeCompaction  ,这个参数是用于     设置执行多少次不压缩的  Full GC  后,跟着来一次带压缩的(默认值为  0  ,体现每次进入  Full     GC  时都举行碎片整理)。  3.5.7 G1  收集器     G1  (  Garbage-First  )收集器是当今收集器技术发展的最前沿成果之一,早在  JDK 1.7  刚刚     建立项目目标,  Sun  公司给出的  JDK 1.7 RoadMap  里面,它就被视为  JDK 1.7  中  HotSpot  假造机     的一个重要进化特性。从  JDK 6u14  中开始就有  Early Access  版本的  G1  收集器供开发人员实     验、试用,由此开始  G1  收集器的  “Experimental”  状态持续了数年时间,直至  JDK 7u4  ,  Sun  公     司才以为它到达充足成熟的商用水平,移除了  “Experimental”  的标识。     G1  是一款面向服务端应用的垃圾收集器。  HotSpot  开发团队赋予它的使命是(在比较长     期的)未来可以替换掉  JDK 1.5  中发布的  CMS  收集器。与其他  GC  收集器相比,  G1  具备如下特     点。     并行与并发:  G1  能充实利用多  CPU  、多核环境下的硬件优势,利用多个  CPU  (  CPU  大概     CPU  核心)来缩短  Stop-The-World  停顿的时间,部分其他收集器原来须要停顿  Java  线程执行的     GC  动作,  G1  收集器仍然可以通过并发的方式让  Java  程序继承执行。     分代收集:与其他收集器一样,分代概念在  G1  中依然得以保留。虽然  G1  可以不须要其     他收集器配合就能独立管理整个  GC  堆,但它可以或许采用不同的方式去处理惩罚新创建的对象和已     经存活了一段时间、熬过多次  GC  的旧对象以获取更好的收集效果。     空间整合:与  CMS  的  “  标记  —  清理  ”  算法不同,  G1  从整体来看是基于  “  标记  —  整理  ”  算法实     现的收集器,从局部(两个  Region  之间)上来看是基于  “  复制  ”  算法实现的,但无论如何,这     两种算法都意味着  G1  运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种     特性有利于程序长时间运行,分配大对象时不会由于无法找到连续内存空间而提前触发下一     次  GC  。     可预测的停顿:这是  G1  相对于  CMS  的另一大优势,低落停顿时间是  G1  和  CMS  共同的关     注点,但  G1  除了寻求低停顿外,还能建立可预测的停顿时间模型,能让利用者明白指定在一     个长度为  M  毫秒的时间片段内,消耗在垃圾收集上的时间不得凌驾  N  毫秒,这险些已经是实     时  Java  (  RTSJ  )的垃圾收集器的特性了。     在  G1  之前的其他收集器举行收集的范围都是整个新生代大概老年代,而  G1  不再是这     样。利用  G1  收集器时,  Java  堆的内存布局就与其他收集器有很大差别,它将整个  Java  堆划分     为多个巨细相等的独立区域(  Region  ),虽然还保留有新生代和老年代的概念,但新生代和     老年代不再是物理隔离的了,它们都是一部分  Region  (不须要连续)的聚集。     G1  收集器之以是能建立可预测的停顿时间模型,是由于它可以有计划地制止在整个  Java     堆中举行全区域的垃圾收集。  G1  跟踪各个  Region  里面的垃圾堆积的价值巨细(接纳所获得的     空间巨细以及接纳所需时间的履历值),在后台维护一个优先列表,每次根据答应的收集时     间,优先接纳价值最大的  Region  (这也就是  Garbage-First  名称的来由)。这种利用  Region  划分     内存空间以及有优先级的区域接纳方式,包管了  G1  收集器在有限的时间内可以获取尽可能高     的收集服从。     G1  把内存  “  化整为零  ”  的思绪,理解起来似乎很轻易,但此中的实现细节却远远没有想象     中那样简朴,否则也不会从  2004  年  Sun  实验室发表第一篇  G1  的论文开始直到今天(将近  10  年     时间)才开发出  G1  的商用版。笔者以一个细节为例:把  Java  堆分为多个  Region  后,垃圾收集  是否就真的能以  Region  为单位举行了?听起来顺理成章,再仔细想想就很轻易发现标题所     在:  Region  不可能是孤立的。一个对象分配在某个  Region  中,它并非只能被本  Region  中的其     他对象引用,而是可以与整个  Java  堆恣意的对象发生引用关系。那在做可达性判定确定对象     是否存活的时候,岂不是还得扫描整个  Java  堆才能包管正确性?这个标题实在并非在  G1  中才     有,只是在  G1  中更加突出而已。在以前的分代收会合,新生代的规模一般都比老年代要小许     多,新生代的收集也比老年代要频仍很多,那接纳新生代中的对象时也面临雷同的标题,如     果接纳新生代时也不得不同时扫描老年代的话,那么  Minor GC  的服从可能降落不少。     在  G1  收集器中,  Region  之间的对象引用以及其他收集器中的新生代与老年代之间的对象     引用,假造机都是利用  Remembered Set  来制止全堆扫描的。  G1  中每个  Region  都有一个与之对     应的  Remembered Set  ,假造机发现程序在对  Reference  范例的数据举行写操作时,会产生一个     Write Barrier  暂时制止写操作,查抄  Reference  引用的对象是否处于不同的  Region  之中(在分代     的例子中就是查抄是否老年代中的对象引用了新生代中的对象),假如是,便通过     CardTable  把相干引用信息记录到被引用对象所属的  Region  的  Remembered Set  之中。当举行内     存接纳时,在  GC  根节点的罗列范围中到场  Remembered Set  即可包管不对全堆扫描也不会有遗     漏。     假如不计算维护  Remembered Set  的操作,  G1  收集器的运作大抵可划分为以下几个步骤:     初始标记(  Initial Marking  )     并发标记(  Concurrent Marking  )     最终标记(  Final Marking  )     筛选接纳(  Live Data Counting and Evacuation  )     对  CMS  收集器运作过程熟悉的读者,一定已经发现  G1  的前几个步骤的运作过程和  CMS     有很多相似之处。初始标记阶段仅仅只是标记一下  GC Roots  能直接关联到的对象,并且修改     TAMS  (  Next Top at Mark Start  )的值,让下一阶段用户程序并发运行时,能在正确可用的     Region  中创建新对象,这阶段须要停顿线程,但耗时很短。并发标记阶段是从  GC Root  开始     对堆中对象举行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执     行。而最终标记阶段则是为了修正在并发标记期间因用户程序继承运作而导致标记产生变更     的那一部分标记记录,假造机将这段时间对象厘革记录在线程  Remembered Set Logs  里面,最     终标记阶段须要把  Remembered Set Logs  的数据合并到  Remembered Set  中,这阶段须要停顿线     程,但是可并行执行。最后在筛选接纳阶段首先对各个  Region  的接纳价值和本钱举行排序,     根据用户所期望的  GC  停顿时间来制定接纳计划,从  Sun  公司透暴露来的信息来看,这个阶段     实在也可以做到与用户程序一起并发执行,但是由于只接纳一部分  Region  ,时间是用户可控     制的,而且停顿用户线程将大幅提高收集服从。通过图  3-11  可以比较清楚地看到  G1  收集器的     运作步骤中并发和须要停顿的阶段。  图   3-11 G1  收集器运行表示图     由于目前  G1  成熟版本的发布时间还很短,  G1  收集器险些可以说还没有颠末实际应用的     考验,网络上关于  G1  收集器的性能测试也非常缺少,到目前为止,笔者还没有搜刮到有关的     生产环境下的性能测试报告。夸大  “  生产环境下的测试报告  ”  是由于对于垃圾收集器来说,仅     仅通过简朴的  Java  代码写个  Microbenchmark  程序来创建、移除  Java  对象,再用  -XX  :     +PrintGCDetails  等参数来检察  GC  日记是很难做到正确衡量其性能的。因此,关于  G1  收集器的     性能部分,笔者引用了  Sun  实验室的论文《  Garbage-First Garbage Collection  》中的一段测试数     据。     Sun  给出的  Benchmark  的执行硬件为  Sun V880  服务器(  8×750MHz UltraSPARC III CPU  、     32G  内存、  Solaris     10  操作系统)。执行软件有两个,分别为  SPECjbb  (模拟贸易数据库应     用,堆中存活对象约为  165MB  ,结果反映吐量和最长事务处理惩罚时间)和  telco  (模拟电话应答     服务应用,堆中存活对象约为  100MB  ,结果反映系统能支持的最大吞吐量)。为了便于对     比,还收集了一组利用  ParNew+CMS  收集器的测试数据。所有测试都配置为与  CPU  数量雷同     的  8  条  GC  线程。     在反应停顿时间的软及时目标(  Soft Real-Time Goal  )测试中,横向是两个测试软件的     时间片段配置,单位是毫秒,以(  X/Y  )的形式体现,代表在  Y  毫秒内最大答应  GC  时间为  X     毫秒(对于  CMS  收集器,无法直接指定这个目标,通过调解分代巨细的方式大抵模拟)。纵     向是两个软件在对应配置和不同的  Java  堆容量下的测试结果,  V%  、  avgV%  和  wV%  分别代表     的含义如下。     V%  :体现测试过程中,软及时目标失败的概率,软及时目标失败即某个时间片段中实     际  GC  时间凌驾了答应的最大  GC  时间。     avgV%  :体现在所有实际  GC  时间超标的时间片段里,实际  GC  时间凌驾最大  GC  时间的平     均百分比(实际  GC  时间减去答应最大  GC  时间,再除以总时间片段)。     wV%  :体现在测试结果最差的时间片段里,实际  GC  时间占用执行时间的百分比。     测试结果见表  3-1  。  从表  3-1  所示的结果可见,对于  telco  来说,软及时目标失败的概率控制在  0.5%  ~  0.7%  之     间,  SPECjbb  就要差一些,但也控制在  2%  ~  5%  之间,概率随着(  X/Y  )的比值减小而增长。     另一方面,失败时超出答应  GC  时间的比值随着总时间片段增长而变小(分母变大了),在     (  100/200  )、  512MB  的配置下,  G1  收集器出现了某些时间片段下  100%  时间在举行  GC  的最坏     环境。而相比之下,  CMS  收集器的测试结果就要差很多,  3  种  Java  堆容量下都出现了  100%  时     间举行  GC  的环境。     在吞吐量测试中,测试数据取  3  次  SPECjbb  和  15  次  telco  的均匀结果如图  3-12  所示。在     SPECjbb  的应用下,各种配置下的  G1  收集器体现出了同等的行为,吞吐量看起来只与答应最     大  GC  时间成正比关系,而在  telco  的应用中,不同配置对吞吐量的影响则显得很薄弱。与     CMS  收集器的吞吐量对比可以看到,在  SPECjbb  测试中,在堆容量凌驾  768MB  时,  CMS  收集     器有  5%  ~  10%  的优势,而在  telco  测试中,  CMS  的优势则要小一些,只有  3%  ~  4%  左右。  图   3-12   吞吐量测试结果     在更大规模的生产环境下,笔者引用一段在  StackOverflow.com  上看到的履历与读者分     享:  “  我在一个真实的、较大规模的应用程序中利用过  G1  :约莫分配有  60  ~  70GB  内存,存活     对象约莫在  20  ~  50GB  之间。服务器运行  Linux  操作系统,  JDK  版本为  6u22  。  G1  与  PS/PS Old  相     比,最大的好处是停顿时间更加可控、可预测,假如我在  PS  中设置一个很低的最大答应  GC     时间,譬准期望  50  毫秒内完成  GC  (  -XX  :  MaxGCPauseMillis=50  ),但在  65GB  的  Java  堆下有     可能得到的直接结果是一次长达  30  秒至  2  分钟的漫长的  Stop-The-World  过程;而  G1  与  CMS  相     比,虽然它们都立足于低停顿时间,  CMS  仍然是我现在的选择,但是随着  Oracle  对  G1  的持续     改进,我相信  G1  会是最终的胜利者。假如你现在采用的收集器没有出现标题,那就没有任何     理由现在去选择  G1  ,假如你的应用寻求低停顿,那  G1  现在已经可以作为一个可实验的选     择,假如你的应用寻求吞吐量,那  G1  并不会为你带来什么特殊的好处  ”  。  3.5.8   理解  GC  日记     阅读  GC  日记是处理惩罚  Java  假造机内存标题标基础技能,它只是一些人为确定的规则,没有     太多技术含量。在本书的第  1  版中没有专门讲解如何阅读分析  GC  日记,为此作者收到很多读     者来信,反映对此感到狐疑,因此专门增长本节内容来讲解如何理解  GC  日记。     每一种收集器的日记形式都是由它们自身的实现所决定的,换而言之,每个收集器的日     志格式都可以不一样。但假造机设计者为了方便用户阅读,将各个收集器的日记都维持一定     的共性,比方以下两段典型的  GC  日记:     33.125  :  [GC[DefNew  :  3324K-  >  152K  (  3712K  ),  0.0025925 secs]3324K-  >  152K  (  11904K  ),  0.0031680 secs]     1 0 0.6 6 7  :  [F u l l G C[T e n u r e d  :  0 K-  >  2 1 0 K  (  1 0 2 4 0 K  ),  0.0 1 4 9 1 4 2 s e c s]4603K-  >  210K  (  19456K  ),  [Perm  :  2999K-  >     2999K  (  21248K  )  ]  ,  0.0150007 secs][Times  :  user=0.01 sys=0.00  ,  real=0.02 secs]     最前面的数字  “33.125  :  ”  和  “100.667  :  ”  代表了  GC  发生的时间,这个数字的含义是从  Java     假造机启动以来颠末的秒数。     GC  日记开头的  “[GC”  和  “[Full GC”  说明了这次垃圾收集的停顿范例,而不是用来区分新     生代  GC  还是老年代  GC  的。假如有  “Full”  ,说明这次  GC  是发生了  Stop-The-World  的,比方下面     这段新生代收集器  ParNew  的日记也会出现  “[Full GC”  (这一般是由于出现了分配包管失败之     类的标题,以是才导致  STW  )。假如是调用  System.gc  ()方法所触发的收集,那么在这里将     显示  “[Full GC  (  System  )  ”  。     [Full GC 283.736  :  [ParNew  :  261599K-  >  261599K  (  261952K  ),  0.0000288 secs]     接下来的  “[DefNew”  、  “[Tenured”  、  “[Perm”  体现  GC  发生的区域,这里显示的区域名称与     利用的  GC  收集器是密切相干的,比方上面样例所利用的  Serial  收集器中的新生代名为  “Default     New     Generation”  ,以是显示的是  “[DefNew”  。假如是  ParNew  收集器,新生代名称就会变     为  “[ParNew”  ,意为  “Parallel New Generation”  。假如采用  Parallel Scavenge  收集器,那它配套     的新生代称为  “PSYoungGen”  ,老年代和永世代同理,名称也是由收集器决定的。     后面方括号内部的  “3324K-  >  152K  (  3712K  )  ”  含义是  “GC  前该内存区域已利用容量  -  >     GC  后该内存区域已利用容量(该内存区域总容量)  ”  。而在方括号之外的  “3324K-  >     152K  (  11904K  )  ”  体现  “GC  前  Java  堆已利用容量  -  >  GC  后  Java  堆已利用容量(  Java  堆总容     量)  ”  。     再往后,  “0.0025925 secs”  体现该内存区域  GC  所占用的时间,单位是秒。有的收集器会     给出更具体的时间数据,如  “[Times  :  user=0.01 sys=0.00  ,  real=0.02 secs]”  ,这里面的  user  、     sys  和  real  与  Linux  的  time  命令所输出的时间含义同等,分别代表用户态消耗的  CPU  时间、内核     态消耗的  CPU  事件和操作从开始到结束所颠末的墙钟时间(  Wall Clock Time  )。  CPU  时间与     墙钟时间的区别是,墙钟时间包罗各种非运算的等待耗时,比方等待磁盘  I/O  、等待线程阻     塞,而  CPU  时间不包罗这些耗时,但当系统有多  CPU  大概多核的话,多线程操作会叠加这些     CPU  时间,以是读者看到  user  或  sys  时间凌驾  real  时间是完全正常的。  3.5.9   垃圾收集器参数总结     JDK 1.7  中的各种垃圾收集器到此已全部介绍完毕,在描述过程中提到了很多假造机非     稳定的运行参数,在表  3-2  中整理了这些参数供读者实践时参考。  3.6   内存分配与接纳计谋     Java  技术体系中所提倡的自动内存管理最终可以归结为自动化地办理了两个标题:给对     象分配内存以及接纳分配给对象的内存。关于接纳内存这一点,我们已经利用了大量篇幅去     介绍假造机中的垃圾收集器体系以及运作原理,现在我们再一起来探讨一下给对象分配内存     的那点事儿。     对象的内存分配,往大方向讲,就是在堆上分配(但也可能颠末  JIT  编译后被拆散为标     量范例并间接地栈上分配    [1]    ),对象重要分配在新生代的  Eden  区上,假如启动了当地线程分     配缓冲,将按线程优先在  TLAB  上分配。少数环境下也可能会直接分配在老年代中,分配的     规则并不是百分之百固定的,其细节取决于当前利用的是哪一种垃圾收集器组合,尚有假造     机中与内存相干的参数的设置。     接下来我们将会讲解几条最普遍的内存分配规则,并通过代码去验证这些规则。本节下     面的代码在测试时利用  Client  模式假造机运行,没有手工指定收集器组合,换句话说,验证     的是在利用  Serial/Serial Old  收集器下(  ParNew/Serial Old  收集器组合的规则也基本同等)的     内存分配和接纳的计谋。读者不妨根据本身项目中利用的收集器写一些程序去验证一下利用     其他几种收集器的内存分配计谋。     3.6.1   对象优先在  Eden  分配     大多数环境下,对象在新生代  Eden  区中分配。当  Eden  区没有充足空间举行分配时,假造     机将发起一次  Minor GC  。     假造机提供了  -XX  :  +PrintGCDetails  这个收集器日记参数,告诉假造机在发生垃圾收集     行为时打印内存接纳日记,并且在历程退出的时候输出当前的内存各区域分配环境。在实际     应用中,内存接纳日记一般是打印到文件后通过日记工具举行分析,不外本实验的日记并不     多,直接阅读就能看得很清楚。     代码清单  3-5  的  testAllocation  ()方法中,实验分配  3  个  2MB  巨细和  1  个  4MB  巨细的对象,     在运行时通过  -Xms20M  、  -Xmx20M  、  -Xmn10M  这  3  个参数限定了  Java  堆巨细为  20MB  ,不可扩     展,此中  10MB  分配给新生代,剩下的  10MB  分配给老年代。  -XX  :  SurvivorRatio=8  决定了新     生代中  Eden  区与一个  Survivor  区的空间比例是  8:1  ,从输出的结果也可以清晰地看到  “eden     space     8192K  、  from     space     1024K  、  to     space     1024K”  的信息,新生代总可用空间为     9216KB  (  Eden  区  +1  个  Survivor  区的总容量)。     执行  testAllocation  ()中分配  allocation4  对象的语句时会发生一次  Minor GC  ,这次  GC  的     结果是新生代  6651KB  变为  148KB  ,而总内存占用量则险些没有减少(由于  allocation1  、     allocation2  、  allocation3  三个对象都是存活的,假造机险些没有找到可接纳的对象)。这次     GC  发生的缘故原由是给  allocation4  分配内存的时候,发现  Eden  已经被占用了  6MB  ,剩余空间已不     足以分配  allocation4  所需的  4MB  内存,因此发生  Minor GC  。  GC  期间假造机又发现已有的  3  个     2MB  巨细的对象全部无法放入  Survivor  空间(  Survivor  空间只有  1MB  巨细),以是只好通过分     配包管机制提前转移到老年代去。     这次  GC  结束后,  4MB  的  allocation4  对象顺利分配在  Eden  中,因此程序执行完的结果是     Eden  占用  4MB  (被  allocation4  占用),  Survivor  空闲,老年代被占用  6MB  (被  allocation1  、  allocation2  、  allocation3  占用)。通过  GC  日记可以证明这一点。     注意 作者多次提到的  Minor GC  和  Full GC  有什么不一样吗?     新生代  GC  (  Minor GC  ):指发生在新生代的垃圾收集动作,由于  Java  对象大多都具备朝     生夕灭的特性,以是  Minor GC  非常频仍,一般接纳速率也比较快。     老年代  GC  (  Major GC/Full GC  ):指发生在老年代的  GC  ,出现了  Major GC  ,常常会伴     随至少一次的  Minor GC  (但非绝对的,在  Parallel Scavenge  收集器的收集计谋里就有直接举行     Major GC  的计谋选择过程)。  Major GC  的速率一般会比  Minor GC  慢  10  倍以上。     代码清单  3-5   新生代  Minor GC     private static final int_1MB=1024*1024  ;     /**     *VM  参数:  -verbose  :  gc-Xms20M-Xmx20M-Xmn10M-XX  :  +PrintGCDetails     -XX  :  SurvivorRatio=8     */     public static void testAllocation  ()  {     byte[]allocation1  ,  allocation2  ,  allocation3  ,  allocation4  ;     allocation1=new byte[2*_1MB]  ;     allocation2=new byte[2*_1MB]  ;     allocation3=new byte[2*    _    1MB]  ;     allocation4=new byte[4*    _    1MB]  ;  //  出现一次  Minor GC     }     运行结果:     [GC[DefNew  :  6651K-  >  148K  (  9216K  ),  0.0070106 secs]6651K-  >  6292K  (  19456K  ),     0.0070426 secs][Times  :  user=0.00 sys=0.00  ,  real=0.00 secs]     Heap     def new generation total 9216K,used 4326K[0x029d0000  ,  0x033d0000  ,  0x033d0000  )     eden space 8192K  ,  51%used[0x029d0000  ,  0x02de4828  ,  0x031d0000  )     from space 1024K  ,  14%used[0x032d0000  ,  0x032f5370  ,  0x033d0000  )     to space 1024K  ,  0%used[0x031d0000  ,  0x031d0000  ,  0x032d0000  )     tenured generation total 10240K,used 6144K[0x033d0000  ,  0x03dd0000  ,  0x03dd0000  )     the space 10240K  ,  60%used[0x033d0000  ,  0x039d0030  ,  0x039d0200  ,  0x03dd0000  )     compacting perm gen total 12288K,used 2114K[0x03dd0000  ,  0x049d0000  ,  0x07dd0000  )     the space 12288K  ,  17%used[0x03dd0000  ,  0x03fe0998  ,  0x03fe0a00  ,  0x049d0000  )     No shared spaces configured.     [1]  JIT  即时编译器相干优化可拜见第  11  章。  3.6.2   大对象直接进入老年代     所谓的大对象是指,须要大量连续内存空间的  Java  对象,最典型的大对象就是那种很长     的字符串以及数组(笔者列出的例子中的  byte[]  数组就是典型的大对象)。大对象对假造机     的内存分配来说就是一个坏消息(替  Java  假造机抱怨一句,比遇到一个大对象更加坏的消息     就是遇到一群  “  朝生夕灭  ”  的  “  短命大对象  ”  ,写程序的时候应当制止),常常出现大对象轻易     导致内存尚有不少空间时就提前触发垃圾收集以获取充足的连续空间来  “  安置  ”  它们。     假造机提供了一个  -XX  :  PretenureSizeThreshold  参数,令大于这个设置值的对象直接在老     年代分配。这样做的目标是制止在  Eden  区及两个  Survivor  区之间发生大量的内存复制(复习     一下:新生代采用复制算法收集内存)。     执行代码清单  3-6  中的  testPretenureSizeThreshold  ()方法后,我们看到  Eden  空间险些没有     被利用,而老年代的  10MB  空间被利用了  40%  ,也就是  4MB  的  allocation  对象直接就分配在老     年代中,这是由于  PretenureSizeThreshold  被设置为  3MB  (就是  3145728  ,这个参数不能像  -Xmx     之类的参数一样直接写  3MB  ),因此凌驾  3MB  的对象都会直接在老年代举行分配。注意     PretenureSizeThreshold  参数只对  Serial  和  ParNew  两款收集器有用,  Parallel Scavenge  收集器不     认识这个参数,  Parallel     Scavenge  收集器一般并不须要设置。假如遇到必须利用此参数的场     合,可以考虑  ParNew  加  CMS  的收集器组合。     代码清单  3-6   大对象直接进入老年代     private static final int_1MB=1024*1024  ;     /**     *VM  参数:  -verbose  :  gc-Xms20M-Xmx20M-Xmn10M-XX  :  +PrintGCDetails-XX  :  SurvivorRatio=8     *-XX  :  PretenureSizeThreshold=3145728     */     public static void testPretenureSizeThreshold  ()  {     byte[]allocation  ;     allocation=new byte[4*_1MB]  ;  //  直接分配在老年代中     }     运行结果:     Heap     def new generation total 9216K,used 671K[0x029d0000  ,  0x033d0000  ,  0x033d0000  )     eden space 8192K  ,  8%used[0x029d0000  ,  0x02a77e98  ,  0x031d0000  )     from space 1024K  ,  0%used[0x031d0000  ,  0x031d0000  ,  0x032d0000  )     to space 1024K  ,  0%used[0x032d0000  ,  0x032d0000  ,  0x033d0000  )     tenured generation total 10240K,used 4096K[0x033d0000  ,  0x03dd0000  ,  0x03dd0000  )     the space 10240K  ,  40%used[0x033d0000  ,  0x037d0010  ,  0x037d0200  ,  0x03dd0000  )     compacting perm gen total 12288K,used 2107K[0x03dd0000  ,  0x049d0000  ,  0x07dd0000  )     the space 12288K  ,  17%used[0x03dd0000  ,  0x03fdefd0  ,  0x03fdf000  ,  0x049d0000  )     No shared spaces configured.  3.6.3   长期存活的对象将进入老年代     既然假造机采用了分代收集的思想来管理内存,那么内存接纳时就必须能辨认哪些对象     应放在新生代,哪些对象应放在老年代中。为了做到这点,假造机给每个对象定义了一个对     象年龄(  Age  )计数器。假如对象在  Eden  出生并颠末第一次  Minor GC  后仍然存活,并且能被     Survivor  容纳的话,将被移动到  Survivor  空间中,并且对象年龄设为  1  。对象在  Survivor  区中     每  “  熬过  ”  一次  Minor GC  ,年龄就增长  1  岁,当它的年龄增长到一定水平(默以为  15  岁),就     将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数  -XX  :     MaxTenuringThreshold  设置。     读者可以试试分别以  -XX  :  MaxTenuringThreshold=1  和  -XX  :  MaxTenuringThreshold=15  两     种设置来执行代码清单  3-7  中的  testTenuringThreshold  ()方法,此方法中的  allocation1  对象需     要  256KB  内存,  Survivor  空间可以容纳。当  MaxTenuringThreshold=1  时,  allocation1  对象在第二     次  GC  发生时进入老年代,新生代已利用的内存  GC  后非常干净地变成  0KB  。而     MaxTenuringThreshold=15  时,第二次  GC  发生后,  allocation1  对象则还留在新生代  Survivor  空     间,这时新生代仍然有  404KB  被占用。     代码清单  3-7   长期存活的对象进入老年代     private static final int_1MB=1024*1024  ;     /**     *VM  参数:  -verbose  :  gc-Xms20M-Xmx20M-Xmn10M-XX  :  +PrintGCDetails-XX  :  SurvivorRatio=8-XX  :  MaxTenuringThreshold=1     *-XX  :  +PrintTenuringDistribution     */     @SuppressWarnings  (  "unused"  )     public static void testTenuringThreshold  ()  {     byte[]allocation1  ,  allocation2  ,  allocation3  ;     allocation1=new byte[ 1MB/4]  ;     //  什么时候进入老年代取决于  XX  :  MaxTenuringThreshold  设置     _     allocation2=new byte[4*_1MB]  ;     allocation3=new byte[4*_1MB]  ;     allocation3=null  ;     allocation3=new byte[4*_1MB]  ;     }     以  MaxTenuringThreshold=1  参数来运行的结果:     [GC[DefNew     Desired Survivor size 524288 bytes,new threshold 1  (  max 1  )     -age 1  :  414664 bytes  ,  414664 total     :  4859K-  >  404K  (  9216K  ),  0.0065012 secs]4859K-  >  4500K  (  19456K  ),  0.0065283 secs][Times  :  user=0.02 sys=0.00  ,  real=0.02 secs]     [GC[DefNew     Desired Survivor size 524288 bytes,new threshold 1  (  max 1  )     :  4500K-  >  0K  (  9216K  ),  0.0009253 secs]8596K-  >  4500K  (  19456K  ),  0.0009458 secs][Times  :  user=0.00 sys=0.00  ,  real=0.00 secs]     Heap     def new generation total 9216K,used 4178K[0x029d0000  ,  0x033d0000  ,  0x033d0000  )     eden space 8192K  ,  51%used[0x029d0000  ,  0x02de4828  ,  0x031d0000  )     from space 1024K  ,  0%used[0x031d0000  ,  0x031d0000  ,  0x032d0000  )     to space 1024K  ,  0%used[0x032d0000  ,  0x032d0000  ,  0x033d0000  )     tenured generation total 10240K,used 4500K[0x033d0000  ,  0x03dd0000  ,  0x03dd0000  )     the space 10240K  ,  43%used[0x033d0000  ,  0x03835348  ,  0x03835400  ,  0x03dd0000  )     compacting perm gen total 12288K,used 2114K[0x03dd0000  ,  0x049d0000  ,  0x07dd0000  )     the space 12288K  ,  17%used[0x03dd0000  ,  0x03fe0998  ,  0x03fe0a00  ,  0x049d0000  )     No shared spaces configured.     以  MaxTenuringThreshold=15  参数来运行的结果:     [GC[DefNew     Desired Survivor size 524288 bytes,new threshold 15  (  max 15  )     -age 1  :  414664 bytes  ,  414664 total     :  4859K-  >  404K  (  9216K  ),  0.0049637 secs]4859K-  >  4500K  (  19456K  ),  0.0049932 secs][Times  :  user=0.00 sys=0.00  ,  real=0.00 secs]     [GC[DefNew     Desired Survivor size 524288 bytes,new threshold 15  (  max 15  )     -age 2  :  414520 bytes  ,  414520 total     :  4500K-  >  404K  (  9216K  ),  0.0008091 secs]8596K-  >  4500K  (  19456K  ),  0.0008305 secs][Times  :  user=0.00 sys=0.00  ,  real=0.00 secs]     Heap     def new generation total 9216K,used 4582K[0x029d0000  ,  0x033d0000  ,  0x033d0000  )     eden space 8192K  ,  51%used[0x029d0000  ,  0x02de4828  ,  0x031d0000  )     from space 1024K  ,  39%used[0x031d0000  ,  0x03235338  ,  0x032d0000  )     to space 1024K  ,  0%used[0x032d0000  ,  0x032d0000  ,  0x033d0000  )     tenured generation total 10240K,used 4096K[0x033d0000  ,  0x03dd0000  ,  0x03dd0000  )     the space 10240K  ,  40%used[0x033d0000  ,  0x037d0010  ,  0x037d0200  ,  0x03dd0000  )     compacting perm gen total 12288K,used 2114K[0x03dd0000  ,  0x049d0000  ,  0x07dd0000  )     the space 12288K  ,  17%used[0x03dd0000  ,  0x03fe0998  ,  0x03fe0a00  ,  0x049d0000  )     No shared spaces configured.  3.6.4   动态对象年龄判定     为了能更好地适应不同程序的内存状况,假造机并不是永远地要求对象的年龄必须到达     了  MaxTenuringThreshold  才能晋升老年代,假如在  Survivor  空间中雷同年龄所有对象巨细的总     和大于  Survivor  空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等     到  MaxTenuringThreshold  中要求的年龄。     执行代码清单  3-8  中的  testTenuringThreshold2  ()方法,并设置  -XX  :     MaxTenuringThreshold=15  ,会发现运行结果中  Survivor  的空间占用仍然为  0%  ,而老年代比预     期增长了  6%  ,也就是说,  allocation1  、  allocation2  对象都直接进入了老年代,而没有等到  15     岁的临界年龄。由于这两个对象加起来已经到达了  512KB  ,并且它们是同年的,满足同年对     象到达  Survivor  空间的一半规则。我们只要注释掉此中一个对象  new  操作,就会发现另外一个     就不会晋升到老年代中去了。     代码清单  3-8   动态对象年龄判定     private static final int_1MB=1024*1024  ;     /**     *VM  参数:  -verbose  :  gc-Xms20M-Xmx20M-Xmn10M-XX  :  +PrintGCDetails-XX  :  SurvivorRatio=8-XX  :  MaxTenuringThreshold=15     *-XX  :  +PrintTenuringDistribution     */     @SuppressWarnings  (  "unused"  )     public static void testTenuringThreshold2  ()  {     byte[]allocation1  ,  allocation2  ,  allocation3  ,  allocation4  ;     allocation1=new byte[ 1MB/4]  ;     //allocation1+allocation2  大于  survivo  空间一半     _     allocation2=new byte[_1MB/4]  ;     allocation3=new byte[4*_1MB]  ;     allocation4=new byte[4*_1MB]  ;     allocation4=null  ;     allocation4=new byte[4*_1MB]  ;     }     运行结果:     [GC[DefNew     Desired Survivor size 524288 bytes,new threshold 1  (  max 15  )     -age 1  :  676824 bytes  ,  676824 total     :  5115K-  >  660K  (  9216K  ),  0.0050136 secs]5115K-  >  4756K  (  19456K  ),  0.0050443 secs][Times  :  user=0.00 sys=0.01  ,  real=0.01 secs]     [GC[DefNew     Desired Survivor size 524288 bytes,new threshold 15  (  max 15  )     :  4756K-  >  0K  (  9216K  ),  0.0010571 secs]8852K-  >  4756K  (  19456K  ),  0.0011009 secs][Times  :  user=0.00 sys=0.00  ,  real=0.00 secs]     Heap     def new generation total 9216K,used 4178K[0x029d0000  ,  0x033d0000  ,  0x033d0000  )     eden space 8192K  ,  51%used[0x029d0000  ,  0x02de4828  ,  0x031d0000  )     from space 1024K  ,  0%used[0x031d0000  ,  0x031d0000  ,  0x032d0000  )     to space 1024K  ,  0%used[0x032d0000  ,  0x032d0000  ,  0x033d0000  )     tenured generation total 10240K,used 4756K[0x033d0000  ,  0x03dd0000  ,  0x03dd0000  )     the space 10240K  ,  46%used[0x033d0000  ,  0x038753e8  ,  0x03875400  ,  0x03dd0000  )     compacting perm gen total 12288K,used 2114K[0x03dd0000  ,  0x049d0000  ,  0x07dd0000  )     the space 12288K  ,  17%used[0x03dd0000  ,  0x03fe09a0  ,  0x03fe0a00  ,  0x049d0000  )     No shared spaces configured.  3.6.5   空间分配包管     在发生  Minor GC  之前,假造机会先查抄老年代最大可用的连续空间是否大于新生代所有     对象总空间,假如这个条件建立,那么  Minor GC  可以确保是安全的。假如不建立,则假造机     会检察  HandlePromotionFailure  设置值是否答应包管失败。假如答应,那么会继承查抄老年代     最大可用的连续空间是否大于历次晋升到老年代对象的均匀巨细,假如大于,将实验着举行     一次  Minor GC  ,只管这次  Minor GC  是有风险的;假如小于,大概  HandlePromotionFailure  设置     不答应冒险,那这时也要改为举行一次  Full GC  。     下面解释一下  “  冒险  ”  是冒了什么风险,前面提到过,新生代利用复制收集算法,但为了     内存利用率,只利用此中一个  Survivor  空间来作为轮换备份,因此当出现大量对象在  Minor     GC  后仍然存活的环境(最极端的环境就是内存接纳后新生代中所有对象都存活),就须要     老年代举行分配包管,把  Survivor  无法容纳的对象直接进入老年代。与生活中的贷款包管类     似,老年代要举行这样的包管,条件是老年代本身尚有容纳这些对象的剩余空间,一共有多     少对象会活下来在实际完成内存接纳之前是无法明白知道的,以是只好取之前每一次接纳晋     升到老年代对象容量的均匀巨细值作为履历值,与老年代的剩余空间举行比较,决定是否进     行  Full GC  来让老年代腾出更多空间。     取均匀值举行比较实在仍然是一种动态概率的本领,也就是说,假如某次  Minor GC  存活     后的对象突增,远远高于均匀值的话,依然会导致包管失败(  Handle Promotion Failure  )。     假如出现了  HandlePromotionFailure  失败,那就只好在失败后重新发起一次  Full GC  。虽然包管     失败时绕的圈子是最大的,但大部分环境下都还是会将  HandlePromotionFailure  开关打开,避     免  Full GC  过于频仍,拜见代码清单  3-9  ,请读者在  JDK 6 Update 24  之前的版本中运行测试。     代码清单  3-9   空间分配包管     private static final int_1MB=1024*1024  ;     /**     *VM  参数:  -Xms20M-Xmx20M-Xmn10M-XX  :  +PrintGCDetails-XX  :  SurvivorRatio=8-XX  :  -HandlePromotionFailure     */     @SuppressWarnings  (  "unused"  )     public static void testHandlePromotion  ()  {     byte[]allocation1  ,  allocation2  ,  allocation3  ,  allocation4  ,  allocation5  ,  allocation6  ,  allocation7  ;     allocation1=new byte[2*_1MB]  ;     allocation2=new byte[2*_1MB]  ;     allocation3=new byte[2*_1MB]  ;     allocation1=null  ;     allocation4=new byte[2*_1MB]  ;     allocation5=new byte[2*_1MB]  ;     allocation6=new byte[2*_1MB]  ;     allocation4=null  ;     allocation5=null  ;     allocation6=null  ;     allocation7=new byte[2*_1MB]  ;     }     以  HandlePromotionFailure=false  参数来运行的结果:     [GC[DefNew  :  6651K-  >  148K  (  9216K  ),  0.0078936 secs]6651K-  >  4244K  (  19456K  ),  0.0079192 secs][Times  :  user=0.00 sys=0.02  ,  real=0.02 secs]     [G C[D e f N e w  :  6 3 7 8 K-  >  6 3 7 8 K  (  9 2 1 6 K  ),  0.0 0 0 0 2 0 6 s e c s][T e n u r e d  :  4096K-  >  4244K  (  10240K  ),  0.0042901 secs]10474K-  >     4244K  (  19456K  ),  [Perm  :  2104K-  >  2104K  (  12288K  )  ]  ,  0.0043613 secs][Times  :  user=0.00 sys=0.00  ,  real=0.00 secs]     以  HandlePromotionFailure=true  参数来运行的结果:     [GC[DefNew  :  6651K-  >  148K  (  9216K  ),  0.0054913 secs]6651K-  >  4244K  (  19456K  ),  0.0055327 secs][Times  :  user=0.00 sys=0.00  ,  real=0.00 secs]     [GC[DefNew  :  6378K-  >  148K  (  9216K  ),  0.0006584 secs]10474K-  >  4244K  (  19456K  ),  0.0006857 secs][Times  :  user=0.00 sys=0.00  ,  real=0.00 secs]     在  JDK 6 Update 24  之后,这个测试结果会有差别,  HandlePromotionFailure  参数不会再影  响到假造机的空间分配包管计谋,观察  OpenJDK  中的源码厘革(见代码清单  3-10  ),虽然源     码中还定义了  HandlePromotionFailure  参数,但是在代码中已经不会再利用它。  JDK 6 Update     24  之后的规则变为只要老年代的连续空间大于新生代对象总巨细大概历次晋升的均匀巨细就     会举行  Minor GC  ,否则将举行  Full GC  。     代码清单  3-10 HotSpot  中空间分配查抄的代码片段     bool TenuredGeneration  :  promotion_attempt_is_safe  (  size_t     max    _    promotion    _    in    _    bytes  )  const{     //  老年代最大可用的连续空间     size    _    t available=max    _    contiguous    _    available  ();     //  每次晋升到老年代的均匀巨细     size    _    t av    _    promo=  (  size    _    t  )  gc    _    stats  ()  -  >  avg    _    promoted  ()  -  >  padded    _    average  ();     //  老年代可用空间是否大于均匀晋升巨细,大概老年代可用空间是否大于当此  GC  时新生代所有对象容量     bool res=  (  available  >  =av_promo  )  ||  (  available  >  =     max_promotion_in_bytes  );     return res  ;     }  3.7   本章小结     本章介绍了垃圾收集的算法、几款  JDK 1.7  中提供的垃圾收集器特点以及运作原理。通     过代码实例验证了  Java  假造机中自动内存分配及接纳的重要规则。     内存接纳与垃圾收集器在很多时候都是影响系统性能、并发本领的重要因素之一,假造     机之以是提供多种不同的收集器以及提供大量的调节参数,是由于只有根据实际应用需求、     实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合,也没有最     优的调优方法,假造机也就没有什么必然的内存接纳行为。因此,学习假造机内存知识,如     果要到实践调优阶段,那么必须了解每个具体收集器的行为、优势和劣势、调节参数。在接     下来的两章中,作者将会介绍内存分析的工具和调优的一些具体案例。  第  4  章 假造机性能监控与故障处理惩罚工具     Java  与  C++  之间有一堵由内存动态分配和垃圾收集技术所围成的  “  高墙  ”  ,墙表面的人想     进去,墙里面的人却想出来。     4.1   概述     颠末前面两章对于假造机内存分配与接纳技术各方面的介绍,相信读者已经建立了一套     比较完备的理论基础。理论总是作为指导实践的工具,能把这些知识应用到实际工作中才是     我们的最终目标。接下来的两章,我们将从实践的角度去了解假造机内存管理的天下。     给一个系统定位标题标时候,知识、履历是关键基础,数据是依据,工具是运用知识处     理数据的本领。这里说的数据包罗:运行日记、异常堆栈、  GC  日记、线程快照     (  threaddump/javacore  文件)、堆转储快照(  heapdump/hprof  文件)等。常常利用适当的假造     机监控和分析的工具可以加速我们分析数据、定位办理标题标速率,但在学习工具前,也应     当意识到工具永远都是知识技能的一层包装,没有什么工具是  “  秘密武器  ”  ,不可能学会了就     能包治百病。  4.2 JDK  的命令行工具     Java  开发人员肯定都知道  JDK  的  bin  目次中有  “java.exe”  、  “javac.exe”  这两个命令行工具,     但并非所有程序员都了解过  JDK  的  bin  目次之中其他命令行程序的作用。每逢  JDK  更新版本之     时,  bin  目次下命令行工具的数量和功能总会不知不觉地增长和增强。  bin  目次的内容如图  4-1     所示。     在本章中,笔者将介绍这些工具的此中一部分,重要包罗用于监视假造机和故障处理惩罚的     工具。这些故障处理惩罚工具被  Sun  公司作为  “  礼物  ”  附赠给  JDK  的利用者,并在软件的利用说明中     把它们声明为  “  没有技术支持并且是实验性子的  ”  (  unsupported and experimental  )    [1]    的产物,但     事实上,这些工具都非常稳定而且功能强盛,能在处理惩罚应用程序性能标题、定位故障时发挥     很大的作用。     图   4-1 Sun JDK  中的工具目次     说起  JDK  的工具,比较仔细的读者,可能会注意到这些工具的程序体积都异常小巧。假     如以前没注意到,现在不妨再看看图  4-1  中的最后一列  “  巨细  ”  ,险些所有工具的体积基本上     都稳定在  27KB  左右。并非  JDK  开发团队刻意把它们制作得如此精炼来炫耀编程水平,而是因     为这些命令行工具大多数是  jdk/lib/tools.jar  类库的一层薄包装而已,它们重要的功能代码是     在  tools  类库中实现的。读者把图  4-1  和图  4-2  两张图片对比一下就可以看得很清楚。  假如读者利用的是  Linux  版本的  JDK  ,还会发现这些工具中很多乃至就是由  Shell  脚本直接     写成的,可以用  vim  直接打开它们。     JDK  开发团队选择采用  Java  代码来实现这些监控工具是有特殊用意的:当应用程序部署     到生产环境后,无论是直接接触物理服务器还是远程  Telnet  到服务器上都可能会受到限定。     借助  tools.jar  类库里面的接口,我们可以直接在应用程序中实现功能强盛的监控分析功能    [2]    。     图   4-2 tools.jar  包的内部状况     须要特殊说明的是,本章介绍的工具全部基于  Windows  平台下的  JDK 1.6 Update 21  ,如     果  JDK  版本、操作系统不同,工具所支持的功能可能会有较大差别。大部分工具在  JDK 1.5  中     就已经提供,但为了制止运行环境带来的差别和兼容性标题,建议读者利用  JDK 1.6  来验证     本章介绍的内容,由于  JDK 1.6  的工具可以正常兼容运行于  JDK 1.5  的假造机之上的程序,反     之则不一定。表  4-1  中说明了  JDK  重要命令行监控工具的用途。     注意 假如读者在工作中须要监控运行于  JDK 1.5  的假造机之上的程序,在程序启动时     请添加参数  “-Dcom.sun.management.jmxremote”  开启  JMX  管理功能,否则由于部分工具都是基     于  JMX  (包罗  4.3  节介绍的可视化工具),它们都将会无法利用,假如被监控程序运行于  JDK     1.6  的假造机之上,那  JMX  管理默认是开启的,假造机启动时无须再添加任何参数。  4.2.1 jps  :假造机历程状况工具     JDK  的很多小工具的名字都参考了  UNIX  命令的定名方式,  jps  (  JVM     Process     Status     Tool  )是此中的典型。除了名字像  UNIX  的  ps  命令之外,它的功能也和  ps  命令雷同:可以列出     正在运行的假造机历程,并显示假造机执行主类(  Main Class,main  ()函数所在的类)名称     以及这些历程的当地假造机唯一  ID  (  Local Virtual Machine Identifier,LVMID  )。虽然功能比较     单一,但它是利用频率最高的  JDK  命令行工具,由于其他的  JDK  工具大多须要输入它查询到     的  LVMID  来确定要监控的是哪一个假造机历程。对于当地假造机历程来说,  LVMID  与操作系     统的历程  ID  (  Process Identifier,PID  )是同等的,利用  Windows  的任务管理器大概  UNIX  的  ps  命     令也可以查询到假造机历程的  LVMID  ,但假犹如时启动了多个假造机历程,无法根据历程名     称定位时,那就只能依赖  jps  命令显示主类的功能才能区分了。     jsp  命令格式:     jps[options][hostid]     jps  执行样例:     D  :  \Develop\Java\jdk1.6.0_21\bin  >  jps-l     2388 D  :  \Develop\glassfish\bin\..\modules\admin-cli.jar     2764 com.sun.enterprise.glassfish.bootstrap.ASMain     3788 sun.tools.jps.Jps     jps  可以通过  RMI  协议查询开启了  RMI  服务的远程假造机历程状态,  hostid  为  RMI  注册表中     注册的主机名。  jps  的其他常用选项见表  4-2  。  [1]  http://download.oracle.com/javase/6/docs/technotes/tools/index.html  。     [2]  tools.jar  中的类库不属于  Java  的标准  API  ,假如引入这个类库,就意味着用户的程序只能运     行于  Sun     Hotspot  (或一些从  Sun  公司购买了  JDK  的源码  License  的假造机,如  IBM     J9  、  BEA     JRockit  )上面,大概在部署程序时须要一起部署  tools.jar  。  4.2.2 jstat  :假造机统计信息监视工具     jstat  (  JVM Statistics Monitoring Tool  )是用于监视假造机各种运行状态信息的命令行工     具。它可以显示当地大概远程    [1]    假造机历程中的类装载、内存、垃圾收集、  JIT  编译等运行数     据,在没有  GUI  图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位假造     机性能标题标首选工具。     jstat  命令格式为:     jstat[option vmid[interval[s|ms][count]]]     对于命令格式中的  VMID  与  LVMID  须要特殊说明一下:假如是当地假造机历程,  VMID  与     LVMID  是同等的,假如是远程假造机历程,那  VMID  的格式应当是:     [protocol  :  ][//]lvmid[@hostname[  :  port]/servername]     参数  interval  和  count  代表查询间隔和次数,假如省略这两个参数,说明只查询一次。假设     须要每  250  毫秒查询一次历程  2764  垃圾收集状况,一共查询  20  次,那命令应当是:     jstat-gc 2764 250 20     选项  option  代表着用户盼望查询的假造机信息,重要分为  3  类:类装载、垃圾收集、运行     期编译状况,具体选项及作用请参考表  4-3  中的描述。  jstat  监视选项众多,囿于版面缘故原由无法逐一演示,这里仅举监视一台刚刚启动的     GlassFish v3  服务器的内存状况的例子来演示如何检察监视结果。监视参数与输出结果如代码     清单  4-1  所示。     代码清单  4-1 jstat  执行样例     D  :  \Develop\Java\jdk1.6.0_21\bin  >  jstat-gcutil 2764     S0 S1 E O P YGC YGCT FGC FGCT GCT     0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577     查询结果表明:这台服务器的新生代  Eden  区(  E  ,体现  Eden  )利用了  6.2%  的空间,两个     Survivor  区(  S0  、  S1  ,体现  Survivor0  、  Survivor1  )里面都是空的,老年代(  O  ,体现  Old  )和     永世代(  P  ,体现  Permanent  )则分别利用了  41.42%  和  47.20%  的空间。程序运行以来共发生     Minor GC  (  YGC  ,体现  Young GC  )  16  次,总耗时  0.105  秒,发生  Full GC  (  FGC  ,体现  Full     GC  )  3  次,  Full GC  总耗时(  FGCT  ,体现  Full GC Time  )为  0.472  秒,所有  GC  总耗时(  GCT  ,     体现  GC Time  )为  0.577  秒。     利用  jstat  工具在纯文本状态下监视假造机状态的厘革,确实不如后面将会提到的     VisualVM  等可视化的监视工具直接以图表显现那样直观。但很多服务器管理员都风俗了在文     本控制台中工作,直接在控制台中利用  jstat  命令依然是一种常用的监控方式。     [1]  须要远程主机提供  RMI  支持,  Sun  提供的  jstatd  工具可以很方便地建立远程  RMI  服务器。  4.2.3 jinfo  :  Java  配置信息工具     jinfo  (  Configuration Info for Java  )的作用是及时地检察和调解假造机各项参数。利用  jps     命令的  -v  参数可以检察假造机启动时显式指定的参数列表,但假如想知道未被显式指定的参     数的系统默认值,除了去找资料外,就只能利用  jinfo  的  -flag  选项举行查询了(假如只限于     JDK 1.6  或以上版本的话,利用  java-XX  :  +PrintFlagsFinal  检察参数默认值也是一个很好的选     择),  jinfo  还可以利用  -sysprops  选项把假造机历程的  System.getProperties  ()的内容打印出     来。这个命令在  JDK     1.5  时期已经随着  Linux  版的  JDK  发布,当时只提供了信息查询的功     能,  JDK     1.6  之后,  jinfo  在  Windows  和  Linux  平台都有提供,并且到场了运行期修改参数的能     力,可以利用  -flag[+|-]name  大概  -flag     name=value  修改一部分运行期可写的假造机参数值。     JDK 1.6  中,  jinfo  对于  Windows  平台功能仍然有较大限定,只提供了最基本的  -flag  选项。     jinfo  命令格式:     jinfo[option]pid     执行样例:查询  CMSInitiatingOccupancyFraction  参数值。     C  :  \  >  jinfo-flag CMSInitiatingOccupancyFraction 1444     -XX  :  CMSInitiatingOccupancyFraction=85  4.2.4 jmap  :  Java  内存映像工具     jmap  (  Memory Map for Java  )命令用于生成堆转储快照(一般称为  heapdump  或  dump  文     件)。假如不利用  jmap  命令,要想获取  Java  堆转储快照,尚有一些比较  “  暴力  ”  的本领:譬如     在第  2  章中用过的  -XX  :  +HeapDumpOnOutOfMemoryError  参数,可以让假造机在  OOM  异常出     现之后自动生成  dump  文件,通过  -XX  :  +HeapDumpOnCtrlBreak  参数则可以利用  [Ctrl]+[Break]     键让假造机生成  dump  文件,又大概在  Linux  系统下通过  Kill-3  命令发送历程退出信号  “  恐吓  ”  一     下假造机,也能拿到  dump  文件。     jmap  的作用并不仅仅是为了获取  dump  文件,它还可以查询  finalize  执行队列、  Java  堆和永     久代的具体信息,如空间利用率、当前用的是哪种收集器等。     和  jinfo  命令一样,  jmap  有不少功能在  Windows  平台下都是受限的,除了生成  dump  文件的  -     dump  选项和用于检察每个类的实例、空间占用统计的  -histo  选项在所有操作系统都提供之     外,别的选项都只能在  Linux/Solaris  下利用。     jmap  命令格式:     jmap[option]vmid     option  选项的正当值与具体含义见表  4-4  。     代码清单  4-2  是利用  jmap  生成一个正在运行的  Eclipse  的  dump  快照文件的例子,例子中的     3500  是通过  jps  命令查询到的  LVMID  。     代码清单  4-2   利用  jmap  生成  dump  文件  C  :  \Users\IcyFenix  >  jmap-dump  :  format=b,file=eclipse.bin 3500     Dumping heap to C  :  \Users\IcyFenix\eclipse.bin……     Heap dump file created  4.2.5 jhat  :假造机堆转储快照分析工具     Sun JDK  提供  jhat  (  JVM Heap Analysis Tool  )命令与  jmap  搭配利用,来分析  jmap  生成的堆     转储快照。  jhat  内置了一个微型的  HTTP/HTML  服务器,生成  dump  文件的分析结果后,可以在     欣赏器中检察。不外实事求是地说,在实际工作中,除非笔者手上真的没有别的工具可用,     否则一般都不会去直接利用  jhat  命令来分析  dump  文件,重要缘故原由有二:一是一般不会在部署     应用程序的服务器上直接分析  dump  文件,纵然可以这样做,也会只管将  dump  文件复制到其     他呆板    [1]    上举行分析,由于分析工作是一个耗时而且消耗硬件资源的过程,既然都要在其他     呆板举行,就没有须要受到命令行工具的限定了;另一个缘故原由是  jhat  的分析功能相对来说比     较简陋,后文将会介绍到的  VisualVM  ,以及专业用于分析  dump  文件的  Eclipse     Memory     Analyzer  、  IBM HeapAnalyzer     [2]    等工具,都能实现比  jhat  更强盛更专业的分析功能。代码清单  4-     3  演示了利用  jhat  分析  4.2.4  节中采用  jmap  生成的  Eclipse IDE  的内存快照文件。     代码清单  4-3   利用  jhat  分析  dump  文件     C  :  \Users\IcyFenix  >  jhat eclipse.bin     Reading from eclipse.bin……     Dump file created Fri Nov 19 22  :  07  :  21 CST 2010     Snapshot read,resolving……     Resolving 1225951 objects……     Chasing references,expect 245 dots……     Eliminating duplicate references……     Snapshot resolved.     Started HTTP server on port 7000     Server is ready.     屏幕显示  “Server is ready.”  的提示后,用户在欣赏器中键入  http://localhost  :  7000/  就可以     看到分析结果,如图  4-3  所示。  图   4-3 jhat  的分析结果     分析结果默认是以包为单位举行分组显示,分析内存泄漏标题重要会利用到此中     的  “Heap Histogram”  (与  jmap-histo  功能一样)与  OQL  页签的功能,前者可以找到内存中总容     量最大的对象,后者是标准的对象查询语言,利用雷同  SQL  的语法对内存中的对象举行查询     统计,读者若对  OQL  有爱好的话,可以参考本书附录  D  的介绍。     [1]  用于分析的呆板一般也是服务器,由于加载  dump  快照文件须要比生成  dump  更大的内存,     以是一般在  64  位  JDK  、大内存的服务器上举行。     [2]  IBM HeapAnalyzer  用于分析  IBM J9  假造机生成的映像文件,各个假造机产生的映像文件格     式并不同等,以是分析工具也不能通用。  4.2.6 jstack  :  Java  堆栈跟踪工具     jstack  (  Stack Trace for Java  )命令用于生成假造机当前时刻的线程快照(一般称为     threaddump  大概  javacore  文件)。线程快照就是当前假造机内每一条线程正在执行的方法堆栈     的聚集,生成线程快照的重要目标是定位线程出现长时间停顿的缘故原由,如线程间死锁、死循     环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见缘故原由。线程出现停顿     的时候通过  jstack  来检察各个线程的调用堆栈,就可以知道没有相应的线程到底在后台做些     什么事情,大概等待着什么资源。     jstack  命令格式:     jstack[option]vmid     option  选项的正当值与具体含义见表  4-5  。     代码清单  4-4  是利用  jstack  检察  Eclipse  线程堆栈的例子,例子中的  3500  是通过  jps  命令查询     到的  LVMID  。     代码清单  4-4   利用  jstack  检察线程堆栈(部分结果)     C  :  \Users\IcyFenix  >  jstack-l 3500     2010-11-19 23  :  11  :  26     Full thread dump Java HotSpot  (  TM  )  64-Bit Server VM  (  17.1-b03 mixed mode  ):     "[ThreadPool Manager]-Idle Thread"daemon prio=6 tid=0x0000000039dd4000 nid=0xf50 in Object.wait  ()  [0x000000003c96f000]     java.lang.Thread.State  :  WAITING  (  on object monitor  )     at java.lang.Object.wait  (  Native Method  )     -waiting on  <  0x0000000016bdcc60  >(  a org.eclipse.equinox.internal.util.impl.tpt.threadpool.Executor  )     at java.lang.Object.wait  (  Object.java  :  485  )     at org.eclipse.equinox.internal.util.impl.tpt.threadpool.Executor.run  (  Executor.java  :  106  )     -locked  <  0x0000000016bdcc60  >(  a org.eclipse.equinox.internal.util.impl.tpt.threadpool.Executor  )     Locked ownable synchronizers  :     -None     在  JDK     1.5  中,  java.lang.Thread  类新增了一个  getAllStackTraces  ()方法用于获取假造机     中所有线程的  StackTraceElement  对象。利用这个方法可以通过简朴的几行代码就完成  jstack  的     大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时利用欣赏器来检察     线程堆栈,如代码清单  4-5  所示,这是笔者的一个小履历。     代码清单  4-5   检察线程状况的  JSP  页面     <  %@page import="java.util.Map"%  >     <  html  >     <  head  >     <  title  >服务器线程信息<  /title  >     <  /head  >     <  body  >     <  pre  >     <  %     for  (  Map.Entry  <  Thread,StackTraceElement[]  >  stackTrace  :  Thread.     getAllStackTraces  ()  .entrySet  ())  {     Thread thread=  (  Thread  )  stackTrace.getKey  ();     StackTraceElement[]stack=  (  StackTraceElement[]  )  stackTrace.getValue  ();  if  (  thread.equals  (  Thread.currentThread  ()))  {     continue  ;     }     out.print  (  "\n  线程:  "+thread.getName  ()  +"\n"  );     for  (  StackTraceElement element  :  stack  )  {     out.print  (  "\t"+element+"\n"  );     }     }     %  >     <  /pre  >     <  /body  >     <  /html  >  4.2.7 HSDIS  :  JIT  生成代码反汇编     在  Java  假造机规范中,具体描述了假造机指令会合每条指令的执行过程、执行前后对操     作数栈、局部变量表的影响等细节。这些细节描述与  Sun  的早期假造机(  Sun Classic VM  )高     度吻合,但随着技术的发展,高性能假造机真正的细节实现方式已经徐徐与假造机规范所描     述的内容产生了越来越大的差距,假造机规范中的描述逐渐成了假造机实现的  “  概念模     型  ”——  即实现只能包管规范描述等效。基于这个缘故原由,我们分析程序的执行语义标题(虚     拟机做了什么)时,在字节码层面上分析完全可行,但分析程序的执行行为标题(假造机是     怎样做的、性能如何)时,在字节码层面上分析就没有什么意义了,须要通过其他方式解     决。     分析程序如何执行,通过软件调试工具(  GDB  、  Windbg  等)来断点调试是最常见的手     段,但是这样的调试方式在  Java  假造机中会遇到很大困难,由于大量执行代码是通过  JIT  编译     器动态生成到  CodeBuffer  中的,没有很简朴的本领来处理惩罚这种混合模式的调试(不外相信虚     拟机开发团队内部肯定是有内部工具的)。因此,不得不通过一些特殊的本领来办理标题,     基于这种背景,本节的主角  ——HSDIS  插件就正式登场了。     HSDIS  是一个  Sun  官方推荐的  HotSpot  假造机  JIT  编译代码的反汇编插件,它包罗在  HotSpot     假造机的源码之中,但没有提供编译后的程序。在  Project Kenai  的网站    [1]    也可以下载到单独的     源码。它的作用是让  HotSpot  的  -XX  :  +PrintAssembly  指令调用它来把动态生成的当地代码还     原为汇编代码输出,同时还生成了大量非常有价值的注释,这样我们就可以通过输出的代码     来分析标题。读者可以根据本身的操作系统和  CPU  范例从  Project Kenai  的网站上下载编译好     的插件,直接放到  JDK_HOME/jre/bin/client  和  JDK_HOME/jre/bin/server  目次中即可。假如没     有找到所需操作系统(譬如  Windows  的就没有)的成品,那就得本身利用源码编译一下    [2]    。     还须要注意的是,假如读者利用的是  Debug  大概  FastDebug  版的  HotSpot  ,那可以直接通     过  -XX  :  +PrintAssembly  指令利用插件;假如利用的是  Product  版的  HotSpot  ,那还要额外到场     一个  -XX  :  +UnlockDiagnosticVMOptions  参数。笔者以代码清单  4-6  中的简朴测试代码为例演     示一下这个插件的利用。     代码清单  4-6   测试代码     public class Bar{     int a=1  ;     static int b=2  ;     public int sum  (  int c  )  {     return a+b+c  ;     }     public static void main  (  String[]args  )  {     new Bar  ()  .sum  (  3  );     }     }     编译这段代码,并利用以下命令执行。     java-XX  :  +PrintAssembly-Xcomp-XX  :  CompileCommand=dontinline  ,  *Bar.sum-XX  :  Compi leCommand=compileonly  ,  *Bar.sum test.Bar     此中,参数  -Xcomp  是让假造机以编译模式执行代码,这样代码可以  “  偷懒  ”  ,不须要执行     充足次数来预热就能触发  JIT  编译    [3]    。两个  -XX  :  CompileCommand  意思是让编译器不要内联     sum  ()并且只编译  sum  (),  -XX  :  +PrintAssembly  就是输出反汇编内容。假如统统顺利的     话,那么屏幕上会出现雷同下面代码清单  4-7  所示的内容。  代码清单  4-7   测试代码     [Disassembling for mach='i386']     [Entry Point]     [Constants]     #{method}'sum''  (  I  )  I'in'test/Bar'     #this  :  ecx='test/Bar'     #parm0  :  edx=int     #[sp+0x20]  (  sp of caller  )     ……     0x01cac407  :  cmp 0x4  (  %ecx  ),  %eax     0x01cac40a  :  jne 0x01c6b050  ;  {runtime_call}     [Verified Entry Point]     0x01cac410  :  mov%eax  ,  -0x8000  (  %esp  )     0x01cac417  :  push%ebp     0x01cac418  :  sub$0x18  ,  %esp  ;  *aload_0     ;  -test.Bar  :  sum@0  (  line 8  )     ;  block B0[0  ,  10]     0x01cac41b  :  mov 0x8  (  %ecx  ),  %eax  ;  *getfield a     ;  -test.Bar  :  sum@1  (  line 8  )     0x01cac41e  :  mov$0x3d2fad8  ,  %esi  ;  {oop  (  a     'java/lang/Class'='test/Bar'  )  }     0x01cac423  :  mov 0x68  (  %esi  ),  %esi  ;  *getstatic b     ;  -test.Bar  :  sum@4  (  line 8  )     0x01cac426  :  add%esi  ,  %eax     0x01cac428  :  add%edx  ,  %eax     0x01cac42a  :  add$0x18  ,  %esp     0x01cac42d  :  pop%ebp     0x01cac42e  :  test%eax  ,  0x2b0100  ;  {poll_return}     0x01cac434  :  ret     上段代码并不多,下面一句句举行说明。     1  )  mov%eax  ,  -0x8000  (  %esp  ):查抄栈溢。     2  )  push%ebp  :保存上一栈帧基址。     3  )  sub$0x18  ,  %esp  :给新帧分配空间。     4  )  mov 0x8  (  %ecx  ),  %eax  :取实例变量  a  ,这里  0x8  (  %ecx  )就是  ecx+0x8  的意思,前     面  “[Constants]”  节中提示了  “this  :  ecx='test/Bar'”  ,即  ecx  寄存器中放的就是  this  对象的所在。偏     移  0x8  是越过  this  对象的对象头,之后就是实例变量  a  的内存位置。这次是访问  “Java  堆  ”  中的数     据。     5  )  mov$0x3d2fad8  ,  %esi  :取  test.Bar  在方法区的指针。     6  )  mov 0x68  (  %esi  ),  %esi  :取类变量  b  ,这次是访问  “  方法区  ”  中的数据。     7  )  add%esi  ,  %eax  和  add%edx  ,  %eax  :做两次加法,求  a+b+c  的值,前面的代码把  a  放在     eax  中,把  b  放在  esi  中,而  c  在  [Constants]  中提示了,  “parm0  :  edx=int”  ,说明  c  在  edx  中。     8  )  add$0x18  ,  %esp  :撤销栈帧。     9  )  pop%ebp  :规复上一栈帧。     10  )  test%eax  ,  0x2b0100  :轮询方法返回处的  SafePoint  。     11  )  ret  :方法返回。     [1]  Project Kenai  :  http://kenai.com/projects/base-hsdis  。     [2]  HLLVM  圈子中有已编译好的:  http://hllvm.group.iteye.com/  。     [3]  -Xcomp  在较新的  HotSpot  中被移除了,假如读者的假造机无法利用这个参数,请加个循环     预热代码,触发  JIT  编译。  4.3 JDK  的可视化工具     JDK  中除了提供大量的命令行工具外,尚有两个功能强盛的可视化工具:  JConsole  和     VisualVM  ,这两个工具是  JDK  的正式成员,没有被贴上  “unsupported and experimental”  的标     签。     此中  JConsole  是在  JDK     1.5  时期就已经提供的假造机监控工具,而  VisualVM  在  JDK 1.6     Update7  中才首次发布,现在已经成为  Sun  (  Oracle  )主力推动的多合一故障处理惩罚工具    [1]    ,并且     已经从  JDK  中分离出来成为可以独立发展的开源项目。     为了制止本节的讲解成为对软件说明文档的简朴翻译,笔者准备了一些代码样例,都是     笔者特意编写的  “  反面教材  ”  。后面将会利用这两款工具去监控、分析这几段代码存在的问     题,算是本节简朴的实战分析。读者可以把在可视化工具观察到的数据、现象,与前面两章     中讲解的理论知识互相印证。     4.3.1 JConsole  :  Java  监视与管理控制台     JConsole  (  Java Monitoring and Management Console  )是一种基于  JMX  的可视化监视、管     理工具。它管理部分的功能是针对  JMX MBean  举行管理,由于  MBean  可以利用代码、中间件     服务器的管理控制台大概所有符合  JMX  规范的软件举行访问,以是本节将会着重介绍     JConsole  监视部分的功能。     1.  启动  JConsole     通过  JDK/bin  目次下的  “jconsole.exe”  启动  JConsole  后,将自动搜刮出本机运行的所有假造     机历程,不须要用户本身再利用  jps  来查询了,如图  4-4  所示。双击选择此中一个历程即可开     始监控,也可以利用下面的  “  远程历程  ”  功能来连接远程服务器,对远程假造机举行监控。  图   4-4 JConsole  连接页面     从图  4-4  可以看出,笔者的呆板现在运行了  Eclipse  、  JConsole  和  MonitoringTest  三个当地虚     拟机历程,此中  MonitoringTest  就是笔者准备的  “  反面教材  ”  代码之一。双击它进入  JConsole  主     界面,可以看到主界面里共包罗  “  概述  ”  、  “  内存  ”  、  “  线程  ”  、  “  类  ”  、  “VM  择要  ”  、  “MBean”6  个     页签,如图  4-5  所示。  图   4-5 JConsole  主界面     “  概述  ”  页签显示的是整个假造机重要运行数据的概览,此中包罗  “  堆内存利用环境  ”  、     “  线程  ”  、  “  类  ”  、  “CPU  利用环境  ”4  种信息的曲线图,这些曲线图是后面  “  内存  ”  、  “  线程  ”  、     “  类  ”  页签的信息汇总,具体内容将在后面介绍。     2.  内存监控     “  内存  ”  页签相称于可视化的  jstat  命令,用于监视受收集器管理的假造机内存(  Java  堆和     永世代)的厘革趋势。我们通过运行代码清单  4-8  中的代码来体验一下它的监视功能。运行     时设置的假造机参数为:  -Xms100m-Xmx100m-XX  :  +UseSerialGC  ,这段代码的作用是以     64KB/50  毫秒的速率往  Java  堆中添补数据,一共添补  1000  次,利用  JConsole  的  “  内存  ”  页签举行     监视,观察曲线和柱状指示图的厘革。     代码清单  4-8 JConsole  监视代码  /**     *  内存占位符对象,一个  OOMObject  约莫占  64KB     */     static class OOMObject{     public byte[]placeholder=new byte[64*1024]  ;     }     public static void fillHeap  (  int num  )  throws InterruptedException{     List  <  OOMObject  >  list=new ArrayList  <  OOMObject  >();     for  (  int i=0  ;  i  <  num  ;  i++  )  {     //  稍作延时,令监视曲线的厘革更加显着     Thread.sleep  (  50  );     list.add  (  new OOMObject  ());     }     System.gc  ();     }     public static void main  (  String[]args  )  throws Exception{     fillHeap  (  1000  );     }     程序运行后,在  “  内存  ”  页签中可以看到内存池  Eden  区的运行趋势呈现折线状,如图  4-6     所示。而监视范围扩大至整个堆后,会发现曲线是一条向上增长的平滑曲线。并且从柱状图     可以看出,在  1000  次循环执行结束,运行了  System.gc  ()后,虽然整个新生代  Eden  和     Survivor  区都基本被清空了,但是代表老年代的柱状图仍然保持峰值状态,说明被添补进堆     中的数据在  System.gc  ()方法执行之后仍然存活。笔者的分析到此为止,现提两个小标题供     读者思索一下,答案稍后给出。     1  )假造机启动参数只限定了  Java  堆为  100MB  ,没有指定  -Xmn  参数,能否从监控图中估     计出新生代有多大?     2  )为何执行了  System.gc  ()之后,图  4-6  中代表老年代的柱状图仍然显示峰值状态,代     码须要如何调解才能让  System.gc  ()接纳掉添补到堆中的对象?  图   4-6 Eden  区内存厘革状况     标题  1  答案:图  4-6  显示  Eden  空间为  27 328KB  ,由于没有设置  -XX  :  SurvivorRadio  参数,     以是  Eden  与  Survivor  空间比例为默认值  8:1  ,整个新生代空间约莫为  27     328KB×125%=34     160KB  。     标题  2  答案:执行完  System.gc  ()之后,空间未能接纳是由于  List  <  OOMObject  >  list  对象     仍然存活,  fillHeap  ()方法仍然没有退出,因此  list  对象在  System.gc  ()执行时仍然处于作     用域之内    [2]    。假如把  System.gc  ()移动到  fillHeap  ()方法外调用就可以接纳掉全部内存。     3.  线程监控     假如上面的  “  内存  ”  页签相称于可视化的  jstat  命令的话,  “  线程  ”  页签的功能相称于可视化     的  jstack  命令,遇到线程停顿时可以利用这个页签举行监控分析。前面讲解  jstack  命令的时候     提到过线程长时间停顿的重要缘故原由重要有:等待外部资源(数据库连接、网络资源、装备资  源等)、死循环、锁等待(活锁和死锁)。通过代码清单  4-9  分别演示一下这几种环境。     代码清单  4-9   线程等待演示代码     /**     *  线程死循环演示     */     public static void createBusyThread  ()  {     Thread thread=new Thread  (  new Runnable  ()  {     @Override     public void run  ()  {     while  (  true  )  //  第  41  行     ;     }     }  ,  "testBusyThread"  );     thread.start  ();     }     /**     *  线程锁等待演示     */     public static void createLockThread  (  final Object lock  )  {     Thread thread=new Thread  (  new Runnable  ()  {     @Override     public void run  ()  {     synchronized  (  lock  )  {     try{     lock.wait  ();     }catch  (  InterruptedException e  )  {     e.printStackTrace  ();     }     }     }     }  ,  "testLockThread"  );     thread.start  ();     }     public static void main  (  String[]args  )  throws Exception{     BufferedReader br=new BufferedReader  (  new InputStreamReader  (  System.in  ));     br.readLine  ();     createBusyThread  ();     br.readLine  ();     Object obj=new Object  ();     createLockThread  (  obj  );     }     程序运行后,首先在  “  线程  ”  页签中选择  main  线程,如图  4-7  所示。堆栈追踪显示     BufferedReader  在  readBytes  方法中等待  System.in  的键盘输入,这时线程为  Runnable  状     态,  Runnable  状态的线程会被分配运行时间,但  readBytes  方法查抄到流没有更新时会立刻归     还执行令牌,这种等待只消耗很小的  CPU  资源。     图   4-7 main  线程     接着监控  testBusyThread  线程,如图  4-8  所示,  testBusyThread  线程一直在执行空循环,从     堆栈追踪中看到一直在  MonitoringTest.java  代码的  41  行停留,  41  行为:  while  (  true  )。这时候     线程为  Runnable  状态,而且没有归还线程执行令牌的动作,会在空循环上用尽全部执行时间     直到线程切换,这种等待会消耗较多的  CPU  资源。  图   4-8 testBusyThread  线程     图  4-9  显示  testLockThread  线程在等待着  lock  对象的  notify  或  notifyAll  方法的出现,线程这时     候处于  WAITING  状态,在被唤醒前不会被分配执行时间。     图   4-9 testLockThread  线程     testLockThread  线程正在处于正常的活锁等待,只要  lock  对象的  notify  ()或  notifyAll  ()     方法被调用,这个线程便能激活以继承执行。代码清单  4-10  演示了一个无法再被激活的死锁     等待。     代码清单  4-10   死锁代码样例     /**     *  线程死锁等待演示     */     static class SynAddRunalbe implements Runnable{     int a,b  ;     public SynAddRunalbe  (  int a,int b  )  {     this.a=a  ;     this.b=b  ;     }     @Override     public void run  ()  {     synchronized  (  Integer.valueOf  (  a  ))  {     synchronized  (  Integer.valueOf  (  b  ))  {     System.out.println  (  a+b  );     }     }     }     }     public static void main  (  String[]args  )  {     for  (  int i=0  ;  i  <  100  ;  i++  )  {     new Thread  (  new SynAddRunalbe  (  1  ,  2  ))  .start  ();     new Thread  (  new SynAddRunalbe  (  2  ,  1  ))  .start  ();  }     }     这段代码开了  200  个线程去分别计算  1+2  以及  2+1  的值,实在  for  循环是可省略的,两个线     程也可能会导致死锁,不外那样概率太小,须要实验运行很多次才能看到效果。一般的话,     带  for  循环的版本最多运行  2  ~  3  次就会遇到线程死锁,程序无法结束。造成死锁的缘故原由是     Integer.valueOf  ()方法基于减少对象创建次数和节省内存的考虑,  [-128  ,  127]  之间的数字会     被缓存    [3]    ,当  valueOf  ()方法传入参数在这个范围之内,将直接返回缓存中的对象。也就是     说,代码中调用了  200  次  Integer.valueOf  ()方法一共就只返回了两个不同的对象。假如在某     个线程的两个  synchronized  块之间发生了一次线程切换,那就会出现线程  A  等着被线程  B  持有     的  Integer.valueOf  (  1  ),线程  B  又等着被线程  A  持有的  Integer.valueOf  (  2  ),结果出现各人都     跑不下去的情景。     出现线程死锁之后,点击  JConsole  线程面板的  “  检测到死锁  ”  按钮,将出现一个新的  “  死     锁  ”  页签,如图  4-10  所示。     图   4-10   线程死锁     图  4-10  中很清晰地显示了线程  Thread-43  在等待一个被线程  Thread-12  持有  Integer  对象,而     点击线程  Thread-12  则显示它也在等待一个  Integer  对象,被线程  Thread-43  持有,这样两个线程     就互相卡住,都不存在等到锁释放的盼望了。     [1]  VisualVM  官方站点:  https://visualvm.dev.java.net/  。     [2]  正确地说,只有在假造机利用解释器执行的时候,  “  在作用域之内  ”  才能包管它不会被回     收,由于这里的接纳还涉及局部变量表  Slot  复用、即时编译器参与时机等标题,具体读者可     参考第  8  章中关于局部变量表内存接纳的例子。     [3]  默认值,实际值取决于  java.lang.Integer.IntegerCache.high  参数的设置。  4.3.2 VisualVM  :多合一故障处理惩罚工具     VisualVM  (  All-in-One Java Troubleshooting Tool  )是到目前为止随  JDK  发布的功能最强盛     的运行监视和故障处理惩罚程序,并且可以预见在未来一段时间内都是官方主力发展的假造机故     障处理惩罚工具。官方在  VisualVM  的软件说明中写上了  “All-in-One”  的描述字样,预示着它除了     运行监视、故障处理惩罚外,还提供了很多其他方面的功能。如性能分析     (  Profiling  ),  VisualVM  的性能分析功能乃至比起  JProfiler  、  YourKit  等专业且收费的  Profiling     工具都不会逊色多少,而且  VisualVM  的尚有一个很大的优点:不须要被监视的程序基于特殊     Agent  运行,因此它对应用程序的实际性能的影响很小,使得它可以直策应用在生产环境     中。这个优点是  JProfiler  、  YourKit  等工具无法与之媲美的。     1.VisualVM  兼容范围与插件安装     VisualVM  基于  NetBeans  平台开发,因此它一开始就具备了插件扩展功能的特性,通过插     件扩展支持,  VisualVM  可以做到:     显示假造机历程以及历程的配置、环境信息(  jps  、  jinfo  )。     监视应用程序的  CPU  、  GC  、堆、方法区以及线程的信息(  jstat  、  jstack  )。     dump  以及分析堆转储快照(  jmap  、  jhat  )。     方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法。     离线程序快照:收集程序的运行时配置、线程  dump  、内存  dump  等信息建立一个快照,     可以将快照发送开发者处举行  Bug  反馈。     其他  plugins  的无穷的可能性  ……     VisualVM  在  JDK 1.6 update 7  中才首次出现,但并不意味着它只能监控运行于  JDK 1.6  上     的程序,它具备很强的向下兼容本领,乃至能向下兼容至近  10  年前发布的  JDK 1.4.2  平台    [1]    ,     这对无数已经处于实施、维护的项目很有意义。当然,并非所有功能都能完美地向下兼容,     重要特性的兼容性见表  4-6  。  首次启动  VisualVM  后,读者先不必着急找应用程序举行监测,由于现在  VisualVM  还没有     加载任何插件,虽然基本的监视、线程面板的功能主程序都以默认插件的形式提供了,但是     不给  VisualVM  装任何扩展插件,就相称于放弃了它最英华的功能,和没有安装任何应用软件     操作系统差不多。     插件可以举行手工安装,在相干网站    [2]    上下载  *.nbm  包后,点击  “  工具  ”→“  插件  ”→“  已下     载  ”  菜单,然后在弹出的对话框中指定  nbm  包路径便可举行安装,插件安装后存放在     JDK_HOME/lib/visualvm/visualvm  中。不外手工安装并不常用,利用  VisualVM  的自动安装功     能已经可以找到大多数所需的插件,在有网络连接的环境下,点击  “  工具  ”→“  插件菜单  ”  ,弹     出如图  4-11  所示的插件页签,在页签的  “  可用插件  ”  中枚举了当前版本  VisualVM  可以利用的插     件,选中插件后在右边窗口将显示这个插件的基本信息,如开发者、版本、功能描述等。  图   4-11 VisualVM  插件页签     各人可以根据本身的工作须要和爱好选择合适的插件,然后点击安装按钮,弹出如图  4-     12  所示的下载进度窗口,跟着提示操作即可完成安装。  图   4-12 VisualVM  插件安装过程     安装完插件,选择一个须要监视的程序就进入程序的主界面了,如图  4-13  所示。根据读     者选择安装插件数量的不同,看到的页签可能和图  4-13  中的有所不同。  图   4-13 VisualVM  主界面     VisualVM  中  “  概述  ”  、  “  监视  ”  、  “  线程  ”  、  “MBeans”  的功能与前面介绍的  JConsole  差别不     大,读者根据上文内容类比利用即可,下面挑选几个特色功能、插件举行介绍。     2.  生成、欣赏堆转储快照     在  VisualVM  中生成  dump  文件有两种方式,可以执行下列任一操作:     在  “  应用程序  ”  窗口中右键单击应用程序节点,然后选择  “  堆  Dump”  。     在  “  应用程序  ”  窗口中双击应用程序节点以打开应用程序标签,然后在  “  监视  ”  标签中单     击  “  堆  Dump”  。     生成了  dump  文件之后,应用程序页签将在该堆的应用程序下增长一个以  [heapdump]  开头     的子节点,并且在主页签中打开了该转储快照,如图  4-14  所示。假如须要把  dump  文件保存或     发送出去,要在  heapdump  节点上右键选择  “  另存为  ”  菜单,否则当  VisualVM  关闭时,生成的     dump  文件会被当做临时文件删除掉。要打开一个已经存在的  dump  文件,通过文件菜单中     的  “  装入  ”  功能,选择硬盘上的  dump  文件即可。  图   4-14   欣赏  dump  文件     从堆页签中的  “  择要  ”  面板可以看到应用程序  dump  时的运行时参数、     System.getProperties  ()的内容、线程堆栈等信息,  “  类  ”  面板则是以类为统计口径统计类的实     例数量、容量信息,  “  实例  ”  面板不能直接利用,由于不能确定用户想检察哪个类的实例,所     以须要通过  “  类  ”  面板进入,在  “  类  ”  中选择一个关心的类后双击鼠标,即可在  “  实例  ”  里面看见     此类中  500  个实例的具体属性信息。  “OQL  控制台  ”  面板中就是运行  OQL  查询语句的,同  jhat  中     介绍的  OQL  功能一样。假如须要了解具体  OQL  语法和利用,可拜见本书附录  D  的内容。     3.  分析程序性能     在  Profiler  页签中,  VisualVM  提供了程序运行期间方法级的  CPU  执行时间分析以及内存分     析,做  Profiling  分析肯定会对程序运行性能有比较大的影响,以是一般不在生产环境中利用     这项功能。     要开始分析,先选择  “CPU”  和  “  内存  ”  按钮中的一个,然后切换到应用程序中对程序举行     操作,  VisualVM  会记录到这段时间中应用程序执行过的方法。假如是  CPU  分析,将会统计每     个方法的执行次数、执行耗时;假如是内存分析,则会统计每个方法关联的对象数以及这些     对象所占的空间。分析结束后,点击  “  制止  ”  按钮结束监控过程,如图  4-15  所示。  图   4-15   对应用程序举行  CPU  执行时间分析     注意 在  JDK 1.5  之后,在  Client  模式下的假造机到场并且自动开启了类共享  ——  这是一     个在多假造机历程中共享  rt.jar  中类数据以提高加载速率和节省内存的优化,而根据相干  Bug     报告的反映,  VisualVM  的  Profiler  功能可能会由于类共享而导致被监视的应用程序瓦解,以是     读者举行  Profiling  前,最好在被监视程序中利用  -Xshare  :  off  参数来关闭类共享优化。     图  4-15  中是对  Eclipse IDE  一段操作的录制和分析结果,读者分析本身的应用程序时,可     以根据实际业务的复杂水平与方法的时间、调用次数做比较,找到最有优化价值的方法。     4.BTrace  动态日记跟踪     BTrace     [3]    是一个很  “  有趣  ”  的  VisualVM  插件,本身也是可以独立运行的程序。它的作用是     在不制止目标程序运行的条件下,通过  HotSpot  假造机的  HotSwap  技术
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

勿忘初心做自己

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表