Java性能优化之JVM概览(二)

HotSpot VM运行时

命令行选项

Hotspot VM运行时系统解析命令行选项,并据此配置 Hotspot VM。其中一些选项供 Hotspot VM启动器使用,例如指定选择哪个JIT编译器、选择何种垃圾收集器等,还有一些经启动器处理后传给完成启动的 Hotspot VM,例如指定Java雄的大小。
命令行选项主要有3类:标准选项( Standard Option)、非标准选项( Nonstandard Option)和非稳定选项( Developer Option)。标准选项是 Java Virtual Machine Specification要求所有Java虚拟机都必须实现的选项,它们在发行版之间保持稳定,但也可能在后续的发行版中被废除。非标准选项(以-X为前级)不保证、也不强制所有JVM实现都必须支持,它可能未经通知就在 Java SDK发行版之间发生更改。非稳定选项(以-XX为前缀)通常是为了特定需要而对JVM的运行进行校正,并且可能需要有系统配置参数的访问权限。和非标准选项一样,非稳定选项也可能不经通知就在发行版之间发生变动。

命令行选项用于控制 Hotspot VM的内部变量,每个变量都有类型和默认值。对于内部变量为布尔类型的选项来说,只要在 Hotspot VM命令行上添加或去掉它就可以控制这些变量。对于带有布尔标记的非稳定选项来说,选项名前的+或一表示true或 false,用以开启或关闭特定的 Hotspot VM特性或参数。例如,-XX:+ Aggressiveopts设置某个 Hotspot内部布尔变量为tue以开启额外的性能优化,反之,-XX:- Aggressiveopts则设置同样的变量为 falser以关闭额外的性能优化。除了布尔标记,还有一类带有附加选项的非稳定选项,形如-XX: Opttonname=几乎所有附加选项为整数的非稳定选项,整数后面都可以接后缀k、m、g,表示千、百万及十亿。有一小部分选项没有分隔标记,而是选项名后直接跟选项值,这和特定的命令行选项及解析机制有关。

VM生命周期

Hotspot VM启动时JNI_ CreateJavavm方法将执行以下一系列操作。

  1. 确保只有一个线程调用这个方法并且确保只创建一个 Hotspot VM实例。因为 Hotspot VM创建的静态数据结构无法再次初始化,所以一旦初始化到达某个确定点后,进程空间里就只能有一个 Hotspot VM。在 Hotspot VM的开发工程师看来, Hotspot VM启动至此已经是无法逆转了。
  2. 检查并确保支持当前的JNI版本,初始化垃圾收集日志的输出流。
  3. 初始化OS模块,如随机数生成器( Random Number Generator)、当前进程id( CurrentProcess id)、高精度计时器(High- Resolution Timer)、内存页尺寸( Memory Page Sizes)、保护页( Guard Pages)。保护页是不可访问的内存页,用作内存访问区城的边界。例如,操作系统常在线程栈顶压入一个保护页以保证引用不会超出栈的边界。
  4. 解析传入JNI_CreateJavavm的命令行选项,保存以备将来使用。
  5. 初始化标准的Java系统属性,例如java.version、 java.vendor、os.name等。
  6. 初始化支持同步、機、内存和安全点页的模块。
  7. 加载libzip、libhpi、libjava及libthread等库。
  8. 初始化并设置信号处理器( Signal Handler)
  9. 初始化线程库。
  10. 初始化输出流日志记录器( Logger)
  11. 如果用到 Agent库( hprof、jdi),则初始化并启动。
  12. 初始化线程状态( Thread State)和线程本地存储( Thread Local Storage),它们存储了线程私有数据
  13. 初始化部分 Hotspot VM全局数据,例如事件日志( Event Log),OS同步原语、perfmemory(性能统计数据内存),以及 chunkpoo1(内存分配器)
  14. 至此, Hotspot VM可以创建线程了。创建出来的Java版main线程被关联到当前操作系统的线程,只不过还没有添加到已知线程列表中。
  15. 初始化并激活Java级别的同步。
  16. 初始化启动类加载器( Bootclassloader)、代码缓存、解释器、JIT编译器、JNI、系统词典( System Dictionary)及 universe"(一种必备的全局数据结构集)。
  17. 现在,添加lava主线程到已知线程列表中。检查 universe是否正常。创建 HotspotVmthread,它执行 Hotspot VM所有的关键功能。同时发出适当的JMT事件,报告 Hotspot VM的当前状态。
  18. 加载和初始化以下Java类:java.lang. String、java.lang. System、java.lang.Thread, java.lang.Threadgroup, java.lang.reflect.Method, java.lang.ref.Finalizer、java.lang.Class以及余下的Java系统类。此时, Hotspot t已经初始化完毕并可使用,只是功能还不完各。
  19. 启动 Hotspot VM的信号处理器线程,初始化JT编译器并启动 Hot Spots编译代理线程。启动 Hotspot VM辅助线程(如监控线程和统计抽样器)至此, Hotspot VM已功能完备。
  20. 最后,生成 JNIENV:对象返回给调用者, Hotspot 则准备响应新的JNI请求。

