HotSpot运行时概览#1

来源:互联网 发布:googlenet tensorflow 编辑:程序博客网 时间:2024/06/08 06:27

原文地址:http://openjdk.java.net/groups/hotspot/docs/RuntimeOverview.html


命令行参数处理

有许多的命令行选项和环境变量可以影响到HotSpot虚拟机的性能。其中有些选项直接由启动器处理(例如-server-client),有些则是启动器先加工一下再交给虚拟机处理,但大部分选项还是由启动器直接交给虚拟机来处理。

主要有三类选项:标准选项非标准选项开发者选项。所有的JVM实现都要支持标准选项,即使不同的版本也要稳定支持(不管选项是否被弃用)。以-X开头的是非标准选项(并不能保证所有的JVM实现都支持该选项),非标准选项在后续的Java SDK版本有可能在你不知情的情况下就被修改。以-XX开头的是开发者选项,这些选项通常需要特定的系统环境支持,并且可能需要访问系统配置参数的权限;一般用户并不推荐使用。开发者选项也可能在你不知情的情况下被修改。

命令行标记可以设置虚拟机内部变量,这些变量都有默认值。对于布尔类型的变量,命令行标记出现与否就可以控制该变量的值。对于-XX选项控制的布尔变量,在变量名前面加上+或者-分别可以设置该变量的值为true或者false。对于那些需要额外参数的变量,有许多不同的方式进行参数传递。有些标记可以直接将参数放在标记名后面, 有些则需要用:或者=将标记名与参数隔开。很不幸,使用哪种传递方式要看具体是哪个标记。开发者标记(-XX标记)只有三种格式:-XX:+OptionName-XX:-OptionName,和-XX:OptionName=

大部分用整数来表示大小的选项都可以使用km或者g作后缀来表示多少K,多少M或者多少G。通常是用在控制内存大小的参数上。

虚拟机生命周期

下面来看下通用的java启动器与HotSpot虚拟机生命周期相关的东西。

启动器

在JavaSE中有几个HotSpot虚拟机的启动器,通常使用的是,Unix平台上的java命令,Windows平台上的javajavaw命令,不要和javaws混淆起来,javaws是一个基于网络的启动器。

虚拟机的启动操作如下:

  1. 解析命令行选项,一些选项由启动器自己处理,如-client-server,它们被用来选择具体要加载的虚拟机库,其他的选项则包装在JavaVMInitArgs再传给虚拟机处理。
  2. 如果没有在命令行显示指定堆的大小和编译器类型(client或者server)则需要计算一下。
  3. 设置环境变量,例如LD_LIBRARY_PATHCLASSPATH
  4. 如果命令行没有指定Main-Class则从JAR包的manifest读取。
  5. 启动一个新线程并调用JNI_CreateJavaVM来创建虚拟机。注意:如果在原来的线程创建虚拟机将会大大降低定制虚拟机的能力,例如在Windows平台上栈的大小会受限制。
  6. 一旦虚拟机被创建并初始化,Main-Class就会被加载,并且启动器也能拿到Main-Class的main方法了。
  7. 通过CallStaticVoidMethod调用main方法。
  8. 执行完main方法后需要检查下是否有产生阻塞的异常,如果有则清除掉,并返回退出状态。通过调用ExceptionOccurred来进行清除,方法返回值为0表示成功,其他都表示失败。
  9. 通过调用DetachCurrentThread来detach main线程(译者注:main线程即当前线程,也就是第5步中启动的新线程,另,本地线程需要attach到VM才能由VM管理并且成为Java层面的线程),DetachCurrentThread方法会减小线程计数,可以确保main线程不会再在虚拟机中执行操作并且没有活动的Java栈帧。之后就可以安全地调用DestroyJavaVM了。

其中最重要的是JNI_CreateJavaVMDestroyJavaVM,下面我们就来看看这两个方法。

JNI_CreateJavaVM

