详解Hotspot JVM——生命周期、类加载、字节码验证等细节

本章概要介绍HotSpot VM,其有有三个主要组件:
VM运行时(Runtime)、JIT编译器(JIT Compiler)以及内存管理器(Memory Manager)

1. HotSpot VM的基本架构


图3-1 HotSpot Vm基本架构
如图所示,JIT编译器(Client或Server)和垃圾收集器(Serial、Throughput、CMS或G1)都是可插拔的
HotSpot为VM提供启动、线程管理、JNI(Java本地接口)等基本功能。

64位HotSpot VM对寻址对一些应用有帮助,但64位VM也带了性能损失:HotSpot VM内部Java对象表示(成为普通对象指针,Ordinary Object Pointers,或oops)的长度从32位变成了64为,导致CPU高速缓存行(CPU Cache Line)中可用的oops变少,从而降低了CPU缓存的效率。
缓存效率的降低常常导致性能比32位JVM下降8%~15%,最新的HopSpot VM添加了压缩指针(Compressed oops, -XX:+UseCompressedOops开启)的新特性。同时在一些ing台上也可以使用更多的CPU寄存器。

2. HotSpot VM运行时

某些情况下只需要调整VM运行时环境的选项参数就可以显著改善Java应用的性能

HotSpot VM运行时换担当许多职责,包括命令行选项解析、VM生命周期管理、类加载、字节码解释、异常处理、同步、线程管理、Java本地接口、VM致命错误处理和C++(非Java)堆管理

2.1 命令行选项

HotSpot VM运行时可以解析命令行选项。
命令行选项可以指定选择哪个JIT编译器、选择何种垃圾收集器等,还有一些经启动器处理后传给完成启动的HotSpot VM,例如指定Java堆的大小。

命令行选项主要有3类:标准选项、非标准选项和非稳定选项

  • 标准选项:时JVM Specification要求所有Java虚拟机都必须实现的选项。
  • 非标准选项(Nonstandard option)以-X位前缀,不保证、也不强制所有JVM都必须支持,他可能未经通知就在Java SDK更改。
  • 非稳定选项(Developer Option)以-XX位前缀,通常为了支持特定需要而对JVM的运行进行校正,并且可能需要有系统配置参数的访问权限

命令行选项用于控制HotSpot VM的内部变了,每个变量都有类型和默认值。

  • Boolean类型:在HotSpot VM命令行上添加或去掉它就可以控制这些变量
    • 对于带有布尔标记的非稳定选项来说,选项名前的+或-表示true或false。比如:-XX:+AggressiveOpts
  • Integer类型:-XX:OptionName=,且整数后面可以接后缀k、m、g,表示千、百万以及十亿。

2.2 VM生命周期

HotSpot VM运行时系统负责启动和停止HotSpot VM

启动器

启动HotSot VM的组件时启动器

HotSpot VM有若干个启动器。

  • Unixt/Linux:常用的是Java
  • Windows:java和Javaw,也可以通过JNI接口JNI_CreateJavaVM启动内嵌的JVM
  • 网络:javaws,web浏览器用它来启动applet。末尾的“ws“通常指的是“web start”,而术语“Java Web Start”即指javaws。

启动过程

启动器启动HotSpot MV时会执行一系列操作,步骤如下

  1. 解析命令行选项

启动器会直接处理一些命令行选项,例如-client或-server,它们决定加载哪个JIT编译器,其他参数则传给HotSpot VM

  1. 设置堆的大小和JIT编译器

如果命令行没有指定堆大小和JIT编译器(client或server),则自行优化设置

  1. 设定环境变量如LD_LIBARARY_PATH 和 CLASSPATH
  2. 如果命令行有-jar选项,启动器则从指定JAR的manifest中查找Main-Class,否则从命令行读取Main-Class
  3. 使用标准Java本地接口(Java Native Interface,JNI)方法JNI_CreateJavaVM在新创建的线程中创建HotSpot VM

