《java性能优化权威指南》

编程入门 行业动态 更新时间:2024-10-08 04:33:55

《java<a href=https://www.elefans.com/category/jswz/34/1771266.html style=性能优化权威指南》"/>

《java性能优化权威指南》

文章目录

  • 一、基本架构
  • 二、HotSpot VM运行时
    • 1、命令行选项
    • 2、VM生命周期
    • 3、VM类加载
      • (1)类加载阶段
      • (2)类加载器委派
      • (3)启动类加载器
      • (4)类型安全
      • (5)HotSpot类元数据
      • (6)内部的类加载数据
    • 4、字节码验证
    • 5、类数据共享
    • 6、解释器
    • 7、处理异常
    • 8、同步
    • 9、线程管理
      • (1)线程模型
      • (2)线程创建和销毁
      • (3)线程状态
      • (4)VM内部线程
      • (5)VM操作和安全点
    • 10、C++堆管理
    • 11、Java本地接口(JNI)
    • 12、VM致命错误处理

一、基本架构

HotSpot VM有3个主要组件:VM运行时(Runtime)、JIT编译器(JIT Compiler)、内存管理器(Memory Manager)

早期的HotSpot VM是32位JVM,内存地址空间限制为4G。随着服务器系统内存越来越大,64位HotSpot VM出现。它增大了Java堆,使得这些系统可以使用更多内存。虽然64位寻址对一些应用有帮助,但64位VM也带来了性能损失:HotSpot VM内部Java对象表示(称为普通对象指针, Ordinary Object Pointers,或oops)的长度从32位变成了64位,导致CPU高速缓存行(CPU Catch Line)中可用的oops变少,从而降低了CPU缓存的效率。

Java 6 HotSpot VM添加了压缩指针(Compressed oops,-XX:+UseCompressedOops 开启)
压缩指针之所以能改善性能,是因为它通过对齐(Alignment),还有偏移量(Offset)将64位指针压缩成32位。换言之,性能提高 是因为使用了更小更节省空间的压缩指针而不是完整长度的64位指针,CPU缓存使用率由此得以改善,应用程序也能执行得更快。另外,有些平台更多的CPU寄存器可以避免寄存器卸载(Register Spilling)。

二、HotSpot VM运行时

HotSpot VM各组件中,VM垃圾收集器和JT编译器最受关注,而VM运行时环境则常常被忽略,虽然它提供的恰恰是HotSpot VM的核心功能。

HotSpot VM运行时环境担当许多职责,包括命令行选项解析、VM生命周期管理、类加载、字节码解释、异常处理、同步、线程管理、Java本地接口、VM致命错误处理和C++ (非Java )堆管理。下面将详细介绍VM运行时系统的上述各项职责。

1、命令行选项

HotSpot VM运行时系统解析命令行选项,并据此配置。HotSpot VM其中一些选项供HotSpotVM启动器使用,例如指定选择哪个JIT编译器、选择何种垃圾收集器等,还有一些经启动器处理后传给完成启动的HotSpot VM,例如指定Java堆的大小。

命令行选项主要有3类:标准选项(Standard Option )、非标准选项( Nonstandard Option)和非稳定选项( Developer Option)

  • 标准选项是Java Virtual Machine Specification 要求所有Java虚拟机都必须实现的选项。
  • 非标准选项(以-X为前缀)不保证、也不强制所有JVM实现都必须支持。
  • 非稳定选项(以-XX为前缀)通常是为了特定需要而对JVM的运行进行校正,并且可能需要有系统配置参数的访问权限。

命令行选项用于控制HotSpot VM的内部变量,每个变量都有类型和默认值:

  • 带有布尔标记的非稳定选项,选项名前的+或-表示true或false,用以开启或关闭特定的HotSpot VM特性或参数。形如:-XX:+AggressiveOpts-XX:-AggressiveOpts
  • 带有附加选项的非稳定选项,形如-XX:0ptionName=<N>。几乎所有附加选项为整数的非稳定选项,整数后面都可以接后缀k、m、g,表示干、百万及十亿。
  • 有一小部分选项没有分隔标记,而是选项名后直接跟选项值,这和特定的命令行选项及解析机制有关。(类似枚举值)