如果 Hotspot VM启动过程中发生错误,启动器则调用 Destroy JavaVM方法关闭 HotspotVM。如果 Hotspot VM启动后的执行过程中发生很严重的错误,也会调用 DestroyJavaVM方法。
DestroyJavaVM按以下步骤停止 Hotspot VM

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

VM类加载

  1. 类加载阶段
    对于给定的Java类或接口,类加载时会依据它的名字找到Java类的二进制类文件,定义Java类然后创建代表这个类或者接口的java.lang.Class对象。如果没有找到Java类或接口的二进制表示,就会抛出 NoClassdefFound。此外,类加载阶段会对类的格式进行语法检查,如果有错,则会抛出 ClassformatError或 UnsupportedClassVersionError.Java类加载前, Hotspot VM必须先加载它的所有超类和超接口。如果类的继承层次有错,例如Java类是它自己的超类或超接口(类层次递归), Hotspot VM则会抛出 ClassCircularityError。如果所引用的直接超接口本身并不是接口,或者直接超类实际上是接口, Hotspot VM则会抛出 IncompatibleClassChangeError.链接的第一步是验证,检查类文件的语义、常量池符号以及类型。如果检查有错,就会抛出VerifyError。

链接的下一步是准备,它会创建静态字段,初始化为标准默认值,以及分配方法表。请注意,此时还没有执行任何Java代码。接下来解析符号引用,这一步是可选的。然后初始化类,运行类构造器。这是迄今为止,类中运行的第一段Java代码。值得注意的是,初始化类需要首先初始化超类(不会初始化超接口)。

Java Virtual Machine Specification规定首次使用类时进行类初始化,而 Java LanguageSpecification则允许在链接阶段符号解析时灵活处理,只要保持语言的语义不变,JMM依次执行加载、链接和初始化,保证及时抛出错误即可。出于性能优化的考虑,通常直到类初始化时 Hotspot VM才会加载和链接类。这意味着,类A引用类B,加载A不一定导致加载B(除非B需要验证)。执行B的第一条指令会导致初始化B,从而加载和链接B。

  1. 类加载器委派
    双亲委派:
    启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载<JAVA_HOME>/lib下的类。
    扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载<JAVA_HOME>/lib/ext下的类。
    系统类加载器/应用程序类加载器(Application ClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
    在这里插入图片描述

字节码验证

Java是一门类型安全语言,官方标准的Java编译器( javac)可以生成合法的类文件和类型安全的字节码,但Java虚拟机无法确保字节码一定是由可信的 ]avac編译器产生的,所以在链接时必须进行字节码验证以保障类型安全。 Java Virtual Machine Specification4.8节规定了字节码验证。这个规范规定了Java虚拟机需要进行字节码的静态和动态约束验证。如果发现任何冲突,Java虚拟机就会抛出 Verifyerror并且阻止链接该类。

许多字节码约束都可以进行静态检查,例如字节码“"1dc”的操作数必须是有效的常量池索引,其类型是 CONSTANT_ Integer、 CONSTANT_ String或 CONSTANT_F1oat。另外有些指令的参数类型和个数约束检查需要在执行过程中动态分析代码,从而确定表达式栽里可以有哪些操作数。日前有两种判断指令操作数类型和个数的字节码分析方法。常用的方法称为类型推导(TypeInference),它对每个字节码进行抽象解释并在目标分支或者异常处理器上合并类型状态。它对字节码进行迭代分析直到发现稳定的类型。如果没有发现稳定的类型,或者结果类型与某些字节码约束冲突,则会抛出 VerifyError。这个验证步骤的代码写在 Hotspot VM的外部库 libverify.so中,它使用NI获取类和类型所需要的所有信息。

第二种验证方法是Java6 Hotspot VM中新出现的类型检查( Type Verification).Java编译器将每个目标分支或异常分支中的类型信息设置在code属性的 Stackmaptable中。 Stackmaptable包含若干个栈映射帧,每个機映射帧都会用字节码偏移量指示表达式栈和局部变量表中元素的类型状态。Java虚拟机验证字节码时只需要扫描一次就可以验证类型的正确性。对于字节码验证,这个方法比常用的类型推导来得快也更为轻巧。

对于版本号小于50的类文件(如Java6之前的JDK所生成的2), Hotspot VM使用类型推导进行验证。大于或等于50的类文件,由于 Stackmaptable属性, Hotspot VM验证会用新的“类型检查”进行验证。由于较老的外部工具可能只修改字节码而没有更改 Stackmaptable,所以如果类型检查验证出错, Hotspot VM就会切换成类型推导进行验证,如果类型推导失败,则抛出 VerifyError

类数据共享

类数据共享是Java5引入的特性,可以缩短Java程序(特别是小程序)的启动时间,同时也能减少它们的内存占用。使用 Java Hotspot JRE安装程序在32位平台上安装Java运行环境(JRE)时,安装程序会加载系统jar中的部分类,变成私有的内部表示并转储成文件,称为共享文档( Shared Archive)。如果没有使用 Java Hotspot JRE安装程序,也可以手工生成该文件。之后调用Java虚拟机时,共享文档会映射到ⅣM内存中,从而减少加载这些类的开销,也使得这些类的大部分JVM元数据能在多个JVM进程间共享。