这个本地方法所做的事情如下:

  1. 确保这个方法不会同时被两个线程调用,并且在同一个进程中不存在两个虚拟机实例。注意,在同一进程当中,如果虚拟机的初始化进行到了某一个点,那么也已经不能再创建另一个虚拟机了。这是因为目前的虚拟机会创建一些静态的数据结构,这数据结构不能被重复初始化。
  2. 检查JNI版本,初始化GC日志的输出流。一些OS模块初始化,例如随机数生成器,当前的pid,高精度的时间,内存页大小,保护页。
  3. 解析并存储传进来的参数和属性。初始化标准的Java系统属性。
  4. 使用解析后的参数和属性进一步初始化OS模块,包括同步,栈,内存,安全点页。然后会加载一些其它的库,例如libziplibhpilibjavalibthread,初始化信号处理器,初始化线程库。
  5. 日志输出流初始化。agent库(hprofjdi)初始化与启动。
  6. 线程状态与线程本地存储(TLS)初始化。TLS保存了许多线程操作需要的数据。
  7. 全局数据初始化。例如事件日志,OS同步原语,perfMemory(性能数据),chunkPool(内存分配器)。
  8. 现在我们可以创建线程了。Java版的main线程被创建并attach到当前这个OS线程。现在还不能将该线程添加到线程列表中。初始化Java层面的同步。
  9. 初始化其余的全局模块,例如BootClassLoaderCodeCacheInterpreterCompilerJNISystemDictionaryUniverse。这时候已经没有退路了,不能在相同的进程地址空间再创建一个虚拟机了。
  10. main线程添加到线程列表中,这个操作需要第一次使用Thread_Lock。检查Universe中的一些全局数据结构。创建VMThread,所有重要的虚拟机操作都在这个线程执行。发送JVMTI事件通知当前状态。
  11. 加载并初始化java.lang.Stringjava.lang.Systemjava.lang.Threadjava.lang.ThreadGroupjava.lang.reflect.Methodjava.lang.ref.Finalizerjava.lang.Class和其他的系统类。这时候虚拟机已经完成初始化并且可以开始使用,但还没有开启全部功能。
  12. 启动信号处理器线程,初始化编译器并启动CompileBroker线程。启动StatSamplerWatcherThreads线程。这时候虚拟机已经打开所有功能,JNIEnv设置完成,虚拟机可以开始接收JNI请求了。

DestroyJavaVM

可以通过启动器调用这个方法来关闭虚拟机。当发生了严重的错误时,虚拟机自身也可以调用这个方法。

关闭虚拟机要经过以下步骤:

  1. 一直等待直到main线程是最后一个非守护线程。此时虚拟机仍然是可以使用的。
  2. 调用java.lang.Shutdown.shutdown()。这个方法会调用Java层面的关闭钩子,如果设置了finalization-on-exit就执行finalizer。
  3. 调用before_exit(),为执行虚拟机层面的关闭钩子(通过JVM_OnExit()注册)做准备,停止ProfilerStatSamplerWatcher和GC线程。发送JVMTI/PI状态事件,关闭JVMPI,停止信号处理器线程。
  4. 调用JavaThread::exit(),释放JNIHandleBlock,清除栈保护页,从线程列表删除main线程。此时已经不能再执行任何Java代码了。
  5. 关闭对JNI/JVM/JVMPI调用的跟踪。
  6. 对那些还在运行本地代码的线程设置_vm_exited
  7. 删除Java版的main线程。
  8. 调用exit_globals(),该方法会删除IO与PerfMemory等资源。
  9. 返回。

类加载

HotSpot虚拟机实现了由Java语言规范第三版,Java虚拟机规范第二版所定义,更新后的Java虚拟机规范第五章所修正的类加载机制。

虚拟机要负责解析常量池符号,这其中涉及到类与接口的加载,链接和初始化。下面我们将使用类加载这个词来描述将类名或接口名映射到类对象的整个过程,而加载链接初始化将用于描述具体的由虚拟机规范所定义的类加载过程。

在字节码解析过程中,当遇到常量池符号时就会牵扯到类加载。Java API,例如Class.forName()classLoader.loadClass(),反射的API,和JNI_FindClass都可以启动类加载。虚拟机自身也可以。在虚拟机启动阶段,虚拟机就会去加载java.lang.Objectjava.lang.Thread等核心类。加载一个类之前需要先加载它所有的父类或者父接口。如果有需要,类文件验证(链接阶段的一部分)也会触发类加载。

虚拟机与JavaSE的类加载库共同完成类加载。虚拟机执行常量池解析,链接初始化加载阶段由虚拟机和特定的类加载器(java.lang.classLoader)共同完成。

类加载过程

