原文地址: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=
。
大部分用整数来表示大小的选项都可以使用k
,m
或者g
作后缀来表示多少K,多少M或者多少G。通常是用在控制内存大小的参数上。
下面来看下通用的java启动器与HotSpot虚拟机生命周期相关的东西。
在JavaSE中有几个HotSpot虚拟机的启动器,通常使用的是,Unix平台上的java
命令,Windows平台上的java
和javaw
命令,不要和javaws
混淆起来,javaws
是一个基于网络的启动器。
虚拟机的启动操作如下:
-client
和-server
,它们被用来选择具体要加载的虚拟机库,其他的选项则包装在JavaVMInitArgs再传给虚拟机处理。Main-Class
则从JAR包的manifest读取。Main-Class
就会被加载,并且启动器也能拿到Main-Class
的main方法了。DetachCurrentThread
方法会减小线程计数,可以确保main线程不会再在虚拟机中执行操作并且没有活动的Java栈帧。之后就可以安全地调用DestroyJavaVM了。其中最重要的是JNI_CreateJavaVM与DestroyJavaVM,下面我们就来看看这两个方法。
这个本地方法所做的事情如下:
libzip
,libhpi
,libjava
和libthread
,初始化信号处理器,初始化线程库。hprof
,jdi
)初始化与启动。BootClassLoader
,CodeCache
,Interpreter
,Compiler
,JNI
,SystemDictionary
和Universe
。这时候已经没有退路了,不能在相同的进程地址空间再创建一个虚拟机了。Thread_Lock
。检查Universe中的一些全局数据结构。创建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
和其他的系统类。这时候虚拟机已经完成初始化并且可以开始使用,但还没有开启全部功能。StatSampler
和WatcherThreads
线程。这时候虚拟机已经打开所有功能,JNIEnv设置完成,虚拟机可以开始接收JNI请求了。可以通过启动器调用这个方法来关闭虚拟机。当发生了严重的错误时,虚拟机自身也可以调用这个方法。
关闭虚拟机要经过以下步骤:
java.lang.Shutdown.shutdown()
。这个方法会调用Java层面的关闭钩子,如果设置了finalization-on-exit就执行finalizer。JVM_OnExit()
注册)做准备,停止Profiler
,StatSampler
,Watcher
和GC线程。发送JVMTI/PI状态事件,关闭JVMPI,停止信号处理器线程。HotSpot虚拟机实现了由Java语言规范第三版,Java虚拟机规范第二版所定义,更新后的Java虚拟机规范第五章所修正的类加载机制。
虚拟机要负责解析常量池符号,这其中涉及到类与接口的加载,链接和初始化。下面我们将使用类加载这个词来描述将类名或接口名映射到类对象的整个过程,而加载,链接和初始化将用于描述具体的由虚拟机规范所定义的类加载过程。
在字节码解析过程中,当遇到常量池符号时就会牵扯到类加载。Java API,例如Class.forName()
,classLoader.loadClass()
,反射的API,和JNI_FindClass都可以启动类加载。虚拟机自身也可以。在虚拟机启动阶段,虚拟机就会去加载java.lang.Object
和java.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的类加载器会是同一个。当然,这里说的类加载器是定义加载器)
类加载会在GC永久代创建一个instanceKlass或者arrayKlass。instanceKlass
持有一个自己的Java镜像的引用,这个Java镜像是一个java.lang.Class
实例。虚拟机通过klassOop访问instanceKlass
。
HotSpot虚拟机主要维护了3个哈希表来跟踪类加载情况。SystemDictionary
保存了已加载的类,将类名/类加载器对映射到一个klassOop
。SystemDictionary
既保存了类名/初始加载器也保存了类名/定义加载器的映射。所保存的映射只有在安全点才能被删除。PlaceholderTable
保存了当前正在被加载的类,用于ClassCircularityError
检查和支持并行类加载。LoaderConstraintTable
用于跟踪类型安全检查的约束。
这几个哈希表都被SystemDictionary_lock
保护。在虚拟机内部通常使用类加载器对象锁来保证类加载串行进行。
Java是一门类型安全的语言,标准的Java编译器会输出有效的类文件和类型安全的代码,但是Java虚拟机仍然需要通过链接时进行字节码验证来保证类型安全,因为不能确保所有的代码都是由可信赖的编译器产生。
字节码验证在Java虚拟机规范4.8小节(译者注:现在应该是在4.10小结,可能是文档没更新)中描述。其中规定了虚拟机需要验证的两种代码约束,即静态约束和动态约束。如果违反了这些约束,虚拟机将会抛出VerifyError
并且阻止链接继续进行。
字节码的约束有许多都是静态的,例如ldc
指令的操作数必须是一个有效的常量池索引,该常量必须是CONSTANT_Integer
,CONSTANT_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类和数组的虚拟机内部表示(instanceKlasses
和arrayKlasses
),可变的String
,Class
,Exception
对象。
原文:http://blog.csdn.net/kisimple/article/details/44558693