Hotspot VM的类数据共享在永久代(方法区)中引人了新的Java子空间,用以包含共享数据。 HotspotVM启动时,共享文档 classes. jsa作为内存映射被加载到永久代。随后 Hotspot VM的内存管理子系统接管该共享区域。只读的共享数据是永久代新的子空间之一,包括常量方法对象、符号对象和本地数组(大多是字符数组)。可读可写的共享数据是另一个永久代新引入的Java堆空间,包括可变方法对象常量池对象Java类和数组的 Hotspot VM内部表示以及各种 String、Class以及Exception对象。
1.8永久代被替换成元空间(即这些信息不存在堆里面,而是存放在计算机本地内存)

线程管理

  1. 线程模型
    Hotspot VM的线程模型中,Java线程被一对一映射为本地操作系统线程。Java线程启动时会创建一个木地操作系统线程,当该Java线程终止时,这个操作系统线程也会被回收。操作系统调度所有的线程并将它们分配给可用的CPU.Java线程的优先级和操作系统线程的优先级之间关系复杂,各个系统之间不尽相同。

  2. 线程状态
    Hotspot VM使用多种不同的内部线程状态来表示线程正在做什么。这有助于协调线程间的交互和出错时提供有用的调试信息。执行不同操作时、线程状态会发生迁移、迁移时会检查线程在该点处理请求的动作是否合适,详细内容请参考安全点的讨论。从 Hotspot VM的角度看,主线程可以有以下状态。

  • 新线程:线程正在初始化的过程中
  • 线程在Java中:线程正在执行Java代码。
  • 线程在WM中:线程正在 Hotspot VM中执行。
  • 线程阻塞:线程因某种原因(获取锁、等待条件满足、体眠和执行阻塞式IO操作等)而被阻塞。
    为了便于调试,用工具报告线程转储、栈追踪等信息时,还需要包括其他的状态信息。这些信息由 Hotspot内部的C++对象 Osthreadg维护。包括的线程状态信息如下所示
  • MONITOR_WATT:线程正在等待获取竟争的监视锁。
  • CONDVAR_MAIT:线程正在等待 Hotspot VM使用的内部条件变量(没有和任何Java对象关联)。
  • OBJECT_MATT:Java线程正在执行java.lang. Object.wait().
    其他 Hotspot VM子系统和库使用它们自己的线程状态信息,例如 JVMTI系统和java.lang. Thread暴露的线程自身状态。一般来说, Hotspot VN内部的线程管理系统无法访问或关联这些信息。
  1. VM内部线程
  • WM线程:是C++单例对象,负责执行VM操作。下ー小节将进一步讨论VM操作。
  • 周期任务线程:是C++单例对象,也称为 Watcher Thread,模拟计时器中断使得在 HotspotVM内可以执行周期性操作。
  • 垃圾收集线程:这些线程有不同类型,支持串行、并行和并发垃圾收集。口JT编译器线程:这些线程进行运行时编译,将字节码编译成机器码。
  • 信号分发线程:这个线程等待进程发来的信号并将它们分发给Java的信号处理方法。
    上述所有的线程都是 Hotspot内部C+线程类的实例,执行Java代码的所有线程都是 HotspotC+内部 Java Thread的实例。 Hotspot VM内部用 Threads_list链表追踪这些线程,用Threads_lock( Hot Spot VM使用的关键同步锁之一)保护这些线程。
  1. VM操作和安全点
    Hotspot VM内部的 /Mthreadh监控称为 Vmoperationqueuel的C+对象,等待该对象中出现VM操作,然后执行这些操作。因为这些操作通常需要 Hotspot VM达到安全点后才能执行,所以它们会直接传递给 Vmthread。简单来说,当 Hotspot VM到达安全点时,所有的Java执行线程都会被阻塞,在安全点时,任何执行本地代码的线程都不能返回Java代码。这意味着 Hotspot VM操作可以执行的前提是,没有线程正在修改其Java栈,线程的Java栈也没有被更改,以及所有线程的Java栈都能被检测。

垃圾收集是最为人所知的 Hotspot VM安全点操作,更明确地说是垃圾收集的Stop-The- World阶段。

还有许多其他安全点,例如偏向锁的撤销、线程栈的转储、线程的挂起或停止(就是java.lang. Thread.stop())以及许多通过JVMT请求的检查和更改操作。

许多 Hotspot VM操作是同步的,也就是说,在这些操作完成前,请求者会被阻塞;但有些是异步或者并发的,这意味着请求者可以在 Vmthread里并行处理(假设没有触碰到安全点)。