加载阶段首先会拿到类名或接口名,找到相应的类文件,定义这个类,生成java.lang.Class对象。如果找不到类的二进制表示会抛出NoClassDefFound。此外,进行类文件语法格式检查也可能抛出ClassFormatError或者UnsupportedClassVersionError。完成一个类的加载之前需要先加载它所有的父类或者父接口。如果这个类的层次结构有问题,例如它是它自己的父类或者父接口(递归了),虚拟机将会抛出ClassCircularityError。如果父接口不是一个接口或者父类是一个接口,那么虚拟机会抛出IncompatibleClassChangeError

链接阶段首先做的事情是验证,包括类文件语法检查,常量池符号检查,还有类型检查。这些检查都可能抛出VerifyError。然后是准备,包括创建并初始化static字段成标准默认值,还有分配方法表。注意此时还不会执行任何Java代码。链接阶段的最后是可选的符号引用解析

初始化阶段会执行static代码块和static字段的初始化。这些是这个类中最先被执行的Java代码。注意,父类的初始化要先执行,父接口则不需要。

Java虚拟机规范规定了在类第一次“活跃使用”时执行初始化。对于链接阶段中的符号解析这一步需要在什么时候执行,Java语言规范没有定死,只要我们遵循语言的语法,加载,链接和初始化这三个阶段严格按顺序执行并且准确地抛出异常就可以。出于性能考虑,HotSpot虚拟机会等到需要初始化时才进行加载与链接。所以如果类A引用了类B,A的加载并不会触发B的加载(除非验证需要)。只有当执行到第一条引用到B的指令才会触发B的加载,链接与初始化。

类加载代理

当一个类加载器收到类加载请求,它可以请求其他类加载器去完成真正的加载操作。这就是类加载的代理机制。第一个加载器称为初始加载器,后一个真正完成加载操作的称为定义加载器。字节码解析时,解析类常量池符号的一定是初始加载器。

类加载器是有层次结构的,并且每个加载器都有一个父加载器。代理机制决定了类的二进制表示的搜索顺序。引导类加载器扩展类加载器系统类加载器,按这个顺序来。系统类加载器是默认的应用类加载器,应用类加载器是用来加载Main-Class和其他类路径上面的类的。可以使用JavaSE类库中的类加载器作为应用类加载器,也可以自己开发应用类加载器。JavaSE所实现的扩展类加载器会从JRE的lib/ext目录加载类文件。

引导类加载器

由虚拟机实现的引导类加载器会从BOOTPATH加载类文件,其中包括rt.jar。为了加快启动速度,虚拟机可以通过类数据共享来执行类的预加载。

类型安全

类名或接口名用全限定名来表示,全限定名包含了类的包名。一个类的类型由全限定名与类加载器唯一确定。所以一个类加载器相当于定义了一个命名空间,相同的类名由不同的定义加载器加载便是两种不同的类型。

由于存在定制的类加载器,虚拟机有必要来保证所有的类加载器都遵循类型安全的约束。参考Dynamic Class Loading in the Java Virtual Machine和Java虚拟机规范 5.3.4小节。通过执行加载约束检查,虚拟机保证了当类A调用了B.foo(),foo方法的方法签名对于A的类加载器与B的类加载器是一样的。(译者注:具体一点,假设在A中有这么一句,C c = B.foo();,那么要求这个C不论是由A的类加载器还是由B的类加载器来加载都是一样的,不过大多数情况下A和B的类加载器会是同一个。当然,这里说的类加载器是定义加载器)

HotSpot中类的元数据

类加载会在GC永久代创建一个instanceKlass或者arrayKlassinstanceKlass持有一个自己的Java镜像的引用,这个Java镜像是一个java.lang.Class实例。虚拟机通过klassOop访问instanceKlass

HotSpot中类加载的数据

HotSpot虚拟机主要维护了3个哈希表来跟踪类加载情况。SystemDictionary保存了已加载的类,将类名/类加载器对映射到一个klassOopSystemDictionary既保存了类名/初始加载器也保存了类名/定义加载器的映射。所保存的映射只有在安全点才能被删除。PlaceholderTable保存了当前正在被加载的类,用于ClassCircularityError检查和支持并行类加载。LoaderConstraintTable用于跟踪类型安全检查的约束。

这几个哈希表都被SystemDictionary_lock保护。在虚拟机内部通常使用类加载器对象锁来保证类加载串行进行。

字节码验证与格式检查

Java是一门类型安全的语言,标准的Java编译器会输出有效的类文件和类型安全的代码,但是Java虚拟机仍然需要通过链接时进行字节码验证来保证类型安全,因为不能确保所有的代码都是由可信赖的编译器产生。

