HotSpot Runtime概述

原文:http://openjdk.java.net/groups/hotspot/docs/RuntimeOverview.html#VM Class Loading|outline
本节介绍与HotSpot Runtime系统的主要子系统相关的关键概念。涵盖以下主题:
 命令行参数处理
 VM生命周期
 VM类加载
 字节码验证器和格式检查器
 类数据共享
 解释器
 Java异常处理
 同步
 线程管理
 C ++堆管理
 Java本地接口(JNI)
 VM致命错误处理
 引用
 进一步阅读

命令行参数处理

有许多命令行选项(command-line options)和环境变量可能会影响Java HotSpot虚拟机的性能特征。其中一些选项由启动程序(Launcher)使用(例如’ -server ‘或’ -client ‘),一些由启动程序处理后并传递给JVM,大多数选项由JVM直接使用。
选项有三大类:标准选项,非标准选项和开发人员选项。
标准选项 要求被所有JVM实现,并且在所有发行版中保持稳定(尽管可以弃用它们)。
以-X开头的选项是非标准的(不保证在所有JVM实现上都受支持),并且可能在不被告知的情况下,在Java SDK的后续版本中更改。
以-XX开头的选项是开发人员选项,为了保证能正确操作,通常会有一些特定的系统要求,可能需要特权访问系统配置参数; 不建议随意使用这些选项。这些选项也有可能在不被告知的情况下被更改。
命令行标志(Command-line flags) 控制JVM中内部变量的值,每一个标志都具有一个类型和一个默认值。
对于布尔类型,命令行上通过存在或不存在此类标志来控制变量的值。
对于 -XX布尔标志,名称前面的’ + ‘或’ - ‘前缀分别表示true或false值。
对于需要额外数据的变量,有许多不同的机制实现传递数据。有些标志接受直接在标志名称之后传入数据,中间没有任何描述符。而其他标志,在标志名称和数据之间,必须用’:‘或’ = ‘符号。采用哪种方法取决于特定标志及其解析机制。开发人员标志(-XX)仅以三种不同的形式出现:
-XX:+OptionName
-XX:-OptionName
-XX:OptionName=
大多数选项采用一个整数值表示大小,并且跟有’ k ‘,’ m ‘或’ g '的后缀来表示kilo,mega或giga,这些参数通常用于控制内存大小。

VM生命周期

以下部分概述了与HotSpot VM的生命周期有关的通用Java启动程序。

  • 启动程序(Launcher)

Java Standard Edition中有几个HotSpot VM启动程序,通常使用的通用启动程序是Unix上的java命令以及Windows的java和javaw命令(不要与基于网络的启动程序javaws混淆)。
与VM启动有关的启动器操作是:

  • 解析命令行选项,一些命令行选项由启动程序本身使用,例如 -client或-server用于确定和加载适当的VM库,其他选项使用JavaVMInitArgs传递给VM 。
  • 如果未在命令行中显式指定堆大小和编译器类型(客户端或服务器),则启动程序自己将确定堆大小和编译器类型。
  • 建立环境变量,如LD_LIBRARY_PATH和CLASSPATH。
  • 如果未在命令行中指定java Main-Class,则从JAR的manifest中获取Main-Class名称。
  • 在新创建的线程(非原始线程 )中使用JNI_CreateJavaVM创建VM 。注意:在原始线程中创建VM会大大降低自定义VM的能力,例如Windows上的栈大小以及许多其他限制。
  • 创建并初始化VM后,将加载Main-Class,并从Main-Class中获取main方法的属性。
  • 然后在VM中使用CallStaticVoidMethod调用java main方法,并从命令行获得的参数传递给main方法。
  • 一旦java main方法完成,检查和清除可能已发生的任何挂起异常,并返回退出状态。通过调用ExceptionOccurred清除异常,如果成功,此方法的返回值为0,否则,其他值将传递回进程。
  • 使用DetachCurrentThread分离主线程,这样我们减少了线程数,因此可以安全地调用DestroyJavaVM,同时确保线程不在VM中执行操作,并且栈中没有活动的java帧(frame)。
    最重要的阶段是JNI_CreateJavaVM和DestroyJavaVM, 这些将在下一节中介绍。
  • JNI_CreateJavaVM