Hotspot VM通过协作、轮询的机制创建安全点。简单来说,线程会经常询问:“我该在安全点停住么?”高效地询问这个问题并不是件容易的事。线程在状态变迁的过程中,会经常询问这个问题,但并非所有的状态变迁都会如此询问,比如线程离开 Hotspot VM进入本地代码的情况。此外,JIT编译代码从Java方法中返回或正在循环迭代的某个阶段时,线程也会询问“我该在安全点停住吗?”。正在执行解释代码的线程通常不会询问它们是否该在安全点停住。相反,当解释器切换到不同的分配表时,会请求安全点。切换操作中包含一部分代码,用以询问何时离开安全点。当离开安全点时,分配表会再次切换回来。一旦请求了安全点, VmThreadj就必须在继续执行VM操作前等待,直到确定所有线程都已进入安全点保全状态为止。在安全点时, VmThread用 Threads_lock阻塞所有正在运行的线程,VM操作完成后, Vmthread释放 Threads_lock。

C++堆管理

除了 Hotspot VM内存管理器和垃圾收集器所维护的Java雄以外, Hotspot VM还用CC+堆存储 hotspot VM的内部对象和数据。从基类 Arena行生出来的一组C+类负责管理 Hotspot VM C++堆的操作,这些类只供 Hotspot VM使用,并不会暴露给 Hotspot VM的使用者。 Arena及其子类是常规CC++内存管理函数 malloc/free之上的一层,可以进行快速CC++内存分配。 Arena以及每个子类,从3个全局 Chunkpoo1中,按照请求内存的大小范同,分配不同的内存块( Hotspot VM内部称之为 Chunk)。例如,1K内存的分配请求从“小” Chunkpoo1中分配,而10K内存的分配请求则从“中等” Chunkpoo1中分配。这么做是为了避免浪费内存片。使用 Arena’分配内存而不是直接使用CC++的malloc/free内存管理函数是为了更好的性能。后者可能需要获取全局OS锁,这会影响扩展性并对性能有影响。

Arena是线程本地对象,会预先保留一定量的内存,这使得 ast-path分配不需要全局共享锁。与此类似,当 Arenal的frec操作将内存释放回 Chunk时,也不需要通常释放内存时所用的锁。在Hotspot VM的内部实现中,线程本地资源管理所用的 Arena,是它的C±子类 Resourcearea。此外,句柄管理所用的 Arena是它的C++子类 Handlearea。在JT编译过程中, Hotspotf的 client,和server JIT编译器也都会使用 Arena。

VM致命错误处理

Hotspot VM的设计者认为有一点非常重要,即能为它的用户和开发者提供足够多的信息,用以诊断和修复VM致命错误。 Outofmemoryerror是常见的VM致命错误。段错( SegmentationFault)是 Solaris和 Linux平台上另一种常见的致命错误。在 Windows上与之等价的错误是访问冲突( Access Violation)。发生这些致命错误时,最关键的是找出这些错误根源,然后予以修复。有时候史改Java应用就能解决根本问题,有时却要深入 Hotspot VM。当 Hotspot VM因致命错误而崩溃时,会生成 Hot Spot错误日志文件,名为 hs err pid.log,这里是崩溃 Hot Spot VM进程的id, hs err pid.log文件生成在 Hotspot VM的启动目录下。 Hotspot VM1.4.2引키入了这个特性,
为了改善致命错误根源的诊断,现在又增强了许多。这些新加的增强包括

  • hs err pid<pid,log错误日志文件中包括内存镜像,可以很容易地看到VM崩溃时的内存布局;
  • 提供命令行选项-XX: ErrorFile,可以设置 hs_err_pid.lg错误日志文件的路径名;
  • 0utofmemoryerror还可以触发生成hs_err_ pid<pid>.log文件。

另一种常用于诊断VM致命错误根源的做法是,添加 Hotspot VM命令行选项-XX: OnError=cmd1 args...;cmd2..。"当 hotspot VM崩溃时,就会执行这个 Hotspot VM命令行选项传递给它的命令列表。这个特性常用于立即调用调试器(如 Linux/ Solaris dbt或 Windows Windbg)检査这次崩溃。对于那些不支持-XX: OnError的Java发行版来说,可以用 Hotspot VM命令行选项XX:+ ShowMessageBoxOneEror来替代。这个参数会使VM退出前显示对话框以表示VM遇到了致命错误。这使得 Hotspot VM在退出前有机会连到调试器。

当Hotspot VM遇到致命错误时,内部使用 Vmerror类收集信息并导出成 hs_err_pid.log。当遇到不可识别的信号或异常时,特定的操作代码就会调用 VmError类。需要仔细编写 HotspotVM致命错误的处理程序,避免自身错误(如 Stackoverf1ow)或持有关键锁(如 malloch锁)时发生的致命错误。