字节码验证在Java虚拟机规范4.8小节(译者注:现在应该是在4.10小结,可能是文档没更新)中描述。其中规定了虚拟机需要验证的两种代码约束,即静态约束和动态约束。如果违反了这些约束,虚拟机将会抛出VerifyError并且阻止链接继续进行。

字节码的约束有许多都是静态的,例如ldc指令的操作数必须是一个有效的常量池索引,该常量必须是CONSTANT_IntegerCONSTANT_String或者CONSTANT_Float类型。有些指令需要检查参数类型和数量,由于要等到运行时才能确定在表达式栈上面有哪些操作数,因此这样的约束只能动态分析了。

目前有两种方法可以确定运行时每条指令将会接收到的操作数类型和数量。类型推导是比较传统的一种做法。类型推导需要对每一个字节码执行抽象解释,并且合并目标分支或者异常处理的类型声明。这个过程会一直迭代直到类型达到稳定状态。如果无法达到稳定状态,或者最终结果的类型违反了字节码约束就会抛出VerifyError。目前执行这一步验证的代码放在libverify.so这个外部库当中,并且使用JNI来获取必要的类和类型信息。

在JDK6中开始使用第二种方法,这种方法称为类型验证在这种方法中,Java编译器通过字节码属性StackMapTable提供了每一个目标分支或者异常处理的稳定状态的类型信息。StackMapTable由许多的栈映射帧组成,每一帧表示了方法中特定位置上,操作数栈和局部变量的类型(译者注:可参考R大的这个讲解。另,这篇抨击StackMapTable的文章说的还是有点道理的)。这样虚拟机只需要遍历一遍字节码,验证类型的正确性就可以了。这个方法已经被JavaME CLDC所采用。这种方式既小又快,因此直接嵌入到虚拟机内部了。

对于版本号小于50的类文件,例如由JDK6之前版本生成的,Java虚拟机会使用传统的类型推导方式进行验证。而版本号大于等于50的,会使用StackMapTable进行类型验证。由于存在一些旧的外部工具会去修改字节码,但不会更新StackMapTable属性,因此当使用类型验证遇到一些错误时,可能会换成使用类型推导的方式来进行。

类数据共享

类数据共享(CDS)是J2SE 5.0引进的一个特性,目的是为了减少Java应用,特别是小应用,的启动时间和内存占用。当你使用Sun提供的安装器在32位平台安装JRE时,安装器会从系统jar包加载一系列的类并将这些类表示成私有的内部格式,再将这些格式的类数据dump成一个文件,这个文件称为共享归档。如果没有使用Sun提供的安装器,那你也可以手动去创建这个文件。这个文件会以内存映射的方式打开,这样就可以降低加载那些类的成本,并且可以在多个虚拟机进程中共享这些类的元数据。

只有HotSpot客户端虚拟机支持类数据共享,并且只能使用串行垃圾收集器。

加入CDS的主要动机是它可以减少启动时间。CDS更适合小应用,因为它消除了一些固定开销:核心类的加载。使用的核心类越少,减少的启动时间就越多。

有两种方式可以减少新的JVM实例的内存占用。首先,共享归档其中一部分是以只读形式在多个JVM实例间共享的,大概5-6M。之前这部分数据在各个JVM实例都会冗余一份。其次,共享归档的数据格式已经是HotSpot虚拟机所使用的格式,因此访问rt.jar里面原始的类信息所需要的内存空间也省了。省下这些空间,同一台机器就可以有更多的应用并发执行。在Windows平台上,通过不同的工具观测,单个进程的内存占用可能会增加,因为有大量的页要映射到进程地址空间。 这部分通过减少rt.jar所占用的内存可以抵消。减少内存占用仍然是需要优先考虑的问题。

HotSpot实现的类数据共享在永久代中引入了新的共享数据的空间。共享归档classes.jsa在虚拟机启动时被映射到内存当中。后续这些共享区域的管理由虚拟机内存管理子系统执行。

只读的共享数据包括不变方法对象(constMethodOops),符号对象(symbolOops),原生类型数组,大部分字符数组。

可读写的共享数据包括可变方法对象(methodOops),常量池对象(constantPoolOops),Java类和数组的虚拟机内部表示(instanceKlassesarrayKlasses),可变的StringClassException对象。

0 0
原创粉丝点击