java performance笔记——(三) jvm overview

来源:互联网 发布:阿里云邮箱管理员登录 编辑:程序博客网 时间:2024/06/06 19:05

体系结构


hotspot VM 包括三个主要方面:VM Runtime,JIT编译器和内存管理。


图中表示了JIT和GC的具体实现都是可插拔的, VMRuntime 也提供了GC 和JIT的api。

在32位jvm中最大内存受到下层操作系统限制,新内核的linux限制为2.5-3G。在一些场景中可以用64位jvm,这样可以有更大的堆,只是由于jvm内部表示java对象的oops(ordinary object pointers)从32位变成了64位,使得一次从cpu cache line中获得的oops更少,降低了cache命中率。相比同样的32位jvm大概有8%-15%的性能下降。

而在java6以后提供了compressed oops解决了这个问题,-XX:+UseCompressedOops 指令能够开启该功能,通过将64位oops压缩到32提高cpu cache利用率,在与32位性能相当同时享受64为的大堆空间。同时64位jvm使用x64 的cpu的额外的寄存器,这样能够缓解寄存器溢出的问题,即当前存活的变量大于寄存器容量时,会被从寄存器移回内存,降低了计算效率。


HotSpot VM Runtime

hotspot VM Runtime 包含许多职责,包括解析命令行参数、VM 生命周期、类装载、字节码解析、异常处理、同步、线程管理、JNI、VM严重错误处理和C++堆管理。


命令行参数

命令行选项有三种类型:standard,nonstandrad,developer

标准命令参数是按照jvm标准能被所有jvm接受的。

非标准的以-X开头,非标准参数不一定能被所有jvm实现支持,也不会在版本变化中通知。

hotspot developer参数以-XX开头,用参数前的+/- 号来开启或关闭某个特性如 -XX:-AggressiveOpts ,其他如 -XX:OptionName= N 设置数值,用k、m、g为单位

VM 生命周期

VM Runtime 的职责之一是启动和停止VM,负责启动Hotspot VM 的组件是launcher,launcher有不同种类,最一般的是java和javaw 命令,其他的还有从JNI启动的JNI_Cre-
ateJavaVM,从网络启动的javaws,可以被浏览器用来启动applet。

launcher启动VM 步骤如下:

1.解析命令行参数

2.设置堆大小和JIT编译器类型

3.建立变量

4.如果Main-Class没有在参数中指定,就从jar列表中获取Main-class

5.用JNI本地方法在非原始线程中创建Hotpot VM

6.Hotspot VM 创建后载入Main-Class,并获得main方法的属性

7.用JNI方法调用main方法并传入参数


VM 类加载

类加载有loading,linking,initializing三个阶段。

loading阶段查找类文件,建立 java.lang.Class对象来代表一个类,对类文件做格式检查,加载父类和接口。

linking阶段验证类型信息,常量池符号,类型检查。初始化静态成员,分配方法表,可选地分析符号引用。

下一步初始化执行类静态初始化,类成员初始化,这是该类的java代码第一次运行,此时父类和接口也会初始化。一般类只在第一次被用到的时候初始化,若A有B的引用,初始化A不一定初始化B.


类加载器的委派

类加载器在加载类的时候并非都是自己直接加载,而是试图委托高层的类加载器加载

java se类加载搜索加载器的顺序是bootstrap class loader,  extension class loader,  system class loader

system class loader是默认的应用程序类加载器,加载classpath里的类和main方法。application classloader 可以是java se库提供的也可以是用户自己实现的。

extension class loader加载lib/ext目录的jar


HotSpot VM GC

young generation --minor gc

old generation  --full gc

permanent generation  --一般不会把用户分配的对象从old generation移到permanent generation


minor gc要快就要避免扫描整个old generation来定位young generation中存活的对象。hotspot gc用了一种叫做card table的结构。把old generation分成512字节的块,称为cards,所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询card table来决定是否可以被收集,而不用查询整个老年代。这个card table由一个write barrier来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但GC的整体时间被显著的减少。


The Young Generation


一个eden,两个survivor相互移动,够年龄或放不下就放到old generation

fast allocation:在eden代为空时相当于一个队列,只要位置一个最后分配对象的末尾和eden末尾的指针就可以在这段空间内分配对象叫bump-the-pointer。而在多线程环境中还需要保持这个操作的线程安全,这里用到的是Thread-Local Allocation Buffers(TLABs)每个线程都有一块从eden来的buffer,可以用bump-the-pointer而不用锁。要是线程的buffer满了,要一块新的buffer 就需要gc清空eden,再分配。


Garbage Collectors


操作上有Mark,Sweep,Compact

类型上有:

Serial GC:串行

Parallel GC: 并行

Mostly-Concurrent GC: CMS接近并发的