因为可能出现 OutofMemoryError,特別是在大规模应用中,所以提供有用的诊断信息以便快速找到解决方法就变得非常关键。通常加大Java雄就可以解决这种错误。当 OutOfMemoryError发生时,错误信息会指示哪种内存有问题。例如,可能是设定的Java雄或水久代太小。从Java6开始,Hotspot VM生成的错误信息中包括了栈追踪信息。此外,它还引人了-X: OnoutofMemoryerror=,所以当抛出第一个 Outofmemoryerrori时,可以执行一条命令。另一个值得提及的有用特性是,当0 utofmemoryerror出现时可以生成堆的转储信息。指定-XX:+ HeapDumpOnOutOfMemory可以开启这个特性。另外一个 Hotspot VM命令行选项-XX: HeapDump-Path=<pathname>可让用
户指定堆转储的存放路径。
虽然开发人员编写应用时力图避免死锁,但错误在所难免。当发生死锁时,在 Windows平台上可以用Ctrl+ Break生成Java级别的线程栈追踪信息并打印到标准输出在 Solaris?和 Linux平台上,发送 SIGOUIT信号给Java进程d也可以得到同样效果。基于线程的伐追踪信息,可以分析死锁根源。从Java6开始,自带的 Jconsole I具添加了一项功能,可以关联到一个挂起的Java进程并分析
死锁的根源。多数情况下,死锁是由于获取锁的顺序错误所导致的

HotSpot VM垃圾收集器

分代垃圾收集

Hotspot VM使用分代垃圾收集器,这个为人所熟知的垃圾收集算法基于以下两个观察事实。弱分代假设

  • 大多数分配对象的存活时间很短

  • 存活时间久的对象很少引用存活时间短的对象

  • 新生代:大多数新创建的对象被分配在新生代中,与整个Java堆相比,通常新生代的空间比较小而且收集频繁。新生代中大部分对象的存活时间很短,所以通常来说,新生代收集(也称为次要垃圾收集,以后记作 Minor GC)之后存活的对象很少。因为 MinorGC关注小并且有大量垃圾对象的空间,所以通常垃圾收集的效率很高。

  • 老年代:新生代中长期存活的对象最后会被提升( Promote)或晋升( Tenure)到老年代。通常来说,老年代的空间比新生代大,而空间占用的增长速度比新生代慢。因此,相比 Minor GC而言,老年代收集(也称为主要垃圾收集或完全垃圾收集,以后记作 Ful GC)的执行频率比较低,但是一旦发生,执行时间就会很长。

分代垃圾收集的一大优点是,每个分代都可以依据其特性使用最适当的垃圾收集算法。新生代通常使用速度快的垃圾收集器,因为 Minor GC頻繁。这种垃圾收集器会浪费一点空间,但新生代通常只是Java堆中的一小部分,所以不是什么大问题。另外一方面,老年代通常使用空间效率高的垃圾收集器,因为老年代要占用大部分Java堆。这种垃圾收集器不公很快,不过 Full GC不会很频繁,所以对性能也不会有很大影响。

分代垃圾收集基于弱分代假设,要想充分发挥分代垃圾收集的威力,应用就必须符合该假设。对于那些不符合该假设的Java应用来说,分代垃圾收集只会増加更多开销,只不过实践中这样的应用很少见。

新生代

  • Eden:大多数新对象分配在这里(不是所有,因为大对象可能直接分配到老年代)。 MinorGC后Eden几乎总是空的。。
  • Survivor(一对):这里存放的对象至少经历了一次 Minor GC,它们在提升到老年代之前还有一次被收集的机会。
    在这里插入图片描述
    下图演示了 Minor GC的操作。灰色X标记的是需要被收集的对象。如下图, Minor GC后Eden中的存活对象被复制到未使用的 Survivor。被占用 Survivor里不够老(即还有在新生代中被收集的机会)的存活对象也被复制到未使用的 Survivor。最后,被占用 Survivor里“足够老(默认分代年龄为15)”的存活对象被提升到老年代。
    在这里插入图片描述
    Minor GC之后,两个 Survivor交换角色.Eden完全为空,仍然只使用一个 Survivor;老年代的占用略微增长。因为收集过程中复制存活对象,所以这种垃圾收集器称为复制垃圾收集器(Copying Garbage Collector )

需要指出的是,在 Minor GC过程中, Survivor r可能不足以容纳Eden和另一个 Survivors中的存活对象。如果 Survivors中的存活对象溢出,多余的对象将被移到老年代。这称为过早提升( Premature Promotion)这会导致老年代中短期存活对象的增长,可能会引发严重的性能问题。再进一步说,在 Minor GC过程中,如果老年代满了而无法容纳更多的对象, Minor GC之后通常就会进行Full GC,这将导致遍历整个Java雄。这称为提升失败( Promotion Failure)仔细对应用程序调优,同时结合垃圾收集器的自动调优,通常可以降低这两种麻烦事出现的可能性。

快速内存分配

对象内存分配器的操作需要和垃圾收集器紧密配合。垃圾收集器必须记录它回收的空间,而分配器在重用堆空间之前需要找到可以满足其分配需求的空闲空间。垃扱收集器以复制方式回收Hotspot VM新生代,其好处在于回收以后Fden总为空,在Eden中运用被称为指针碰撞(Bump-the- Pointer)的技术就可以有效地分配空问。这种技术追踪最后一个分配的对象(常称为top),当有新的分配请求时,分配器只需要检查top和Eden末端之间的空间是否能容纳。如果能容纳,top则跳到新近分配对象的末端。