2、VM生命周期

HotSpot VM运行时系统负责启动和停止HotSpot VM。

启动HotSpot VM的组件是启动器。HotSpot VM有若干个启动器。Unix/Linux上最常用的是java, Windows上是java和javaw。启动器启动HotSpot VM时会执行一系列操作。步骤概述如下:

  • (1)解析命令行选项
  • (2)设置堆的大小和JT编译器
  • (3)设定环境变量
  • (4)如果命令行有-jar选项,启动器则从指定JAR的manifest中查找Main-Class,否则从命令行读取Main-Class。
  • (5)使用标准Java本地接口(Java Native Interface, JNI )方法JNI-CreateJavaVM在新创建的线程中创建HotSpot VM
  • (6)一旦创建并初始化好HotSpot VM,就会加载Java Main-Class,启动器也会从JavaMain-Class中得到Java main方法的参数。
  • (7) HotSpot VM通过JNI方法CallStaticVoidMethod调用Java main方法,并将命令行选项传给它。

3、VM类加载

HotSpot VM和Java SE类加载库共同负责类加载。术语类加载用以描述类名或接口名映射到类(Class )对象的整个过程,类加载的3个阶段:加载、链接和初始化。类加载的最佳时机是在解析Java字节码类文件中常量池符号的时候。Java API如Class.forName ()ClassLoader.loadClass()反射APIJNI-FindClass都可以引发类加载。HotSpot VM自身也可以引发类加载。此外,作为链接阶段的一部分,类文件验证也需要加载一些其他类。实际上,加载阶段是HotSpot VM和特定类加载器如java.lang.ClassLoader之间相互协作的过程。

(1)类加载阶段

  • 加载:对于给定的Java类或接口,类加载时会依据它的名字找到Java类的二进制类文件,定义Java类,然后创建代表这个类或者接口的java.lang.Class对象。如果没有找到Java类或接口的二进制表示,就会抛出NoClassDefFound

  • 链接第一步是验证,检查类文件的语义、常量池符号以及类型。如果检查有错,就会抛出VerifyError。第二步是准备,它会创建静态字段,初始化为标准默认值,以及分配方法表。请注意,此时还没有执行任何Java代码。第三步是解析符号引用,这一步是可选的

  • 初始化运行类构造器。这是迄今为止,类中运行的第一段Java代码。值得注意的是,初始化类需要首先初始化超类(不会初始化超接口)。

出于性能优化的考虑,通常直到类初始化时HotSpotVM才会加载和链接类。这意味着,类A引用类B,加载A不一定导致加载B (除非B需要验证)。执行B的第一条指令会导致初始化B,从而加载和链接B。

(2)类加载器委派

当请求类加载器查找和加载某个类时,该类加载器可以转而请求别的类加载器来加载。这被称为类加载器委派。类的首个类加载器称为初始类加载器( Initiating Class Loader ),最终定义类的类加载器称为定义类加载器( Defining Class Loader)。就字节码解析而言,某个类的初始类加载器是指对该类进行常量池符号解析的类加载器。

类加载器之间是层级化关系,每个类加载器都可以委派给上一级类加载器。这种委派关系定义了二进制类的查找顺序。Java SE类加载器的层级查找顺序为启动类加载器、扩展类加载器及系统类加载器。系统类加载器是默认的应用程序类加载器,它加载Java类的main方法并从classpath上加载类。应用程序类加载器可以是Java SE系统自带的类加载器,或者由应用程序开发人员提供。扩展类加载器则由Java SE系统实现,它负责从JRE (Java Runtime Environment,Java运行环境)的lib/ext目录下加载类。

(3)启动类加载器

启动类加载器由HotSpot VM实现,负责加载BOOTCLASSPATH路径中的类。为了加快启动速度, Client模式的HotSpot VM可以通过称为类数据共享(Class DataSharing)的特性使用已经预加载的类。这个特性默认为开启,可由HotSpot VM命令行开关-Xshare:on开启,-Xshare:off关闭。