JNI调用方法执行以下操作:

  • 确保没有两个线程同时调用此方法,并且在同一进程中不会创建两个VM实例。注意:一旦达到初始化点(不可返回点),在同一进程空间中将无法创建VM。这是因为VM此时已经创建了一个无法重新初始化的静态数据结构。
  • 检查以确保支持JNI版本,并为gc日志初始化ostream。初始化OS模块,例如随机数生成器,current pid,high-resolution time,memory page size和guard pages。
  • 传入的参数和属性将被解析并存储起来供以后使用。初始化标准java系统属性。
  • 基于已解析的参数和属性,进一步创建和初始化OS模块,为同步,栈,内存和safepoint pages初始化。此时加载其他库,如libzip,libhpi,libjava,libthread,初始化和设置信号处理程序,并初始化线程库。
  • 初始化输出流日志记录器。初始化并启动所需的任何代理库(hprof,jdi)。
  • 初始化线程状态和线程本地存储(TLS),其保存线程操作所需的若干线程特定数据。
  • 全局数据初始化,例如事件日志,OS同步基元(OS Synchronization Primitives),perfMemory(性能内存),chunkPool(内存分配器)。
  • 此时,我们可以创建线程。创建Java版本的主线程,并将其attach到当前OS线程。但是,此线程尚未添加到已知的线程列表中。初始化并启用Java级别同步(Java level synchronization)。
  • 其余的全局模块被初始化,例如 BootClassLoader,CodeCache,Interpreter,Compiler,JNI,SystemDictionary和Universe。注意:我们已经达到了“不可返回点”,我们无法再在同一进程地址空间中创建另一个VM。
  • 通过锁定Thread_Lock,将主线程添加到列表中 。对Universe,即一组必需的全局数据结构,进行健全性检查。创建执行VM所有关键功能的VMThread。此时,将发布相应的JVMTI事件以通知当前状态。
  • 以下类java.lang.String,java.lang.System,java.lang.Thread,java.lang.ThreadGroup,java.lang.reflect.Method,java.lang.ref.Finalizer,java.lang.Class和其余的System类都被加载并初始化。此时,VM已初始化并可运行,但尚未完全正常运行。
  • 启动信号处理线程(Signal Handler thread),初始化编译器并启动CompileBroker线程。启动其他helper线程StatSampler和WatcherThreads,此时VM完全正常运行,JNIEnv已填充并返回给调用者,并且VM已准备好为新的JNI请求提供服务。
  • DestroyJavaVM

可以从启动器调用此方法来卸载VM,当发生非常严重的错误时,也可以由VM本身调用。
卸载VM需要执行以下步骤:

  1. 等到执行完最后一个non-daemon线程,注意:VM仍然可以运行。
  2. 调用java.lang.Shutdown.shutdown(),它将调用Java level shutdown hooks,如果finalization-on-exit,则运行finalizers。
  3. 调用before_exit(),准备VM退出,运行VM level shutdown hooks(它们是通过JVM_OnExit()被注册的),停止Profiler, StatSampler,Watcher和 GC 线程。将状态事件发布到JVMTI / PI,禁用JVMPI,并停止信号线程。
  4. 调用JavaThread :: exit(),释放JNI handle blocks,删除栈guard pages,并从线程列表中删除该线程。从现在开始,我们无法执行任何Java代码。
  5. 停止VM线程,它会将剩余的VM带到安全点并停止编译器线程。在安全点,我们应该注意不要使用任何可能被Safepoint阻止的东西。
  6. 禁用JNI / JVM / JVMPI barriers的跟踪。
  7. 为仍在运行本机代码(native code)的线程设置_vm_exited标志。
  8. 删除此线程。
  9. 调用exit_globals(),删除IO和PerfMemory 资源。
  10. 返回到调用者。
    **

VM类加载