重要的Java应用大多是多线程的,因此内存分配的操作需要考虑多线程安全。如果只用全局锁,在Eden中的分配操作就会成为瓶颈閃而降低性能。 Hotspot VN没有采用这种方式,而是以一种称为线程本地分配缓冲区( Thread- Local Allocation Buffer.TLAB)的技术,为每个线程设置各自的缓冲区(即Eden的一小块),以此改善多线程分配的吐量。因为每个TLAB都只有一个线程从中分配对象,所以可以使用指针碰撞技术快速分配而不需要任何锁。然而,当线程的TLAB填满需要获取新的空间时(不常见),它就需要采用多线程安全的方式了。大部分时候, HotspotVM的 new Object()操作只需要大约十条指令。垃圾收集器清空Eden区域,然后就可以支持快速内存分配了。

垃圾收集器

Serial收集器

Serial收集器适合大多数对停顿时间要求不高和在客户端运行的应用。虽然它仅用一个虚拟处理器进行垃圾收集( Serial之名即由此而来),但在现有的硬件条件下,它仍然只需几百兆Java堆就能有效管理许多重要的应用,并且最差情况下仍然能保持比较短的停顿( Full GC大约几秒钟)。同一台机器上运行大量JVM实例(某些情况下JMM的实例数超过了可用的处理器数)时,也常用 Serial!l收集器。当JVM进行垃圾收集时,最好只用一个处理器,虽然会使垃圾收集的时间有所延长,但对其他JyM的干扰最小,这方面 Serial收集器处理得很好。

Parallel收集器:吞吐量为先!

现在许多重要的Java应用都运行在有大量物理内存和多处理器的服务器上(有时是专用的)。理想情况下,垃圾收集器应该充分利用所有可用的处理器资源。并且当它在进行垃圾收集时也不会让多数处理器空闲。

为了减少垃圾收集的开销从而增加服务类应用的吞吐量, Hotspot VM自带了 Paralle收集器,也称为 Throughput收集器。它的操作和 Serial收集器类似(即它在新生代米用Stop-The- World方式收集,而老年代采用标记-压缩方式)。然而, Minor GC。和 Full GC都是并行的,使用所有可用的处理器资源。注意这个收集器的早期版本在老年代是串行收集,引入 Parallel Old收集器之后才改变

以下应用可以从 Parallel收集器获裣,需要高吞吐量的应用,最极端情況下 Full GC引人的Stop-The-Word停顿时间仍然要满足需求的应用,以及运行在多处理器系统之上的应用。批处理引擎、科学计算等也适合 Parallell收集器。与 Serial收集器相比, Parallel收集器改善了垃圾收集的整体效率,从而也改善了应用的吞吐量。

Mostly-Concurrent收集器:低延迟为先!

下图演示了CMS中垃圾收集是如何工作的。开始有一个短的停顿,称为初始标记( Initial Mark),它标记那些从外部“直接可达的老年代对象。然后,在并发标记阶段( Concurrent Marking Phase),它标记所有从这些对象可达的存活对象。因为在标记期间应用可能正在运行并更新引用(因而更改对象图),所以到并发标记阶段结束时,未必所有存活的对象都能确保被标记。为了应对这种情况,应用需要再次停顿,称为重新标记( Remark),重新遍历所有在并发标记期间有变动的对象并进行最后的标记。追踪更改的对象可以重用数据结构卡表。因为重新标记比初始标记更为重要,所以并发执行以提高效率。
在这里插入图片描述
为了进一步减少重新标记时的工作量,CMS收集器引入了并发预清除(Pre-Cleaning)阶段。如上图,预清除在并发标记之后和重新标记之前,完成一些原本要在重新标记阶段完成的工作,即重新遍历那些在标记期间因并发而被改掉的对象。虽然标记结束前仍然需要重新标记(因为程序在预清除阶段仍有可能改变对象),但预清除依然可以减少在重新标记时需要遍历的对象,有时甚至能非常有效地减少重新标记导致的停顿。

在重新标记结束时,所有Java堆中存活的对象已保证被标记。既然预清除和重新标记阶段的重新遍历对象会增加垃圾收集器的工作量(相比而言, Parallel收集器只在标记期间遍历一次),CMS整体的开销相应増加了。对于大多数垃圾收集器来说,这是典型的为了力图减少停顿时间而做的权衡

找到了老年代中所有的存活对象之后,垃圾收集的最后阶段就是并发清除( Concurrent Sweeping),清除整个Java堆,释放没有迁移的垃圾对象。空闲区域不连续,垃圾收集器需要使用一个数据结构( Hotspot VM中使用空闲列表)记录哪部分堆有空闲空间。因此在老年代分配的代价更昂贵,因为空闲列表的分配不如指针碰撞方法有效。这使 Minor GC会产生额外的开销,因为当 Minor GC过程中对象提升时,会在老年代中造成大量的分配。