full garbage collections or compacting garbage collections发生时出现stw会有较长时间的停顿。cms针对之前的stw状况的设计是使用了算法使得一个gc周期中只在initial mark和remark的时候有两次短的停顿。

initial mark过程中标记出可达的对象,然后是concurrent marking 记录修改存活的对象,之后在remark停顿中访问一遍对象。由于只是标记清扫,在old generation出现很多碎片影响到新对象存放时会有个很长的stw来整理碎片。

总体来说cms减少了收集old generation的停顿,需要做更多的工作,但是能够给需要迅速响应时间的应用提供很多好处。

Garbage-First GC: CMS Replacement

G1是个并行的、并发的,递增压缩的低停顿的collector 会在未来很长时间取代cms。G1使用的是与hotspot vm 其他collector截然不同的java堆布局,它把堆分成了相等大小的块,称为regions。G1没有物理分隔的空间来作为young generation和old generation,每一个generation就是一组region,这使得G1可以调节young generation的大小。

在回收过程中,会将存活的对象从一组region移动到另一组region,然后回收掉前一组region。大多数情况gc收集的都是young regions 即young generation,这就相当于是minor gc。

G1也会定时执行并发的marking周期来确定空的和几乎为空的region,这些块回收的效率是最大的。这也是G1名字的由来:查找回收最多垃圾的区域。


===================额外引用自http://m.oschina.net/blog/41090============================

枚举根节点  
在Java语言里面,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。如果要使用可达性分析来判断内存是否可回收的,那分析工作必须在一个能保障一致性的快照中进行——这里“一致性”的意思是整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中,对象引用关系还在不断变化的情况,这点不满足的话分析结果准确性就无法保证。这点也是导致GC进行时必须“Stop The World”的其中一个重要原因,即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。 
由于目前的主流JVM使用的都是准确式GC,所以当执行系统停顿下来之后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样GC在扫描时就就可以直接得知这些信息了。下面的代码清单1是HotSpot Client VM生成的一段String.hashCode()方法的本地代码,可以看到在0x026eb7a9处的call指令有OopMap记录,它指明了EBX寄存器和栈中偏移量为16的内存区域中各有一个普通对象指针(Ordinary Object Pointer)的引用,有效范围为从call指令开始直到0x026eb730(指令流的起始位置)+142(OopMap记录的偏移量)=0x026eb7be,即hlt指令为止。 

代码清单1 String.hashCode()方法的编译后的本地代码  

[Verified Entry Point]0x026eb730: mov    %eax,-0x8000(%esp)…………;; ImplicitNullCheckStub slow case0x026eb7a9: 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    

安全点  
在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。 
实际上HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint),即程序执行时并非在所有的地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。所以安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。 
对于Sefepoint,另外一个需要考虑的问题是如何让GC发生时,让所有线程(这里不包括执行JNI调用的线程)都跑到最近的安全点上再停顿下来。我们有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension),抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。 

而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。下面的代码清单2中的test指令是HotSpot生成的轮询指令,当需要暂停线程时,虚拟机把0x160100的内存页设置为不可读,那线程执行到test指令时就会停顿等待,这样一条指令便完成线程中断了。 

代码清单2 轮询指令   

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  

======================================================================================================

HotSpot VM JIT Compilers

HotSpot是一个混合模式的虚拟机,也就是说它既可以解释字节码,又可以将代码编译为本地机器码以更快的执行。通过配置-XX:+PrintCompilation参数,你可以在log文件中看到方法被JIT编译时的信息。JIT编译发生在运行时 —— 方法经过多次运行之后。到方法需要使用到的时候,HotSpot VM会决定如何优化这些代码。当方法运行足够多次数时或者方法中有很长的循环时会被编译。

Java HotSpot虚拟机可以运行在两种模式下:client或者server。你可以在JVM启动时通过配置-client或者-server选项来选择其中一种。两种模式都有各自的适用场景,本文中,我们只会涉及到server模式。

两种模式最主要的区别是server模式下会进行更激进的优化 —— 这些优化是建立在一些并不永远为真的假设之上。一个简单的保护条件(guard condition)会验证这些假设是否成立,以确保优化总是正确的。如果假设不成立,Java HotSpot虚拟机将会撤销所做的优化并退回到解释模式。也就是说Java HotSpot虚拟机总是会先检查优化是否仍然有效,不会因为假设不再成立而表现出错误的行为。

在server模式下,Java HotSpot虚拟机会默认在解释模式下运行方法10000次才会触发JIT编译。可以通过虚拟机参数-XX:CompileThreshold来调整这个值。比如-XX:CompileThreshold=5000会让触发JIT编译的方法运行次数减少一半。

HotSpot VM Adaptive Tuning

Java 6 Update 18 Updated 后的版本