**
Java Hotspot VM支持Java语言规范,第三版[1],Java虚拟机规范(JVMS),第二版[2]定义的类加载,并根据已更新的JVMS第5章加载,链接和初始化[3]做了修改。
VM负责解析常量池符号,这需要加载,链接然后初始化类和接口。我们使用术语“类加载”来描述将类或接口名称映射到类对象的整个过程,以及在由JVMS定义的类加载阶段使用术语:加载,链接和初始化。
最常见的类加载是在字节码解析期间,类文件中的常量池符号需要解析时。诸如Class.forName(),classLoader.loadClass(),反射API和JNI_FindClass之类的Java API 可以启动类加载。VM本身可以启动类加载。VM 在JVM启动时加载核心类,如java.lang.Object,java.lang.Thread等。加载类需要加载所有父类和父接口。类文件验证是链接阶段的一部分,可能需要加载其他类。
VM和Java SE类加载库共同负责类加载。VM为类和接口执行常量池解析、链接和初始化。加载阶段需要VM与特定类加载器(java.lang.classLoader)协同工作。
类装载阶段
加载类阶段通过类或接口名称,查找二进制格式的类文件,定义类并创建java.lang.Class对象。如果找不到,则加载类阶段会抛出NoClassDefFound错误。此外,加载类阶段会对类文件的语法进行格式检查,这会抛出ClassFormatError或UnsupportedClassVersionError。在完成类的加载之前,VM必须加载其所有父类和父接口。如果类层次结构存在问题,如此类是其自己的父类和父接口(递归),则VM将抛出ClassCircularityError。如果直接父接口不是接口,或者直接父类是个接口,VM也会抛出IncompatibleClassChangeError。
链接类阶段首先进行验证,它检查类文件语义,检查常量池符号并进行类型检查。这些检查可能会抛出VerifyError。然后链接进行准备,创建静态字段并将其初始化为标准默认值并分配方法表。请注意,此时尚未运行任何Java代码。然后必要时链接会执行符号引用的解析。
类初始化运行静态初始化器和静态字段的初始化器。这是为此类运行的第一个Java代码。请注意,类初始化需要父类初始化,但父接口不会初始化。
JVMS指定类初始化发生在类的第一次被使用上。只要我们尊重语言的语义,JLS允许灵活地进行链接的符号解析步骤,在执行下一步之前,会先完成加载、链接和初始化的每个步骤,并且在必要时抛出错误。为了提高性能,HotSpot VM通常会等到类初始化后才加载和链接类。因此,如果类A 引用类B,则加载类A 不一定会导致加载类B (除非需要验证)。执行引用B的第一条指令才会导致B的初始化,这种初始化需要加载和链接B类。
类加载器委托(delegation)
当要求一个类加载器查找并加载类时,该类加载器可以要求另一个类加载器执行实际加载,这称为类加载器委托。第一个加载器是启动加载器,最终定义该类的类加载称为定义加载器。在字节码解析的情况下,启动加载器是正在解析其常量池符号类的类加载器。
类加载器是按层次定义的,每个类加载器都有一个委托双亲(delegation parent)。委托为二进制类代表定义搜索顺序。Java SE类加载器层次结构按顺序搜索引导类加载器(bootstrap class loader),扩展类加载器(extension class loader)和系统类加载器(system class loader)。系统类加载器是默认的应用程序类加载器,它运行“main”并从类路径加载类。应用程序类加载器可以是Java SE类加载器库中的类加载器,也可以由应用程序开发人员提供。Java SE类加载器库实现了扩展类加载器,它从JRE的lib / ext目录加载类。
引导类加载器(Bootstrap class loader)
VM实现引导类加载器,它从BOOTPATH加载类,包括例如rt.jar。为了更快启动,VM还可以通过类数据共享(Class Data Sharing)处理预加载的类。
类型安全
类或接口名称定义为包含包名称的完全限定名称。类类型由该完全限定名称和类加载器唯一确定。因此,每个类加载器定义了一个名称空间,并且由两个不同的定义类加载器加载的相同类名,会导致两个不同的类类型。
鉴于存在自定义类加载器,VM负责确保不良行为的类加载器不会违反类型安全性。请参阅Java虚拟机[4]和JVMS 5.3.4 [2]中的动态类加载。VM确保当A类调用B.foo()时,A的类加载器和B的类加载器通过跟踪和检查加载器约束,来同意foo的参数和返回值。
HotSpot中的类元数据
类加载在GC的持久代(permanent generation)中创建instanceKlass 或arrayKlass。instanceKlass引用一个java镜像,它是镜像此类的java.lang.Class的实例。VM C++是通过klassOop访问instanceKlass。
HotSpot内部类加载数据
HotSpot VM维护三个主要哈希表来跟踪类加载。SystemDictionary包含加载的类,SystemDictionary将一个类名称/类加载器映射到一个klassOop。该 SystemDictionary同时包含类的名称/启动加载器对和类的名称/定义加载器对。目前只能在安全点删除入口(entries)。PlaceholderTable包含目前正在加载的类。它用于ClassCircularityError检查,以及用于(支持多线程类加载的类加载器的)并行类加载。LoaderConstraintTable跟踪类型安全检查约束。
这些哈希表都受SystemDictionary_lock的保护。通常,使用类加载器对象锁序列化VM中的加载类阶段。

字节码验证器和格式检查器

Java语言是一种类型安全的语言,标准的Java编译器生成有效的类文件和类型安全的代码,但是JVM不能保证代码是由值得信赖的编译器生成的,因此它必须通过链接时称为字节码验证的进程,重新建立类型安全性。
字节码验证在Java虚拟机规范的4.8节中规定。规范规定了JVM验证的代码的静态和动态约束。如果发现任何违规,VM将抛出VerifyError并阻止该类被链接。
许多字节码约束可以被静态检查,例如’ldc’代码的操作必须是有效的常量池索引,其类型为CONSTANT_Integer, CONSTANT_String或 CONSTANT_Float。其他约束需要在执行期间动态分析代码来确定表达式栈上存在哪些操作,如检查参数的类型和个数。
目前有两种分析字节码的方法确定每条指令操作的类型和数量。传统方法称为“类型推断”,通过对每个字节码执行抽象解释并在分支目标或异常句柄处合并类型状态来进行操作。迭代分析字节码,直到找到类型的稳定状态。如果找不到稳定状态,或者结果类型违反某些字节码约束,则抛出VerifyError。此验证步骤的代码在libverify.so外部库中,并使用JNI收集有关类和类型的所需信息。
JDK6中的新功能是第二种验证方法,称为“类型验证”。在此方法中,Java编译器通过代码属性StackMapTable为每个分支或异常目标提供稳态类型信息。StackMapTable由若干stack map frame组成,每个stack map frame表示表达式栈上的项目类型和在该方法里某个局部变量的类型。JVM只需要执行一次字节码扫描来验证类型的正确性。这是JavaME CLDC已经使用的方法。由于它更小更快,因此这种验证方法直接在VM本身中实现。
对于版本号小于50的所有类文件,例如在JDK6之前创建的类文件,JVM将使用传统的类型推断方法来验证类文件。对于大于或等于50的类文件,将使用StackMapTable属性和新的验证程序。由于旧的外部工具可能会检测字节码但忽略更新StackMapTable属性,因此在类型检查验证期间发生的某些验证错误可能会发生故障,而转移到类型推断方法。通过以上方法,类文件可以被验证。