CMS与前两个垃圾收集器相比还有一个缺点,就是需要更大的Java堆。这有一些原因。首先,CMS的周期时间长于Stop-The- World垃圾收集所用的时间。同时只有在清除阶段,空间才会真的回收。假使允许应用在标记时继续运行,也就允许它继续分配内存,因而在标记阶段老年代的占用可能会有所增加,而只有到清除阶段才会减少。此外,尽管垃圾收集器确保在标记阶段标识所有存活的对象,但实际上它无法保证找出所有的垃圾对象。标记阶段成为垃圾的对象在周期内可能被收集也可能不彼收集。如果没有,则它将在下一周期被收集。垃圾收集期间没有找出的垃圾对象通常称为浮动垃圾( Floating Garbage)

Garbage-First收集器:CMS替代者

Garbage- Firstl收集器(缩写为G1)是一个并行、并发和増量式床缩低停顿的垃圾收集器,长远来看是为了替代CMS.G1的Java堆布局和 Hotspot VM中其他垃圾收集器有着极大的不同,它将Java堆分成相同尺寸的块(称为区域, Region)。虽然G1也是分代,但整体上没有划分成新生代和老年代。相反,每代是一组(可能不连续)区域,这使得它可以灵活地调整新生代。

G1的垃圾收集是将区域中的存活对象转移到另外一些区域,然后收集前者(通常是更大)。大部分时候只收集新生区域(这些形成G1的新生代),它们相当于 Minor GC.G1也定期执行并发标记,以标识那些空或几乎空的非新生区域。这些是收集效率最高的区域(即G1以最少的代价回收最空的区域),它们定期被回收。这是G1名称的由来:它优先回收垃圾对象最多的区域。

垃圾收集器比较

Serial收集器Paralle收集器CMS收集器G1收集器
是否并行
是否并发
新生代收集器串行并行并行并行
老年代收集器串行并行并行和并发并行和并发

HotSpot VM JIT编译器

类型继承关系分析

在面向对象的语言中。代码经常会划分成小方法,将这些方法进行智能内联是获得高性能的重要手段Java在这方面会遇到一些麻烦,因为默认情况下所有实例方法都可以被子类覆盖,所以只看局部类型信息并不足以了解哪个方法可以内联。 Hotspot VM解决这个问题的办法是类型继承关系分析( Class Hierarchy Analysis,以下简写为CHA)。编译器利用CHA进行即时分析,判断加载的子类是否覆盖了特定方法。这种分析方法的关键在于, Hotspot VM只考虑已经加载的子类,而不关心任何其他还不可见的子类。当编译器使用CHA时,会将CHA信息记录在编译代码中:如果程序在后续执行中请求加载其他覆盖该方法的子类,则原先假定只有一个子类实现的编译代码就会被丢弃:如果编译代码正在执行, Hotspot VM就会通过说优化( Deoptimization)将编译帧转换成与之等价的一组解释器帧,使得原先的CHA假设完全被撤销。此外,CHA也被用来在已加载的类中识别只有一个接口或抽象类实现的情况。

编译策略

由于JIT没有时间编译程序中的所有方法,因此所有代码最初都是在解释器中运行。一旦方法被调用的次数变多,就可能变成编译。这个过程是由 Hot Spot VM中与每个方法关联的计数器来控制的:每个方法都有两个计数器:方法调用计数器和回边计数器。方法调用计数器在每次进人方法时加一:回边计数器在控制流每次从行号靠后的字节码回跳到靠前的字节码时加一。与仅用方法调用计数器相比、用回边计数器可以检测包含循环的方法,能使这些方法更早地转为编译。每次解释器递增这两种计数器时,都会与阈值进行比较,一旦超过,解释器就会请求编译这个方法。方法调用计数器的國值是 CompileThreshold,回边计数器的阈值公式复杂一些,是Compilethreshold * Onstackreplacepercentage/ 100

当解释器执行长期运行的Java循环时, Hotspot VM会选择一种称为栈上替换( On Stack Replacement,OSR)的特殊编译。通常Java代码最后进入编译代码的方式是,解释器在调用方法时发现,该方法有已经编译的代码,那该方法就会分派到编译代码,而不是停留在解释器中。但这个方法对在解释器里开始长时间运行的循环来说没有什么帮助,因为它们不会被再次调用。

当回边计数器溢出时,解释器会发起编译请求,这次编译从回边的字节码开始而不是从方法的首个字节码开始。然后以解释器帧作为输入生成代码,并从此状态开始执行。在这种情况下,长时间运行的循环可以充分利用编译代码。这种以解释器帧作为输入执行的代码生成技术称为機上替换。

Client JIT编译器概览

Client JIT编译器的目标是为了更快的启动时间以及快速编译,使人不会为了应用(如客户端GUI应用)的响应时间而纠结。早期的 lient JIT:编译器是一个简而快的代码生成器,没有太多复杂性,而Java应用的性能也比较合适。它在概念上接近于解释器,会为每种字节码都生成一个模板,同时也维护了一个栈布局,类似于解释器帧。此外,它仅仅是将类的字段访问方法内联。到Java1.4时, Client JIT编译器升级为支持全方法内联,添加了对CHA、逆优化的支持,这两种都有重大改进。Java5 Client JIT编译器看起来几乎没有什么改动,因为当时更重要的变化都是针对Java6 Client JIT编译器