与后创建的线程相比,初始线程:是启动新进程时操作系统内核分配的第一个线程,而新疆HotSpot VM进程中运行的出时间线程也是同样的道理
不在初始线程中创建HotSpot VM,是为了可以对它进行定制,例如windows上更改栈的大小

  1. 一旦创建并初始化好HotSpot VM,就会加载Java Main-Class,启动器也会从Java Main-Class中得到Java main方法的参数
  2. HotSpot VM通过JNI方法CallStaticVoidMethod调用Java main方法,并将命令行选项传给它

至此,HotSpot VM开始正式执行命令行指定的Java程序

一旦Java程序集或者Java main方法执行结束,HotSpot VM就必须检查和清理所有程序或者方法执行过程生成的未处理异常,并将返回值返回给调用方。然后会调用Java本地接口方法DetachCurrentThread将Java main方法与HotSpot VM脱离(Detached)。

每次HotSpot VM调用DetachcurrentThread时,线程数就会减1,因此Java本地接口知道何时可以安全关闭HotSpot VM,并确保当时HotSpot VM没有正在执行的操作,Java栈中也没有激活的Java帧。

JNI_CreateJavaVM详解

  1. 确保只有一个线程调用这个方法并确保只创建一个HotSpot VM。

因为HotSpot VM创建的静态数据结构无法再次初始化,所以一旦初始化到达某个确定点后,进程空间里就只能有一个HotSpot VM。在HotSpot VM的开发工程师看来,HotSpot VM启动至此已经是无法逆转了。

  1. 检查并确保支持当前的JNI版本,初始化垃圾收集日志的输出流
  2. 初始化OS模块,如随机数生成器、当前进程ID、高精度计数器、内存页尺寸、保护页(Guard Pages)

保护页是不可访问的内存页,用作内存访问区域的边界。例如,操作系统通常在线程栈顶压入一个保护页以保证引用不会超出栈的边界

  1. 解析传入JNI_CreateJavaVM的命令行选项,保存以备将来使用。
  2. 初始化标准的Java系统属性,例如java.version、java.vendor、os.name等
  3. 初始化支持同步、栈、内存和安全点页的模块
  4. 加载libzip、libhpi、libjava及libthread等库
  5. 初始化并设置信号处理器
  6. 初始化线程库
  7. 初始化输出流日志记录器
  8. 如果用到Agent库(hprof、jdi),则初始化并启动
  9. 初始化线程状态(Thread State)和线程本地存储(Thread Local Storage),它们存储了线程私有数据
  10. 初始化部分HotSpot VM全局数据,例如事件日志(Event Log),OS同步原语、perfMemory(性能统计数据内存),以及chunkPool(内存分配器)
  11. 至此,HotSpot可以创建线程了。创建出来的Java版mian线程被关联到当前操作系统的新车,只不过还没有添加到已知线程列表中。
  12. 初始化并激活Java级别的同步
  13. 初始化类加载器(Bootclassloader)、代码缓存、解释器、JIT编译器、JNI、系统词典(System Dictionary)及universe(一种必备的全局数据结构集)
  14. 现在,添加Java主线程到已知线程列表中。检查universe是否正常。创建HotSpot VMThread,它执行HotSpot VM所有关键功能。同时发出适当的JVMTI事件,报告HotSpot VM的当前状态。
  15. 加载和初始化以下Java类:java.lang.String、Java.lang.Sytem、java.lang.Thread、java.lang.ThreadGrop、java.lang.ref.FInalizer、java.lang.Class及余下的Java系统类。此时,HotSpot初始化完毕并可使用,只是功能还不完备。
  16. 启动HotSpot VM的信号处理器线程,初始化JIT编译器并启动HotSpot编译代理线程。启动HotSpot VM辅助线程(如监控线程和统计抽样器)。至此,HotSpot VM已功能完备
  17. 最后,生产JNIEnv对象返回给调用者,HotSpot则准备响应新的JNI请求。

DestroyJavaVM详解

如果HotSpot VM启动过程中发生错误,启动器则调用DestroyJavaVM方法关闭HotSpot VM。如果HotSpot VM启动后的执行过程中发生很严重的错误,也会调用DestroyJavaVM方法