类数据共享

类数据共享(CDS)是J2SE 5.0中引入的一项功能,旨在减少Java编程语言应用程序(尤其是较小的应用程序)的启动时间和占用空间。使用Sun提供的安装程序在32位平台上安装JRE时,安装程序会将系统jar文件中的一组类加载到私有内部代表中,并将该代表转储到称为“共享存档”的文件中。如果未使用Sun JRE安装程序,则可以手动完成,如下所述。在后续的JVM调用期间,共享存档是内存映射的,从而节省了加载这些类的成本,并允许在多个JVM进程之间共享这些类的大部分JVM元数据。
仅Java HotSpot Client VM支持类数据共享,并且仅支持串行垃圾收集器。
包含CDS的主要动机是启动时间减少。CDS为较小的应用程序产生更好的结果,因为它消除了固定成本:加载某些核心类。应用程序使用的核心类数量越小,启动时间的节省部分就越大。
新JVM实例的占用空间成本有两种方法得以降低。首先,共享存档的一部分(目前在5到6MB之间)以只读方式映射在多个JVM进程之间共享。以前,此数据复制到每个JVM实例中。其次,由于共享存档包含了Java Hotspot VM需要使用的类数据,因此在rt.jar中访问原始类信息所需的内存将不再需要。如此节省的资源可以允许更多应用程序在同一台机器上同时运行。在Microsoft Windows上,进程的占用空间可能会增加,因为正在将大量页映射到进程的地址空间。这可以通过减少在rt.jar上保存部分所需的内存量(在Microsoft Windows中)来抵消。
在HotSpot中,类数据共享实现将新空间引入包含共享数据的永久代。classes.jsa共享存档在VM启动时被内存映射到这些空间。随后,共享区域由现有VM内存管理子系统管理。
只读共享数据包括常量方法对象(constMethodOops),符号对象(symbolOops)和基元数组,主要是字符数组。
读写共享数据由可变方法对象(methodOops),常量池对象(constantPoolOops),Java类和数组的VM内部表示(instanceKlass es和arrayKlasses)以及各种String,Class和Exception对象组成。

解释器

当前用于执行字节码的HotSpot解释器,是一个基于模板的解释器。HotSpot runtime(又称InterpreterGenerator)使用TemplateTable中的信息(对应于每个字节码的汇编代码)在启动时在内存中生成解释器。模板是每个字节码的描述。该TemplateTable定义所有的模板,并提供访问函数来获取对于给定的字节代码的模板。非产品标志-XX:+ PrintInterpreter可用于查看VM启动过程中在内存中生成的模板表。
由于多种原因,模板设计的性能优于经典的switch语句循环。首先,switch语句执行重复的比较操作,并且在最坏的情况下,可能需要将给定命令与除一个字节码之外的所有字节码进行比较以定位所需的一个。其次,它使用单独的软件栈来传递Java参数,而本机C栈则由VM本身使用。许多JVM内部变量(例如程序计数器或Java线程的栈指针)都存储在C变量中,这些变量不能保证始终保存在硬件寄存器中。这些软件解释器结构的管理消耗了相当大的执行时间。[5]
总体而言,HotSpot解释器显著缩小了VM与物理机之间的差距,这使得解释速度大大提高。然而,这需要以例如大量机器特定代码块(大约10KLOC(千行代码)的英特尔特定代码,14KLOC的SPARC特定代码)为代价。总代码大小和复杂性也明显变高,因为例如需要支持动态代码生成的代码。显然,调试动态生成的机器代码比静态代码要困难得多。这些属性当然不利于runtime演变的实现,但也并非绝无可能。[5]
解释器调用VM runtime进行复杂的操作(基本上在汇编语言中都很复杂),例如常量池查找。
HotSpot解释器也是整个HotSpot自适应优化的关键部分。自适应优化通过利用程序属性的优势解决了JIT编译的问题。实际上,所有程序都将大部分时间用于执行一少部分的代码。Java HotSpot VM不是按一个个的方法来编译的,而是just in time,利用解释器立刻运行程序,并在运行时分析代码以检测程序中的关键热点。然后,它将注意力集中在热点上的全局本机代码优化器上。通过避免编译不常执行的代码(大部分程序),Java HotSpot编译器可以更多地关注程序的性能关键部分,而不必增加整个编译时间。这种热点的监视可以像一个程序一样,持续动态地执行,以此满足客户的性能需求。

Java异常处理