为了改善整体性能,Java6 Client JIT:编译器包含了许多变化。 Clients编译器的R改成了SSA风格,简单局部寄存器分配被替换成了线性扫描寄存器分配。此外值计数器也有改善,可以在多个块之间扩展,此外还有一些内存优化的小改善。在x86平台上,可以支持用SSE进行浮点计算,这很显著地改善了浮点性能。

Server JIT编译器概览

Server JIT编译器的目标是使Java应用的性能达到极致,吞吐量也达到最高,所以它的设计焦点就是不遗余力进行优化。这就意味着,与 Client JIT编译器相比,同样的编译, Server JIT编译器可能要求更多的空间或时间。它极力内联,这经常会造成大方法,而方法越大编译花费的时间也越多。它使用扩展的优化技术,涵盖了大量的极端情况,而要满足这些情况,就必须为每一个它可能遇到的字节码都生成优化代码。

HotSpot VM自适应调优

Java 5 Hotspot VM引入了新特性,可以依据ⅣM启动时的底层平台和系统配置自动选择垃圾收集器、配置Java堆以及选择运行时JT编译器。此外,该特性也使得 Throughput收集器可以依据程序运行的情况,自适应调整Java堆和对象分配的速率。自动选择平台相关的默认值和自适应调整Java雄可以减少手工对垃圾收集进行调优的工作量,这称为自动调优( Ergonomics).

Java6 Update18进一步増强了该特性,改善了富客户端应用的性能。本节首先介绍 Java I.4.2 Hotspot VM初始时,堆、垃圾收集器和T编译器的默认值,然后是Java5自动调优选择的默认值,以及Java6 Update18中自动调优的改进。

Java 1.4.2的默认值

Java1.4.2 Hotspot VM中,垃圾收集器、JT编译器和Java堆采用以下默认值。

  • Sea收集器,即-XX:+ UseserialGC.
  • Client JIT编译器,即- client.
  • Java堆的初始和最小值为4MB,最大为64MB,即-Xms4m和-Xmx64m。

Java 6 Update 18更新后的默认优化值

Java6 Update18为了更好地适应富客户端应用,进一步改进了优化技术。当JVM认为系统是非服务器类机器时,就会启用这些增强。记住,服务器类机器的定义是底层配置至少为2GB物理内存,至少有2个虚拟处理器的系统。因此,这些增强其实就是应用在物理内存少于2GB、虚拟处理器少于2个的系统上。

对于非服务器类机器而言,仍然会自动选择客户端JIT编译器。然而,Java雄尺寸的默认值相比以前有所改变,并且垃圾收集的设置也进行了更好地调整。目前Java6 Update18在物理内存不超过192MB时,最大雄是物理内存的1/2.物理内存不超过1GB时,最大堆是物理内存的1/4.物理内存1G或者超过1G的系统,默认最大堆为256MB。非服务器类物理内存不超过512MB的系统,初始堆为8MB。物理内存512MB到1GB之间的初始和最小堆是1/64的物理内存。物理内存1GB或者超过1GB,默认的初始和最小堆就是16MB。另外Java6 Update18设定新生代空间是Java堆的1/3.如果指定CMS收集器而没有另外设定Java雄、初始、最小、最大或者新生代空间尺す,Java6 Update18则重新使用ava5的优化值。
在这里插入图片描述

自适应Java堆调整

JVM的自动优化开启 Throughput收集器时,会开启另一个称为自适应堆调整( Adaptive Heap Sizing)的特性。通过评估应用中对象的分配速率和生命期,自适应堆调整试图优化 Hotspot VM新生代和老年代的空间大小。 Hotspot VM监控Java应用中对象的分配速率和生命期,然后决定如何调整新生代空间,使得存活期短的对象在尚未提升到老年代之前就能被收集,同时允许存活时间长的对象适时地被提升,避免在 Survivor区之间进行不必要的复制。可以显式指定 Hotspot VM初始时的新生代大小,例如-Xmn、-XX: NewSize、-XX: MaxNewSize、-XX: NewRatio及XX: SurvivorRatio,这些是新生代调整的起始点。自适应调整就从这些初始设置开始,自动调整新生代空间大小。

JVM的自动优化开启 Throughput!收集器时,会开启另一个称为自适应堆调整( Adaptive Heap Sizing)的特性。通过评估应用中对象的分配速率和生命期,自适应堆调整试图优化 Hotspot VM新生代和老年代的空间大小。 Hotspot VM监控Java应用中对象的分配速率和生命期、然后决定如何调整新生代空间,使得存活期短的对象在尚未提升到老年代之前就能被收集,同时允许存活时间长的对象适时地被提升,避免在 Survivor区之间进行不必要的复制。可以显式指定 Hotspot VM初始时的新生代大小,例如-Xmn、-X: Newsize、-XX: MaxNewSize、-XX: NewRatio及XX: SurvivorRatio,这些是新生代调整的起始点。自适应调整就从这些初始设置开始,自动调整新生代空间大小。

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页