(4)类型安全

Java类或接口的名字为全限定名(包括包名),Java的类型由全限定名和类加载器唯一确定。换言之,类加载器定义了命名空间,这意味着两个不同的类加载器加载的类,即便全限定名相同,仍然是两个不同的类型

(5)HotSpot类元数据

类加载时,HotSpot VM会在永久代创建类的内部表示instanceklass或arrayklass。HotSpot VM内部使用称为klass0op的数据结构访问instanceklass。后缀"Oop"表示普通对象指针,所以klass0op是引用java.lang.Class的HotSpot内部抽象,它是指向Klass (与Java类对应的内部表示)的普通对象指针。

(6)内部的类加载数据

类加载过程中, HotSpot VM维护了3张散列表。SystemDictionary包含已加载的类。PlaceholderTable包含当前正在加载的类。LoaderConstraintTable用于追踪类型安全检查的约束条件。这些散列表都需要加锁以保证访问安全,在HotSpot VM中,这个锁称为SystemDictionary_lock。通常,HotSpot VM借助类加载器对象锁对加载类的过程进行序列化。

4、字节码验证

Java是一门类型安全语言,在链接时必须进行字节码验证以保障类型安全。字节码验证规定了Java虚拟机需要进行字节码的静态和动态约束验证。如果发现任何冲突,Java虚拟机就会抛出VerifyError并且阻止链接该类。

许多字节码约束都可以进行静态检查,另外有些指令的参数类型和个数约束检查需要在执行过程中动态分析代码。目前有两种判断指令操作数类型和个数的字节码分析方法。常用的方法称为类型推导(TypeInference),第二种方法是类型检查(Type Verification)

对于字节码验证,类型检查比常用的类型推导来得快也更为轻巧,如果类型检查验证出错, HotSpot VM就会切换成类型推导进行验证,如果类型推导失败,则抛出VerifyError。

5、类数据共享

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

6、解释器

HotSpot VM解释器是一种基于模板的解释器。JVM启动时,HotSpot VM运行时系统利用内部TemplateTable中的信息在内存中生成解释器。TemplateTable包含与每个字节码对应的机器代码,每个模板描述一个字节码。HotSpot VM TemplateTable定义了所有的模板,并提供了获得字节码模板的访问函数。结合命令行选项-XX:+UnlockDi agnosticVMOptions-XX:+PrintInterpreter就可以查看生成在内存中的模板表。

HotSpot VM解释器基于模板的设计要好于传统的switch语句循环方式。

解释器使得HotSpot VM运行时系统能够执行复杂的操作。

HotSpot VM解释器也是整个HotSpot VM自适应优化的重要部分。事实上,对所有程序来说,大量时间主要花费在一小部分代码的执行上。HotSpot VM没有逐个方法进行“即时”或“提前”编译,而是直接用解释器运行程序,并在运行中分析代码并监测程序中的重要热点( Hot Spot ),然后用全局机器代码优化器(Global Machine Code Optimizer )集中优化这些热点。避免编译那些很少执行的代码, JIT编译器可以在与程序性能密切相关的部分集中更多注意力,还不用增加总体编译时间。

术语"JIT编译器”并没有很好地描述HotSpot VM编译器如何生成优化的机器代码。它实际上是通过研究程序的运行行为动态生成机器代码,而不是“即时”或者“提前”编译程序。

在程序运行时,JVM会持续动态监控热点,及时调整性能,从而完全适应程序的运行和用户的需求。

7、处理异常

当与Java的语义约束冲突时,Java虚拟机会用异常通知程序。异常处理由HotSpot VM解释器、JT编译器和其他HotSpot VM组件一起协作实现。异常处理主要有两种情形,同一方法中抛出和捕获异常,或由调用方法捕获异常。后一种情况更为复杂,需要退栈才能找到合适的异常处理器。当VM遇到抛出的异常时,就会调用HotSpot VM运行时系统查找该异常最近的处理器(Handler)。如前所述,如果在当前方法中没有找到异常处理器,当前的活动栈帧就会退栈,重复这个过程直至找到异常处理器。一旦发现适当的异常处理器,HotSpot VM的执行状态就会更新,并跳转到该异常处理器继续执行Java代码。