Java虚拟机使用异常来表示程序违反了Java语言的语义约束。例如,尝试在数组边界之外进行索引将导致异常。异常导致从发生异常(或被抛出)的位置到程序员指定的点(或异常被捕获的位置)的非本地控制转移。[6]
HotSpot解释器,动态编译器和runtime都配合实现异常处理。有两种常见的异常处理案例:异常被抛出或被同一方法捕获,或被调用者捕获。后一种情况更复杂,需要栈展开才能找到合适的处理程序。
异常可以被抛出字节码启动,并从VM内部调用返回,或从JNI调用返回,或从Java调用返回(最后一种情况实际上只是前三种情况的后一阶段)。当VM识别出已抛出异常时,将调用runtime系统以查找与该异常最近的处理程序。三个信息用于查找处理程序:当前方法,当前字节码和异常对象。如果在当前方法中找不到处理程序,如上所述,则弹出当前激活栈帧(activation stack frame)并且针对先前帧迭代地重复该过程。
找到正确的处理程序后,将更新VM执行状态,并在恢复Java代码执行时跳转到处理程序。

同步

从广义上讲,我们可以将“同步”定义为防止,避免或从并发操作的不合时宜的交织(通常称为“竞赛”)中恢复的机制。在Java中,并发是通过线程来实现的。互斥是同步的一种特殊情况,即最多允许单个线程访问受保护的代码或数据。
HotSpot提供Java监视器,运行应用程序代码的线程可以通过它来参与互斥协议。一个监视器可以被已锁定或未被锁定,并且任何时候只有一个线程可以拥有监视器。只有在获得监视器的所有权后,线程才能进入受监视器保护的关键部分。在Java中,关键部分称为“同步块”,并由synchronized语句描述。
如果一个线程试图锁定监视器,并且监视器处于解锁状态,则该线程将立即获得监视器的所有权。如果后续线程在监视器被锁定时尝试获得监视器的所有权,则在所有者释放锁并且第二个线程设法获得(或被授予)独占所有权之前,将不允许该线程进入关键部分。
还有一些术语:“enter”监视器意味着获得监视器的独占所有权,并进入相关的关键部分。同样,“exit”监视器意味着释放监视器的所有权并退出关键部分。我们还可以说,锁定监视器的线程现在“owns”该监视器。“Uncontended”指的是仅由单个线程在其他无主监视器上的同步操作。
HotSpot VM采用了先进的技术,适用于无竞争(uncontended)和竞争(contended)同步操作,可大幅提升同步性能。
大多数是无竞争同步操作,无竞争同步操作使用constant-time技术实现的,使用偏向锁(biased locking),在最好的情况下,这种操作基本上是免费的。由于大部分对象在其生命周期中由最多一个线程锁定,所以允许线程bias该对象。一旦biased,该线程可以随后锁定和解锁对象,而无需使用重量级的原子(atomic)指令。[7]
竞争同步操作使用高级自适应旋转技术来提高吞吐量。即便是具有大量锁竞争的应用程序,其同步性能也如此之快,以至于绝大多数的程序都不会有显著的性能问题。
在HotSpot中,大多数同步是通过我们称之为“快速路径(fast-path)”的代码来处理的。我们有两个即时编译器(JIT)和一个解释器,所有这些都将发出快速路径代码。两个JIT分别是“C1”(-client编译器)和“C2”(-server编译器)。C1和C2都直接在同步站点发出快速路径代码。在没有竞争的正常情况下,同步操作将在快速路径中完成。但是,如果需要阻塞或唤醒线程(分别在monitorenter或monitorexit中),快速路径代码将调用慢速路径(slow-path)。慢速路径的实现是在原生C++代码中,而快速路径由JIT发出。
每个对象的同步状态编码为VM对象表示的第一个字(标记字)。对于多个状态,标记字被多路复用以指向附加的同步元数据。(此外,标记字也被多路复用以包含GC年代数据和对象的标识hashCode值。)状态为:
 中立:解锁
 有偏向:锁定/解锁+非共享
 栈锁定:锁定+共享但无竞争
标记指向所有者线程栈上的移位标记字。
 Inflated:锁定/解锁+共享和竞争
线程在monitorenter或wait()中被阻塞。
该标志指向重量级的“objectmonitor”结构。[8]

线程管理

线程管理涵盖线程生命周期的所有方面,从创建到终止,以及VM内线程的协作。这涉及管理从Java代码(无论是应用程序代码还是库代码)创建的线程,直接连接到VM的本地线程,或为一系列目的创建的内部VM线程。虽然线程管理的更广泛的方面是独立于平台的,但细节必然根据底层操作系统而变化。
线程模型
Hotspot中的基本线程模型是Java线程(java.lang.Thread的一个实例)和本地操作系统线程之间的1:1映射。本地线程在Java线程启动时创建,并在终止后回收。操作系统负责调度所有线程并分派到任何可用的CPU。
Java线程优先级和操作系统线程优先级之间是一个复杂的关系,因系统而异。稍后将介绍这些细节。
线程创建和销毁
将一个线程引入VM有两种基本方法:用Java代码调用java.lang.Thread对象的start()方法;或使用JNI将现有本地线程附加到VM。VM为内部目的创建的其他线程将在下面讨论。
VM中的给定线程有许多对象(记住Hotspot是用C ++面向对象的编程语言编写的):
 java.lang.Thread实例,表示在Java代码中的线程
 一个JavaThread实例,表示VM中的java.lang.Thread实例。它包含跟踪线程状态的其他信息。JavaThread保存其相关联的java.lang.Thread对象的引用(oop),并且java.lang.Thread对象也存储JavaThread的引用(raw INT)。JavaThread还保存其关联的OSThread实例的引用。
 一个OSThread实例表示一个操作系统线程,并含有跟踪线程状态所需的操作系统级信息。OSThread还包含一个特定于平台的“handle”,用于标识操作系统的实际线程。