DestroyJavaVM按以下步骤停止HotSpot VM

  1. 一直等待,直到只有一个非守护的线程执行,注意此时HotSpot VM仍然可用。
  2. 调用java.lang.Shutdown.shutdown(),它会调用Java上的shutdown钩子方法,如果finalization-on-exit为true,则运行Java对象的finalizer。
  3. 运行HotSpot VM上的shutdown钩子(通过JVM_OnExit()注册),停止以下线程:性能分析器、统计数据抽样器、监控线程以及垃圾收集器线程。发出状态事件通知JVMTI,然后关闭JVMTI、停止信号线程。
  4. 调用HotSpot的JavaThread::exit()释放JNI处理块,移除保护页,并将当前线程从已知线程队列中移除。从这时起,HotSpot VM就无法执行任何Java代码了
  5. 停止HotSpot VM线程,将遗留的HotSpot VM线程带到安全点并停止JIT编译器线程
  6. 停止追踪JNI,HotSpot VM及JVMTI屏障
  7. 为那些仍然以本地代码运行的线程设置标记“vm exited"
  8. 删除当前线程
  9. 删除或移除所有的输入/输出流,释放PerfMemory(性能统计内存)资源
  10. 最后返回到调用者

2.3 VM类加载

HotSpot VM支持符合JVM规定中所定义的类加载。
HotSpot VM和Java SE类加载库共同负责类加载。
HotSpot VM负责解析常量池符号,这个过程需要加载、链接,然后初始化Java类和Java接口

术语类加载用以描述类名或接口名映射到类(Class)对象的整个过程,Java Virtual Machine Specification则更明确地定义了类加载的3个阶段:加载、链接和初始化。

类加载的最佳时机是在解析Java字节码类文件中常量池符号的时候。

类加载时机
JavaApi如:Class.forName(), Classloader.loadClass()、反射API和JNI_FindClass都可以引发类加载。
HotSpot VM本身也可以引发类加载。

类加载内容
加载类时需要加载它的所有Java超类和所有Java超接口。此外,作为链接阶段的一部分,类文件验证也需要加载一些其他类**。实际上,加载夹断时HotSpot VM和特定类加载起比如java.langClassLoader之间相互协作的过程**。

1. 类加载阶段

第一步:

对于给定的Java类或接口,类加载时会依据它的名字找到Java类的二进制类文件,定义Java类,然后创建代表整个类或结偶的java.lang.Class对象。

如果没有找到Java类或接口的二进制表示 ,就会抛出NoClassDefFound。
此外,类加载阶段会对类的格式进行语法检查,如果有错,则会抛出ClassFormatError或UnsupportedClassVersionError。
Java类加载前,HotSpot VM必须先加载它的所有超类和超接口。
如果类的继承层次有错,例如Java类是它自己的超类或超接口(类层次递归),HotSpot VM则会抛出ClassCircularityError。
如果所引用的直接超接口本身并不是接口,或者直接超类实际上是接口,HotSpot VM则会抛出IncompatibleClassChangeError
第二步:

链接的第一步是验证,检查类文件的语义、常量池符号以及类型

如果检查有错,就会抛出VerifyError
第三步:

链接的下一步是准备,他会创建静态字段,初始化为标准默认值,以及分配方法表

请注意,此时还没有执行任何Java代码。
后续:

接下来是解析符号引用,这一步是可选的。然后初始化类,运行类构造器。

这是迄今为止,类中运行的第一段代码。值得注意的是,初始化类需要首先初始化超类(不会初始化超接口)

JVM Specification规定首次使用类时进行类初始化,而Java Language Specification则允许在链接阶段符号解析时灵活处理,只要保持语言的语义不变,JVM依次执行加载、链接和初始化,保证及时抛出错误即可。
出于性能优化的考虑,通常直到类初始化时候HotSpot VM才会加载和链接类。

2. 类加载委派

当请求类加载器查找和加载某个类时,该类加载器可以转而请求别的类加载器来加载,这被称为类加载器委派

  • 初始类加载器:类的首个类加载器
  • 定义类加载器:最终定义类的类加载器

就字节码解析而言,某个类的初始类加载器是指对该类进行常量池符号解析的类加载器