8、同步

广义上说,同步是一种并发操作机制,用来预防、避免对资源不适当的交替使用(通常称为竞争),保障交替使用资源的安全。Java用称为线程的结构来实现并发。互斥(Mutual Exclusion)是同步的特殊情况,即同一时间最多只允许一个线程访问受保护的代码或数据。HotSpot VM用monitor对象来保障线程运行代码之间的互斥。Java的monitor对象可以锁定或者解锁,但任何时刻只能有一个线程拥有该monitor对象。只有获得monitor对象的所有权后,线程才可以进入它所保护的临界区。Java中临界区由同步块( Synchronized Block)表示,代码中用synchronized语句表示

偏向锁最好情况下成本甚至为零。既然大多数对象在其生命期中最多只会被一个线程锁住,那就可以开启-XX:+UseBiasedLocking允许线程自身使用偏向锁。一旦开启偏向锁,该线程不需要借助昂贵的原子指令就可以对该对象进行锁定和解锁了。

大多数HotSpot VM同步操作使用称为fast-path代码(快速路径代码)的方法。如果需要阻塞或者唤醒线程(分别是monitor-enter或者monitor-exit状态),fast-path代码将调用slow-path代码。slow-path代码由C++实现,而fast-path代码则是IT编译器产生的机器代码。

HotSpot VM的标记字(Mark Word)中可能存放以下对象同步状态。

9、线程管理

线程管理涉及从线程创建到终止的整个生命周期,以及HotSpot VM线程间的协调。

(1)线程模型

HotSpot VM的线程模型中,Java线程( java.lang.Thread实例)被一对一映射为本地操作系统线程。

(2)线程创建和销毁

HotSpot VM有两种引入线程的方式,执行Java代码时调用 java.lang.Thread对象的start0方法,或者用JNI将已存在的本地线程关联到HotSpot VM上。

  • java.lang.Thread实例以Java代码形式表示线程。
  • HotSpot VM内部以C++类JavaThread的实例表示 java.lang.Thread实例。JavaThread也保存了它所关联的OSThread实例的引用。
  • OSThread实例代表操作系统线程。

当java.lang.Thread启动时, HotSpot VM创建与之相关联的JavaThread和OSThread对象,最后是本地线程。所有的HotSpot VM状态(如线程本地存储和分配缓存、同步对象等)准备好后,启动本地线程。

终止线程会释放所有已分配的资源,并从已知线程列表中移除JavaThread,然后调用OSThread和JavaThread的析构函数,当它的初始启动方法完成时,最终停止运行。

HotSpot VM使用JNI的AttachCurrentThread与本地线程关联,并创建与之关联的OSThread和JavaThread实例,然后执行基本的初始化。
一旦关联,线程就可以通过其他JNI方法调用任何它所需要的Java代码。

(3)线程状态

从HotSpot VM的角度看,主线程可以有以下状态:

  • 新线程:线程正在初始化的过程中。
  • 线程在Java中:线程正在执行Java代码。
  • 线程在VM中:线程正在HotSpot VM中执行。
  • 线程阻塞:线程因某种原因(获取锁、等待条件满足、休眠和执行阻塞式1/0操作等)而被阻塞。

为了便于调试,用工具报告线程转储、栈追踪等信息时,还需要包括其他的状态信息。这些信息由HotSpot内部的C++对象OSThread维护。包括的线程状态信息如下所示。

  • MONITOR_WAIT:线程正在等待获取竞争的监视锁。
  • CONDVAR_WAIT:线程正在等待HotSpot VM使用的内部条件变量(没有和任何Java对象关联)。
  • OBJECT_WAIT: Java线程正在执行java.lang.object.wait()