当java.lang.Thread开始执行后,VM会创建相关的JavaThread和OSThread对象,以及本地线程。在准备好所有VM状态(例如线程本地存储和分配缓冲区,同步对象等)之后,本机线程将被启动。本机线程完成初始化,然后执行启动方法,该方法会执行java.lang.Thread对象的run()方法,然后处理任何未捕获的异常,并与VM交互检查此线程是否需要终止整个VM之后,终止线程。线程终止会释放所有已分配的资源,删除已知线程集的JavaThread,调用OSThread和JavaThread的析构函数,并在初始启动方法完成时最终停止执行。
本机线程使用JNI调用AttachCurrentThread来连接到VM。之后创建关联的OSThread和JavaThread实例并执行基本初始化。接下来为此线程创建一个java.lang.Thread对象,这是通过反射调用Thread类构造函数完成的,其参数来自于该线程提供的参数。一旦连接到VM,线程就可以通过其他可用的JNI方法调用它所需的任何Java代码。最后,当本机线程不再希望与VM有关时,它可以调用JNI的DetachCurrentThread方法取消与VM的关联(释放资源,删除对java.lang.Thread实例的引用,释放JavaThread和OSThread对象等)。
连接一个本机线程的一个特例,是通过JNI的CreateJavaVM方法初始创建VM,这可以由原生应用程序或启动程序(java.c)完成。这种方法会触发一系列初始化操,就像调用AttachCurrentThread一样。之后,线程就可以根据需要调用Java代码,例如反射调用应用程序的main方法。有关更多详细信息,请参阅JNI部分。
线程状态
VM使用许多不同的内部线程状态来表征每个线程正在执行的操作。这对于协调线程的交互以及在出现问题时提供有用的调试信息是非常必要的。线程的状态在执行不同的操作时会转换,这些转换点用于检查线程是否适合在该时间点继续执行请求操作 - 请参阅下面关于安全点的讨论。
VM主要的线程状态如下:
 _thread_new:初始化过程中的新线程
 _thread_in_Java:执行Java代码的线程
 _thread_in_vm:在VM内执行的线程
 _thread_blocked:线程因某种原因被阻塞(获取锁定,等待条件,休眠,执行阻塞I / O操作等等)
出于调试目的,在线程dump,stack traces等中还会维护其他状态信息,这些信息可以通过工具得到。这些状态由OSThread来维护,其中一些已经废弃。线程dump等中报告的状态包括:
 MONITOR_WAIT:线程正在等待获取竞争的监视器锁
 CONDVAR_WAIT:线程正在等待VM使用的内部条件变量(不与任何Java级别对象关联)
 OBJECT_WAIT:一个线程正在执行Object.wait()调用
其他子系统和库也添加了自己的状态信息,例如JVMTI系统和 java.lang.Thread类本身公开的ThreadState。此类信息通常无法访问,也与VM内部线程的管理无关。
内部VM线程
人们常常惊讶地发现即使执行一个简单的“Hello World”程序,也可能导致在系统中创建十几个或更多线程。它们来自内部VM线程和库相关线程(例如引用句柄和终结器线程)的组合。VM线程的主要类型如下:
 VM线程:VMThread单例实例,负责执行VM操作,这将在下面讨论。
 周期性任务线程:WatcherThread单例实例,模拟在VM内执行定期操作的计时器中断。
 GC线程:不同类型的线程支持并行和并发垃圾回收
 编译器线程:这些线程将字节码runtime编译为本地代码
 信号分发器(dispatcher)线程:该线程等待进程定向信号并将它们分派(也可以叫调度,仅翻译不同)给Java级信号处理方法