类加载器的委派关系定义了二进制类的查找顺序。
Java SE类加载器的层级(由高到低)查找顺序为:启动类加载器、扩展类加载器及系统类加载器(应用程序类加载器。

  • 系统类加载器是默认的应用程序类加载器,他加载Java类的main方法并从classpath上加载类
  • 应用程序类加载器可以是JavaSE系统自带的类加载器,或者由应用程序开发人员提供。
  • 扩展类加载器则由JavaSE系统实现,他负责从JRE的lib/ext目录下加载类。

3. 启动类加载器

启动类加载器由HotSpot VM实现,负责加载BOOTCLASSPATH路径中的类,如包含JavaSE类库的rt.jar。

Client模式的HotSpotVM可以通过类型数据共享的特性使用与加载的类,这个特性默认开启,Server模式不支持;且即使是Client模式,也只有使用Serial收集器时才支持该机制

4. 类型安全

Java的类型由全限定名和类加载器唯一确定

换言之,类加载器定义了命名空间,这意味着两个不同的类加载器加载的类,即使全限定名相同,仍然是两个不同的类型。

如果有用户类加载器,HotSpotVM则需要确保类型安全不被恶意的类加载器破坏。
当类A调用B.someMethod()时,HotSpot VM会追踪和检查类加载器约束,从而确保A和B的类加载器所看到的sometMehtod()方法前面(包括参数列表和返回类型)是一致的

5. HotSpot类元数据

instanceKlass引用了与之对应的java.lang.Class实例,后者是前者的Java镜像。HotSpot VM内部使用称为klassOop的数据结构访问instanceKlass。后缀“Oop”表示普通对象指针,所以klassOop是引用java.lang.Class的HotSpot内部抽象,他是指向Klass(与Java类对应的内部表示)的普通对象指针

6. 内部的类加载数据

类加载过程中,HotSpot VM维护了3张散列表:SystemDictionary、PlaceHolderTable和LoaderConstraintTable。

  • SystemDictionary:包含已加载的类,它将建立类名/类加载器(包括初始类加载器和定义类加载器)与klassOop对象之间的映射。

目前只有在安全点才能移除SystemDictionary中的元素

  • PlaceholderTable:包含当前正在加载的类,它用于检查ClassCircularityError,多线程类加载器并行加载类时也会用到它
  • LoaderConstraintTable:用于追踪类型安全检查的约束条件

这些散列表都需要加锁以保证访问安全,在HotSpotVM中,这个锁称为SystemDictionary_lock
通常,HotSpotVM借助类加载器对象锁对加载类的过程进行序列化

2.4. 字节码验证

Java是一门类型安全的语言,官方标准的Java编译器(javac)可以生成合法的类文件和类型安全的字节码,但Java虚拟机无法确保字节码一定是由可信的javac编译器产生的

所以在连接时必须进行字节码验证以保障类型安全。

许多字节码约束都可以进行静态检查,例如字节码“ldc”的操作数必须是有效的常量池索引,其类型是CONSTANT_Integer,CONSTANT_String或CONSTANT_Float。另外一些的指令的类型检查和验证则需要在执行过程中动态分析代码。
目前有两种判断质量操作数类型和个数的字节码分析方法:

类型推导(Type Inference)

这是比较常用的方法,他对每个字节码进行抽象解释并在目标分枝或者异常处理器上合并类状态。

  1. 他对字节码进行迭代分析直到发现稳定的类型
  2. 如果没有发现稳定的类型,或者结果类型与某些字节码约束冲突,则会抛出VerifyError。

这个验证步骤的代码写在HotSpotVM的外部库libverify.so中,它使用JNI获取类和类型所需要的所有信息

类型检查(Type Verification)

Java编译器将每个目标分支或异常分支中的类型信息设置在code属性的StackMapTable中。
StackMapTable中包含若干个栈映射帧,每个栈映射帧都会用字节码偏移量指示表达式栈和局部变量表中元素的类型状态。
Java虚拟机验证字节码时只需要扫描一次就可以验证类型的正确性。独具字节码验证,这个方法比常用的类型推导来的快也更为轻巧。

目前先使用类型检查,如果类型检查失败(验证出错),HotSpot VM就会切换成类型推导进行验证,如果类型推导失败则抛出VerifyError。

2.5 数据共享

未完待续…

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值