(4)VM内部线程

  • VM线程:是C++单例对象,负责执行VM操作。
  • 周期任务线程:是C++单例对象:也称为WatcherThread,模拟计时器中断使得在HotSpotVM内可以执行周期性操作。
  • 垃圾收集线程:这些线程有不同类型,支持串行、并行和并发垃圾收集。
  • JIT编译器线程:这些线程进行运行时编译,将字节码编译成机器码。

(5)VM操作和安全点

HotSpot VM内部的VMThread监控称为VMOperationQueue的C++对象,等待该对象中出现VM操作,然后执行这些操作。因为这些操作通常需要HotSpot VM达到安全点后才能执行,所以它们会直接传递给VMThread。简单来说,当HotSpot VM到达安全点时,所有的Java执行线程都会被阻塞,在安全点时,任何执行本地代码的线程都不能返回Java代码。

垃圾收集是最为人所知的HotSpot VM安全点操作,更明确地说是垃圾收集的Stop-The-World阶段。JVM会阻塞或停止所有Java执行线程执行Java代码。如果程序线程正在执行本地代码(如JNI),可以继续执行,不过一旦跨过本地代码边界进入Java代码时就会被阻塞。

在安全点时,VMThread用Threads_lock阻塞所有正在运行的线程,VM操作完成后,VMThread释放Threads_lock

10、C++堆管理

除了HotSpot VM内存管理器和垃圾收集器所维护的Java堆以外,HotSpot VM还用C/C++堆存储HotSpot VM的内部对象和数据。从基类Arena衍生出来的一组C++类负责管理HotSpot VM C++堆的操作,这些类只供HotSpot VM使用,并不会暴露给HotSpot VM的使用者。

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

11、Java本地接口(JNI)

Java本地接口(本文后面称为JNI )是本地编程接口,它允许在Java虚拟机中运行的Java代码和用其他语言(例如C、 C++和汇编语言)编写的程序和库进行协作

JNI本地方法可以用来创建、检测及更新Java对象、调用Java方法、捕获并抛出异常、加载类并获取类信息以及执行运行时类型检查。

切记,一旦在应用中使用JNI,就意味着丧失了Java平台的两个好处。第一:一旦使用JNI就失去了Java承诺的特性,即“一次编写,到处运行”。第二:Java是强类型和安全的语言,本地语言如C或C++则不是。在调用JNI方法前,Java应用常常需要安全检查。额外的安全检查的数据复制会降低应用的性能。作为一般性准则,开发人员应该设计好应用的架构,将本地方法限定在尽可能少的类中。

HotSpot VM的命令行选项(-Xcheck:jni )可以辅助调试使用JNI的本地方法。

HotSpot VM内部JNI函数的实现比较简单。

HotSpot VM追踪正在执行本地方法的线程时必须特别小心。

12、VM致命错误处理

当HotSpot VM因致命错误而崩溃时,会生成HotSpot错误日志文件,名为hs_err_pid<pid>.log,这里<pid>是崩溃HotSpot VM进程的id, hs_err_pid<pid>.log文件生成在HotSpot VM的启动目录下。

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

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

当HotSpot VM遇到致命错误时,内部使用VMError类收集信息并导出成hs_err_pid<pid>.log

-XX:OnOutOfMemoryError=<cmd>当抛出第一个OutOfMemoryError时,可以执行一条命令。另一个值得提及的有用特性是,当OutOfMemoryError出现时可以生成堆的转储信息,指定-XX:+ HeapDumpOnOutOfMemory可以开启这个特性,-XX: HeapDump-Path=<pathname>可让用户指定堆转储的存放路径。

当发生死锁时,在Windows平台上可以用Ctrl+Break生成Java级别的线程栈追踪信息并打印到标准输出。在Solaris和Linux平台上。发送SIGOUIT信号给Java进程id也可以得到同样效果。基于线程的栈迫踪信息,可以分析死锁根源。从Java6开始,自带的JConsole工具添加了一项功能,可以关联到一个挂起的Java进程并分析死锁的根源。多数情况下,死锁是由于获取锁的顺序错误所导致的

更多推荐

《java性能优化权威指南》

本文发布于:2024-02-19 14:33:40,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1764439.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:性能   权威   指南   java

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!