所有线程都是Thread类的实例,执行Java代码的所有线程都是JavaThread实例(Thread的子类)。VM跟踪在Threads_list的链表中的所有线程,并受Threads_lock保护 - Threads_lock是VM中使用的一个关键同步锁。
VM操作和安全点
VMThread等待来自VMOperationQueue的操作,然后执行这些操作。通常,这些操作会传递给VMThread,因为它们要求VM在执行之前达到一个安全点。简单来说,当VM处于安全点时,VM中的所有线程都已被阻止,并且在安全点正在进行时,在原生代码中执行的任何线程将被阻止返回到VM。这意味着VM操作可以在下述条件下执行,1. 在没有线程可以修改Java堆,2. 并且所有线程都处于–它们的Java堆保持不变并且可以被检查。
最熟悉的VM操作是垃圾收集,或者更具体而言,垃圾收集的“stop-the-world”阶段,这是许多垃圾收集算法所共有的。但是存在许多其他基于安全点的VM操作,例如:偏向锁定撤销,线程栈dump,线程挂起或停止(即java.lang.Thread.stop()方法)以及通过JVMTI请求的大量检查/修改操作。
许多VM操作是同步的,即操作完成之前请求者会被阻止,但有些是异步的或并发的,这意味着请求者可以与VMThread并行进行(假设没有启动安全点)。
安全点是使用基于轮询的合作机制启动的。简单来说,线程经常会问“我应该阻止安全点吗?”。有效地提出这个问题并不是那么简单。经常被问到的一个地方是线程状态转换期间。并非所有的状态转换时都会问这个问题,如一个线程让VM转到本机代码时就不会,但很多时候都会。当从方法返回或在循环迭代期间的某些阶段时,线程问该问题的其他位置是在已编译的代码中。执行解释代码的线程通常不会询问该问题,而是在请求安全点时,解释器切换到询问该问题的代码的调度表(dispatch table); 当安全点结束时,调度表再次切换回来。一旦请求安全点,在继续执行VM操作之前,VMThread必须等待所有线程处于安全点的安全状态。在安全点期间, Threads_lock用于阻止正在运行的所有线程,VMThread在执行VM操作后释放Threads_lock。

C++堆管理

除了Java堆(由Java堆管理器和垃圾收集器维护)之外,HotSpot还使用C/C++堆(也称为malloc堆)来存储VM内部对象和数据。从基类Arena派生的一组C++类管理C++堆的操作。
Arena及其子类提供了一个基于malloc/free的快速分配层。每个Arena从3个全局ChunkPool中分配内存块。每个ChunkPool满足不同分配大小范围的分配请求。例如,将从“小”ChunkPool中分配1k内存,而从“中”ChunkPool中分配10K。这样做是为了避免产出内存碎片而浪费内存空间。
Arena系统比单malloc/free性能好。单malloc/free操作可能需要获取全局操作系统锁,这会影响可伸缩性并可能损害性能。Arena是线程本地对象,它们缓存一定量的存储空间,因此在快速路径分配的情况下不需要锁定。而且,Arena的free操作在常见情况下不需要锁定。
Arena用于线程本地资源管理(ResourceArea)和句柄管理(HandleArea)。它们在编译期间也供客户端和服务器编译器使用。

Java本地接口(JNI)

JNI是本机(又叫原生,都是指native,二者意思相同)编程接口,它允许在Java虚拟机内运行的Java代码与使用其他编程语言(如C,C ++和汇编语言)编写的应用程序和库进行互操作。
虽然应用程序可以完全用Java编写,但有些情况下Java本身并不能满足应用程序的需要。当应用程序无法完全用Java编写时,程序员使用JNI编写Java本机方法来处理这些情况。
JNI本机方法可用于创建,检查和更新Java对象,调用Java方法,捕获和抛出异常,加载类和获取类信息,以及执行runtime类型检查。
JNI还可以与Invocation API一起使用,以使任意本机应用程序能够嵌入Java VM。这使程序员可以轻松地使其现有应用程序支持Java,而无需与VM源代码链接。[9]
重要的是,一旦应用程序使用JNI,它就有可能失去Java平台的两个好处。
首先,依赖于JNI的Java应用程序无法再在多个主机环境中轻松运行。尽管用Java编程语言编写的应用程序部分可以移植到多个主机环境,但仍需要重新编译用原生编程语言编写的应用程序。
其次,虽然Java编程语言是类型安全且安全的,但C或C ++等本机语言却不是。因此,Java开发人员在使用JNI编写应用程序时必须格外小心。某些本机方法可能会破坏整个应用程序。因此,在调用JNI功能之前,Java应用程序需要进行安全检查。
作为一般规则,开发人员应该在尽可能少的类中定义原生方法。这需要在原生代码和应用程序的其余部分之间进行更清晰的隔离。[10]
在HotSpot中,JNI函数的实现相对简单。它使用各种VM内部基元(primitives)来执行诸如对象创建,方法调用等活动。通常,这些是与其他子系统(例如解释器)使用的相同的runtime基元。
命令行选项-Xcheck:jni用来帮助调试本机方法在JNI使用中的问题。指定-Xcheck:jni会用另一组调试接口调用JNI。这种接口更严格地验证JNI调用的参数,以及执行其他内部一致性检查。
HotSpot必须特别注意跟踪当前在原生方法中执行的线程。在一些VM的活动,特别是垃圾回收的某些阶段,线程必须在安全点停止,以保证Java内存堆在敏感活动期间不被修改。当我们希望将以原生代码执行的线程带到安全点时,原生代码可以继续执行,但是当线程尝试返回Java代码或进行JNI调用时将停止该线程。

VM致命错误处理

对于任何一个软件,提供一个简单的方法来处理致命错误非常重要。Java虚拟机也不例外。典型的致命错误是OutOfMemoryError。Windows上另一个常见的致命错误称为Access Violation错误,相当于Solaris/Linux平台上的Segmentation Fault。了解这些致命错误的原因至关重要,以便在应用程序中或有时在JVM本身中修复它们。
通常当JVM因致命错误崩溃时,它会将名为hs_err_pid.log的hotspot错误日志文件(其中替换为崩溃的java进程ID)dump到Windows桌面或Solaris/Linux操作系统上的当前应用程序目录。由于JDK 6和其他版本已经重新移植到JDK-1.4.2_09版本,因此已经进行了一些增强以提高此文件的可诊断性。以下是这些改进的一些亮点:
 内存映射包含在错误日志文件中,可以很容易看到崩溃期间内存的布局。
 -XX:ErrorFile=选项,以便用户可以设置错误日志文件的路径名。
 OutOfMemoryError也将触发文件的生成。
另一个重要的特性是你可以在java命令中指定-XX:OnError=“cmd1 args …; com2…”,这样每当VM崩溃时,它将执行引号中指定的命令列表。此功能的一个典型用法是,您可以调用dbx或Windbg等调试器来查看崩溃情况。对于早期版本,您可以将-XX+ShowMessageBoxOnError指定为运行时选项,以便在VM崩溃时,您可以将正在运行的Java进程加到您喜欢的调试器。
在谈到HotSpot错误日志文件之后,这里简要总结一下JVM如何在内部处理致命错误。
 VMError类用于聚集和dump hs_err_pid.LOG文件。当看到无法识别的信号/异常时,由特定于OS代码调用它。
 VM内部使用信号进行通信。当无法识别信号时,将调用致命错误处理程序。在无法识别的情况下,它可能来自应用程序JNI代码,OS本地库,JRE本地库或JVM本身的错误。
 为了避免自身造成错误,致命错误处理程序的编写需小心,如StackOverflow或者持有关键锁时(如malloc锁)崩溃。
由于OutOfMemoryError对于某些大型应用程序来说是如此常见,因此向用户提供有用的诊断消息至关重要,以便他们可以快速识别问题,有时只需指定更大的Java堆大小即可。当OutOfMemoryError的情况发生时,该错误消息将指示哪些类型的内存是有问题的。例如,它可能是Java堆空间或PermGen空间等。从JDK 6开始,stack trace将包含在错误消息中。此外-XX:OnOutOfMemoryError=“”选项可以在抛出第一个OutOfMemoryError时运行某个命令。另一个值得一提的好功能是OutOfMemoryError的内置堆dump。它通过指定启用-XX:+HeapDumpOnOutOfMemoryError选项,您还可以通过指定-XX:HeapDumpPath =告诉VM将堆dump文件放在何处。
尽管应用程序尽量避免死锁,但有时它仍然会发生。发生死锁时,您可以在Windows上键入“Ctrl + Break”或获取Java进程ID并将SIGQUIT发送到Solaris / Linux的挂起进程。Java级别stack trace将被dump到标准输出,以便您可以分析死锁的原因。从JDK 6开始,此功能已内置于jconsole中,这是JDK中非常有用的工具。因此,当应用程序挂起死锁时,使用jconsole来附加进程,它将分析哪个锁存在问题。大多数情况下,死锁是由错误的顺序获取锁引起的。
我们强烈建议您查看“故障排除和诊断指南”[11]。它包含许多对诊断致命错误非常有用的信息。

进一步阅读

“解开Java SE Classloader的奥秘”,Jeff Nisewanger,Karen Kinnear,JavaOne 2006。

参考

[1] Java语言规范,第三版。Gosling,Joy,Steele,Bracha。http://java.sun.com/docs/books/jls/third_edition/html/execution.html#12.2
[2] Java虚拟机规范,第二版。蒂姆林德霍尔姆,弗兰克耶林。http://java.sun.com/docs/books/vmspec/2nd-edition/html/VMSpecTOC.doc.html
[3] Java虚拟机规范的修正案。第5章:加载,链接和初始化。http://java.sun.com/docs/books/vmspec/2nd-edition/ConstantPool.pdf
[4] Java虚拟机中的动态类加载。沉亮,吉拉德布拉查。PROC。ACM会议 面向对象编程,系统,语言和应用,1998年10月http://www.bracha.org/classloaders.ps
[5]“大型长期Java应用程序中的安全Class和数据演变”,Mikhail Dmitriev,http://research.sun.com/techrep/2001/smli_tr-2001-98.pdf
[6] Java语言规范,第三版。Gosling,Joy,Steele,Bracha。http://java.sun.com/docs/books/jls/third_edition/html/exceptions.html
[7]“HotSpot中的偏向锁定”。http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot
[8]“假设您对使用HotSpot作为同步研究工具感兴趣…”。http://blogs.oracle.com/dave/entry/lets_say_you_re_interested
[9]“Java Native Interface Specifications” http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html
[10]“Java Native Interface Programmer’s Guide and Specification”,Sheng Liang,http://java.sun.com/docs/books/jni/html/titlepage.html
[11]“故障排除和诊断指南” http://java.sun.com/javase/6/webnotes/trouble/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值