Skip to content

Latest commit

 

History

History
4267 lines (2319 loc) · 165 KB

JVM调优.md

File metadata and controls

4267 lines (2319 loc) · 165 KB

JVM 调优

第一章:概述

想继续JVM调优,必须先理解JVM虚拟机的内存模型

总的来说,JVM的堆内存在物理上分为两部分

  1. Young Generation (年轻一代)
  2. Old Generation (老一代)

第二章:年轻一代

young Generation是所有新对象创建的地方

当young generation 充满时,垃圾回收就会开始执行

这种垃圾回收被称为Minor GC

Young Generation分为三个部分

即一个Eden Memory 和两个Survivor Memory

有关Young Generation的几个重要观点

  1. 绝大多数新对象创建的内存区域,都是在Eden Memory里面

  2. Eden Memory充满时,JVM会执行Minor GC ,并将还存活的对象全都移动到其中一个

    Survivor Memory内存区域中去

  3. Minor GC 也会检查Survivor Memory内存区域中存活的对象,并将其移动到另一个Survivor Memory内存中。所以每次执行完后,其中一个Survivor Memory内存区域总是空的

  4. 经过多次Minor GC的摧残后还能活下来的对象,将被移动到养老院,也就是Old Generation (老一代)的内存空间。

    到底有多少次Minor GC呢?这就涉及到一个门槛问题了 ​

第三章:年老一代

Old Generation 内存中包含的对象一般都是活的很久,并经过多次的垃圾回收后还存活的。

通常垃圾回收是在Old Generation的内存满的时候中执行的。

Old Generation的垃圾回收又被称为Major GC 通常需要更多的时间

第四章 :垃圾回收

一个很重要的特性:

所有垃圾回收都是时间静止时间,也就是说,当垃圾回收执行时,所有应用程序线程都会停止,直至操作完成

第五章:性能测试

一般来说,软件开发的传统过程是这样的

分析-设计-编码-测试.

但是测试仅仅只是测试功能是否满足需求.

至于性能或者扩展性,那是之后的事了.

当然这样可不行,所以后来才会出现性能测试分析阶段.

要通过这个阶段测试性能是否满足要求

如果不满足,需要返回分析,设计,编码的某个阶段.具体探讨是哪一部分导致了性能问题.

如果满足了,产品才可以继续发布.

当然在需求文档中,必须要有性能需求,不然探讨性能优化就毫无原因了.

举几个吞吐量和延迟性需求

  1. 应用预期的吞吐量是多少?

  2. 请求和响应之间的延迟预期是多少?

  3. 应用支持多少并发任务

  4. 把并发任务达到最大值时,应用程序可接受的吞吐量和延迟量是多少?

  5. 最差情况的延迟量是多少

  6. 如果要使垃圾收集导致的延迟控制在可容忍访问内,

    则垃圾收集的频率应该设计为多少

第六章:性能调优方案

3.1 定义

改善性能一般涉及到三种不同的活动

  1. 性能监控

    指的是非侵入式的收集应用的性能数据的活动.

    一般在找不到性能问题的根本原因时,使用这种方法

    由于是非侵入式,可以在开发,测试,生产环境中使用

  2. 性能分析

    这是一种侵入式的收集性能数据的活动,会影响应用的吞吐性或者响应性

    通常在性能监控后执行

  3. 性能调优

    这是为了改善应用响应性或者吞吐性的而更改参数,源代码,或者属性配置的活动.

    一般在性能监控或者性能调优后执行

第一种:自顶向下

简而言之,就是从软件的栈顶层应用,从上到下一步步寻找优化机会和问题

应用开发人员常用这种方式.

这种方式一般是从发现性能问题的区域(负载)开始监控应用.

监控的范围包括操作系统,JAVA虚拟机,JAVAEE容器以及应用的性能测试统计指标

基于监控信息,来开展下一步工作,比如JVM垃圾收集器调优,JVM命令行选项调优

操作系统调优.

第二种:自底向上

就是从最底层的CPU指令数据(比如说CPU高速缓存未命中率,CPU指令效率)开始,逐渐上升到应用的架构上.

将应用迁移到其他操作系统时,或者应用以及部署到生产环境时,也常常使用这种方法

自底向上需要收集最底层CPU的性能统计数据

​ 包括执行特定任务所需要的CPU指令数(通常称为路径长度 Path Length)

​ 应用在一定负载下运行的CPU高速缓存未命中率.

自底向上一般关注的是不改动应用的情况下,改善CPU使用率.不过通常自底向上也可以为如何修改应用提供建议

第七章:选择合适的CPU架构

有时候,性能差仅仅是运行的CPU架构或者系统不合适.所以,为应用选择一个合适的CPU架构就显得尤为重要.

一般来说,如果你的应用会有比较多的并发线程,那么推荐的CPU架构就是每核多硬件线程

系统会把每一核的每一个硬件线程都看作一个处理器.

这样的话,在发生长延迟时,比如说CPU高速缓存未命中,如果同一个CPU核中还有其他就绪的硬件线程,

那么,在下一个时钟周期,就会让这个硬件线程运行,从而减少等待时间.

相反,如果你用的是每核单硬件线程,当发生长延迟事件,就只能等事件完成而浪费时间周期.

并且,如果其他线程已经就绪却没有可用的硬件线程,那么运行前就必须进行线程的切换,而切换,一般要数百个时钟周期.

但是,对于不需要大量并发的应用而言,就没必要选用每核多硬件线程的CPU,何况这种CPU,它的时钟频率通常比较低,比单核单硬件线程的还要低.

这种应用的话,还是老老实实选用单核单硬件线程的CPU吧

为了评估单核多硬件CPU的性能,一般是要加载大量的并发线程的

另外,单核多硬件线程的CPU典型代表型号是Oracle公司出品的SPARC T 系列处理器

第八章:操作系统的性能监控

5.1 cpu使用率

毋庸置疑,想让应用的性能达到最高,就必须充分榨干CPU周期.

值得注意的是,应用消耗很多CPU并不意味着性能达到最高

要找出应用如何使用CPU周期,就要监控CPU使用率.

CPU使用率分为两种

  1. 用户型CPU使用率:指的是执行应用程序代码的时间占总CPU时间的百分比

  2. 系统型CPU使用率:应用执行操作系统设备调用的时间占总CPU时间的百分比.

    这个CPU使用率高的话,说明应用有共享资源竞争或者IO设备之间有大量的交互

理想情况下,系统型CPU使用率为0时,应用会达到最高的性能.

但是理想是达不到的,我们只能尽量降低系统型CPU使用率

5.1.2 监控CPU使用率的工具:Linux

  1. 图形界面

LINUX可以使用GNOME System Monitor

通过这个命令启动

fdggnome-system-monitor

据说只能普通用户启动,超级用户看不到资源和文件系统的栏目

听说还可以用xosview看cpu使用率,但是我用了之后就死机了..

  1. 命令行

    通常用vmstat来监控CPU使用率,听说加个-w参数会比较好哦

    另外,显示出来的us 是用户型cpu使用率,sy是系统型cpu使用率,id是cpu空闲率

    也可以用top监控CPU使用率使用率

## 5.2:番外篇

对于计算密集型应用,还要关注它的每时钟指令数以及每指令时钟周期.

一般平台提供的CPU使用率监控是看不到每时钟指令数以及每指令时钟周期的

所以需要其他骚操作来监控这两货

这是因为即使CPU在等待内存中的数据,操作系统工具还是会说CPU繁忙.

这种情况被称为停滞,这种停滞还会浪费数几百个时钟周期.

想要提高计算密集型应用的性能时,就要尽量减少停滞和提高CPU高速缓存使用率

5.3:CPU调度程序运行队列

除了cpu使用率之外,监控cpu调度程序运行队列对于分辨系统是否满负荷也是具有重要的意义.

如果调度程序运行队列很长,则表名系统可能已经满负荷.

当系统运行队列长度等于虚拟处理器个数时,用户不会感受到性能下降.

虚拟处理器个数=系统硬件线程个数=Runtime.getRuntime().availableProcessors();

如果系统运行队列长度达到虚拟核心数的4倍或者更多时,系统的响应就很迟缓了

一般说来,只要队列的长度超过虚拟处理器个数的1倍,需要盯紧他,不需要采取行动

如果是超过3-4倍以及更高,就需要采取行动了.

行动1:增加cpu

行动2:研究可以减少应用运行所需cpu周期的葵花宝典,改进cpu使用率

​ 比如减少垃圾搜集的频率或者采用更优的算法以减少cpu指令

5.3.1 linux上的监控调度程序运行队列

还是要请我们的老朋友vmstat

其结果的第一列procs就是运行队列长度

5.4:内存使用率

内存使用率当然也是衡量性能的一个重要指标.

当运行所需内存超过物理可用内存时,就会发生页面交换.

就是将不常用的内存放进磁盘的swap空间.当要访问到这部分内存时,就要把它从磁盘上置换到内存中,而这种置换会对应用的响应性和吞吐量造成很大的影响.

而且,JVM垃圾搜集器受其系统页面交换,性能也会很差.

其一是因为垃圾搜集要占用很多内存

其二由于占较多内存,则java堆内存的一部分可能会发生页面交换,而垃圾搜集时又得置换进内存以便垃圾收集器扫描存活对象.这使得垃圾搜集会占用更多时间.

其三:垃圾搜集时所有java的应用都要停止.然后你懂得

经验:如果垃圾搜集的时间变长,则表明系统可能正在进行页面交换.

为了验证这一点,必须监控系统的页面交换

5.4.1:监控内存使用率:linux

还是老朋友vmstat命令,其中free列就可以监控页面交换.其实它显示的是空闲内存

要关注的还有siso列,分别表示内存页面换入和换出的量.

要留意是否会出现空闲内存少,并且页面调度频繁的情况,相比而言,数量并不是很重要..

5.5:监控锁竞争

一般情况下,锁都应该是很快获取,很快释放.如果要是遇到有一个线程,迟迟不释放锁,那么其他需要锁的程序就只能空转,不断的尝试获取锁,这样对系统的性能会有较大影响.

接下来,我们就要找出Java应用中的锁竞争.

首先来了解一些java1.5之后推出的锁竞争,它是怎么运作的

应用通过忙循环自旋尝试获取锁,如果若干次忙循环自旋都不能获取到锁,那么线程挂起,直到下次唤醒时再尝试获取该锁.

而线程挂起和唤醒都会造成让步式上下文切换这种切换通常会浪费数万个时钟周期.

那么怎么看程序是不是遇到了锁竞争呢?

对于任何java应用来说,如果让步式上下文切换占用它5%或者更多的时钟周期,说明它遇到了锁竞争

顺便说一下抢占式上下文切换,这个切换表明线程由于CPU分配的时间片用尽而被迫放弃执行权或者被更高优先度的线程所抢占.和让步式上下文切换不同,后者是自愿放弃cpu执行权的

可以通过pidstat -w 查看抢占式上下文切换的数值

如果抢占式上下文切换的数值较高,则表明系统会有较多的预备运行线程数.

引发的问题可以看下5.3:CPU调度程序运行队列

像这种问题的解决方案

  1. 创建处理器组并将应用分配给处理器组运行,linux可以通过taskset命令完成
  2. 减少应用运行的线程数
  3. 分析应用,优化性能(不常用)

5.5.1查看Linux上的锁竞争

可以通过这个命令来查看pidstat

该命令可能需要安装sysstat

该命令显示的cswch/s就是应用的让步式上下文切换.

根据这个数值怎么判断应用已经遇到了锁竞争呢?

  1. 用该数值*80000这就是应用由于让步式上下文切换而兰妃的时钟周期

    80000就是一个让步式上下文切换而浪费的时钟周期,当然只是估算而已

  2. 用得出来的结果除以CPU每秒的时钟周期数,就是让步式上下文切换占用的可用时钟周期

    CPU每秒的时钟周期数,如3Ghz CPU每秒的时钟周期数就是3000000000

如果发现应用确实存在锁竞争,那么接下来就是在源代码中查找那一部分是有竞争锁的.

一般来说,都是要定期转储线程,查找锁竞争的线程

5.6 线程迁移

待处理的线程在处理器之间进行迁移也会导致性能下降.

一般来说,待运行的线程都会分配给上次运行它的处理器。

如果这个处理器正忙,那么将会把待运行的线程迁移到其他处理器上。

这个迁移也会降低应用的性能,这是因为迁移后的处理器一般没有这个线程需要的缓存。

降低线程迁移的一个办法就是创建处理器组并将应用分配给处理器组

一般情况下,如果横跨多核或者虚拟处理器的Java应用每秒迁移超过500次,那么把应用绑定在处理器组是非常用的。

5.7 网络IO使用率

分布式Java应用的性能受限于网络带宽或者网络io的性能。

所以我们需要监控应用的网络使用率。

在linux上,可以使用这个命令nicstat 它可以报告网络使用率和网络接口的饱和度

命令输出的列名含义如下

Int网络接口设备名

rKb/s每秒读取的KB数

wKb/s每秒写入的KB数

rPk/s每秒读取的包数

wPk/s每秒写入的包数

rAvs,每秒读取的平均字节数

wAvs每秒写入的平均字节数

%Util网络接口使用率

Sat饱和度

怎么考虑优化网络IO使用率

单次读写数据量小的网络读写数据量大的应用会消耗大量的系统态CPU,产生大量的系统调用

当然想优化的话,就要尽量减少网络读写的系统调用。

使用非堵塞的应用框架NIO也可以显著改善网络性能。

建议使用nio框架而不要使用jdk自带的nio实现,它只是一种原始实现。

5.8 磁盘IO使用率

对于有磁盘操作的应用来说,改善性能,自然就要监控磁盘IO

linux上可以使用iostat来监控磁盘IO使用率

该命令可以监控磁盘IO使用率和系统态cpu使用率

其中%system是系统态CPU使用率,然后%util是磁盘IO使用率

但是这个命令并不能看出是哪个应用在操作磁盘。

直接跳过吧。。。

5.8.1 怎么优化磁盘IO使用率?

  1. 更快的存储设备

  2. 文件系统扩展到多个磁盘?

  3. 操作系统调优,缓存大量文件系统数据结构?

    此外,还有尽量使用带缓存的输入输出流来减少读写次数。

    以及开启磁盘缓存,可以改善严重依赖磁盘IO的应用的性能。

    但是如果一旦断电的话,可能会导致数据损坏

第九章:JVM概览

本章主要讲述的是jvm的架构以及它的主要组件

JVM主要有以下三个组件

  1. JVM运行时
  2. JIT编译器
  3. 内存管理器

9.1 HotSpot VM的基本架构

基本架构就是

垃圾收集器(可插拔)

JIT编译器(可插拔)

HotSpot VM 运行时

​ HotSpot VM 运行时为JIT编译器和垃圾收集器提供服务和通用的API,此外,它还为VM提供启动,线程管理

​ JNI(Java本地接口)等基本功能

内存相关

java堆内存的大小受限于HotSpot版本和操作系统

早期操作系统和hotspot的版本都比较小,所以能用的内存也会比较小

现在大不相同的了,特别是64位的HotSpot VM 能用的java堆内存就更大

虽然64的寻址对一些应用可能有帮助,但是64的HotSpot有性能损失。

因为内部的java对象指针从32位变成了64位,导致cpu高速缓存行可用的oops变少,从而降低了cpu缓存的效率

不过后来加了压缩指针,使得64位的大内存和cpu效率可以兼得。要通过-xx:+UseCompressedOops开启。

9.2 HotSpot VM运行时

这货担当许多职责。

  1. 命令行解析

  2. VM生命周期管理

  3. 类加载

  4. 字节码解释

  5. 异常处理

  6. 同步

  7. 线程管理

  8. java本地接口

  9. vm致命错误

  10. c++堆管理

9.2.2 VM生命周期

本章主要讲的是java程序运行前,终止或者退出时,jvm做了什么事?

  1. 启动

    jvm启动所用的组件是启动器.这启动器有四种

    1. linux系统下的jvm启动器java
    2. windows系统下的jvm启动器 java or javaw
    3. JNI接口的JNI_CreateJavaVm启动内嵌的jvm
    4. javaws启动器,用于启动applet.ws即指的是web start 而术语java web start就是指java ws

    启动器做的操作有

    1. 解析命令行选项

    2. 设置堆的大小,设置JNI编译器

    3. 设定环境变量和读取classpath

    4. 如果,命令行选项里面有-jar那么启动器就从指定jar文件里面找Main-Class

      否则从命令行读取Main-class

    5. 使用java标准本地接口和方法在新创建的线程中创建VM

    6. 一旦创建好vm,那么就开始加在主类了,启动器也会从主类中获取到主方法的参数

    7. vm通过jni方法callstaticvoidmethod调用主方法,并将命令行选项传给他

      至此,jvm开始执行命令行指定的java程序了

  2. 关闭

    jvm又做了什么事呢?

    1. 检查和清理程序或者方法执行过程中生成的未处理异常

    2. 调用java本地接口方法DetachCurrentThread将java main方法和jvm脱离

      每次脱离,线程数都会-1,所以java本地接口知道什么时候应该安全的关闭VM

      并能确保VM此时并没有执行的操作

  3. 遇到错误关闭VM的过程

    当VM启动时,或者运行时遇到很严重的问题,会调用DestroyJavaVM方法停止VM

    过程是这样子的

    1. 一直等待,直到只有一个非守护线程执行(也就是当前线程)

    2. 调用java.lang.Shutdowm.shotdowm(),它会调用java上的ShutDowm钩子方法.

    3. 运行java上的ShutDowm钩子方法,停止一系列线程,并发出状态事件通知jvmTI,然后关闭jvmTI线程.

      最后停止信号线程

    4. 调用Hotspot的javaThread::exit()释放JNI处理块,移除保护页.将当前线程从已知的线程队列中移除.

      从此刻起,VM就无法执行任何java代码了

      保护页是不可访问的内存页,用于内存访问区域的边界

    5. 停止vm线程,并将vm线程带到安全点,并停止JIT编译器的线程

    6. 停止追踪jni,hotspot vm 及jvmti屏障

    7. 为那些仍然以本地代码运行的线程是设置标记vm exited

    8. 删除当前线程

    9. 删除所有输入/输出留,移除perfMenory(性能统计内存资源)资源

    10. 最后返回给调用者

9.2.3 VM类加载

类加载用于描述类名或者接口名映射成class类对象的整个过程

这个过程有三个阶段:1. 加载 2.链接(验证,准备,解析) 3初始化

  1. 加载:首先要从类的全限定名获取二进制字节流,定义java类,然后创建这个类的Class对象

    这个过程会抛出

    1. NoClassDefFound 找不到这个类的二进制流
    2. ClassFormatError or UnSupportedClassVersionError 语法检查出错
    3. ClassCircularityError 类的继承层次有错 ,比如说自己做爸爸,自己又做儿子
    4. IncompatibleClassChangeError 也是类的继承层次有错,比如说实现的不是接口,是类

    这个加载过程一般是由类加载器来完成的,可以使用系统自己提供的类加载器,也可以自己选择

    关于加载器的更多知识可以看下一章

  2. 验证

    不能保证所有被JVM加载的Class都是经过javac编译过来的,为了保证虚拟机自身的安全,通常会对类进行验证

    验证包括以下过程

    1. 文件格式的验证,保证这个class文件的格式是符合规范的,并能被当前版本的虚拟机所识别.

      经过该验证,Class文件就会load到内存内

    2. 对元数据进行校验(对类中各个数据类型进行语法校验)

    3. 对类的方法体进行分析,保证方法体不会作出危害虚拟机的行为

    4. 符号引用验证,,对类以外的信息进行匹配性的校验

  3. 准备

    该过程主要是为类变量在方法区分配内存,并设置初始值的阶段

    值得注意的是,设置的初始值不是在代码中已经写好的值,而是数据类型默认的初始值

    设置代码中写好的值是在初始化阶段才完成的

    比较特别的是,被static final修饰的变量(其实这种情况也叫常量啦),在准备过程中,值就会被初始化成指定的值

  4. 解析

    解析阶段是对常量池中的符号引用转换为直接引用的过程

    符号引用就是Class文件在编译过程中,生成的三类常量

    1.类和接口的全限定名

    2.字段的名称和描述符

    3.方法的名称和描述符

    转换为直接应用的过程就是分析这些符号应用,翻译成直接引用,load到内存中

    什么,直接引用又是什么鬼,嗯,就是能直接指向目标的指针.

    如果有了直接引用,那么引用的目标一定在内存中了

    解析不一定在类初始化之前完成,也有可能是之后(将要被使用时才解析)

    根据符号引用的类型,有不同的解析方案

    1. 类或接口的解析:判断要解析的直接引用是数组类型,还是普通对象类型

    2. 字段解析:要查找的是简单名称和字段描述符都匹配的目标字段.

      先查找本类,查找到,over,没有,下一个

      查找父接口-祖父接口-如果没有,下一个

      查找父类-祖父类...直至找到

    3. 类方法解析,跟字段解析是一样的,多了判断方法是属于接口的,还是类的,以及是先搜索父类,再去搜索接口

    4. 接口方法解析,也是基本一样,不过接口方法没有父类,所以只从父接口一级一级向上查找即可

  5. 初始化

    该过程就真正的执行了Java代码了,在这个过程,类变量会真正的赋上程序员给它的值,而不是默认的初始值了.

    该过程也是执行类构造器<clinit>()方法的过程

    几个小知识:

    1. 该方法是编译器自动收集类变量赋值动作和静态构造代码块合并之后产生的.

    搜集的顺序也就是语句在源文件中的顺序

    特别注意的是,静态代码块可以访问在它之前已经定义过的类变量

    如果类变量定义在静态代码块后面,那么静态代码块只能赋值,不能访问

    1. <clinit>()方法在调用时会调用父类的<clinit>(),所以在虚拟机第一个被执行<clinit>()的类肯定是Object类,另外这个方法是编译时自动生成的..

    2. 不是所以的接口或者类都有<clinit>()方法,如果类没有静态代码块和类变量的赋值操作,那么编译器可以选择不生成该方法

    3. 接口不能使用静态代码块,但是仍然有类变量的赋值操作,所以还是可以生成<clinit>()方法的

    4. 执行接口的<clinit>方法时并不会调用父类的<cliinit>方法

    5. 接口的实现类执行<clinit>方法时并不会调用接口类的<clinit>方法

    6. <clinit>()方法是线程安全的,也就是说,活动线程没执行完<clinit>()方法的话,

      其他想执行这个方法的线程都得等,万一这个方法执行的过程很长,那么就会造成线程堵塞

    7. 什么时候会执行clinit>()方法呢?其实也是在问,什么时候会进行类的初始化呢?

      当类的方法或者字段被访问到时.---(个人观点)

      值得注意的是,调用子类去访问父类的字段时,子类并不会初始化

      因为这个字段还是属于父类的,不是子类的

      完毕!

而这个过程需要hotspot vm和java se 类加载库来共同协作.

其中,vm负责解析常量池符号,这个过程需要加载,链接,然后初始化java类和java接口

那么,什么时候会触发类加载呢?

  1. vm自身引发
  2. Class.forName()
  3. ClassLoader.loadClass()
  4. 反射API以及JNI_FindClass引发类加载

关于类加载的几个小知识

  1. vm启动时,除了加载普通类,也会加载诸如java.lang.Object和Java.lang.Thread这些核心类
  2. 加载类也需要加载它的所有超类和所有超接口
  3. 作为链接阶段的一部分,类验证也需要加载其他类

9.2.4 类加载器

对于我们程序员来说,类加载器总共就三种

  1. 启动类加载器,这个是用c++编写的类加载器,是虚拟机的一部分

    这个类加载器负责加载jdk/jre/lib目录下面的,或者-Xbootclasspath参数指定的路径中类库

    对于许多包名是java or javax 来说,这些包里面的类总是会委托给启动类加载器来加载

  2. 扩展类加载器,负责加载JDK\jre\lib\ext目录中类

  3. 应用程序类加载器,这个的话,负责加载classpath下面的类

    其实还不止这些类记载器,Oracle还规范了四五个类加载器,这里就不讲了

    java应用程序都是通过以上的类加载器进行加载的,我们也可以自定义自己的类加载器

    那么什么时候会用到自定义类加载器呢?

加载器有一个层次关系,又称双亲委派模型(这名字谁起的,怪异)

这个层次关系是这样的

启动类加载器<<<<<扩展类加载器<<<<<应用程序加载器<<<<<自定义加载器

其实这个是简化的层次关系,你要知道,类加载器不止这四种

当一个类加载器接收到类加载请求时,它会委派它的上一级类加载器去加载该类.

所以一般来说,所有类加载请求最终都会交由启动类加载器进行加载

不一般来说,如果上级的类加载器都不能加载该类,那只能由它自己尝试加载类了

这种机制被称为类加载器委派.

这种机制最重要的用处就是确定JVM中类的唯一性.

因为JVM中类的唯一性是由类的全限定名和类加载器来确定的

这你就懂了吧,为了确保jvm里面的类,其类名不会冲突

这也就意味着,就算类的全限定名相同,只要是由两个不同的类记载器加载的,那么就不是同一个类

equals方法都是挂的,当然,这种情况java程序就不稳定了,必须杜绝这种情况

顺便提一下定义类加载器和初始类加载器

类的首个加载器被称为初始类加载器,然后这个初始类加载器会委托另一个类加载器去加载类.

这个真正加载类的类加载器被称为定义类加载器.

比如说,A类引用了B类,那么对B进行常量池符号解析的类加载器被称为A的定义类加载器,B的初始类加载器

9.2.5 异常处理机制

异常会导致程序控制的非局部转移,从异常抛出的地方,转移到程序员指定或者异常被捕获的地方.

异常有两种情形

1:由同一个方法抛出和捕获异常

2:由调用方法捕获异常

第二中稍微有点麻烦,需要退栈才能找到合适的异常处理器,也就是捕获异常的地方.

有三种信息可用于查找异常处理器

  1. 当前方法

  2. 当前字节码

  3. 异常对象

如前所诉,如果当前方法没有找到异常处理器,那么当前活动栈帧就会退帧.

直到找到合适的异常处理器,VM执行状态就会更新,然后跳转到异常处理器的代码继续执行

异常可以由字节码,VM内部调用返回,以及JNI_调用返回或者java调用返回引发.

9.2.6 解释器

jvm解释器是基于模板实现的.jvm启动时,vn运行时利用内部的TemplateTable中的信息在内存中生成解释器.

TemplateTable包含每一个字节码对于的机器码.

TemplateTbale定义了所有的模板,并提供了字节码的访问函数,每一个模板都描述一个字节码

jvm这种基于模板的解释器要好于switch语句循环的形式..

switch语句执行重复的比较操作,最差情况需要和所有的字节码进行比较.

解释器也可以在运行代码的同时.分析代码,检测程序中的重要热点,集中性能优化这些热点代码,避免编译那些很少执行的代码.这个是Hotoopt自适应优化的一个重要部分

这里所指的编译,是把字节码翻译成机器码,解释执行

9.2.7 同步

广义上:同步是一种并发操作机制,用来预防,避免对资源不恰当的交替使用(竞争)

9.2.8 线程管理

线程管理涉及到线程的创建到终止-整个的生命周期,以及vm线程中的协调

线程管理的线程包括:java代码创建的线程,直接与hotSpot关联的本地线程,hotSpot为其他目的创建的内部线程

  1. 线程模型

    线程模型中,java线程会被映射为本地操作系统线程,当java线程开启时,也会随之创建一个本地操作系统线程

    同理,终止java线程时,也会销毁一个本地操作系统线程

  2. 线程的创建和销毁

    在hotSpot vn中,引入线程有两种方式,

    1. 通过java.lang.Thread实例的start()方法.
    2. 通过jni将本地线程关联到hotSpot vn上

    hotSpot vn内部的很多对象,都和线程相关,拿出来溜一下

    1. java.lang.Thread实例用java代码来表示线程

    2. hotSpot内部用C++的JavaThread实例来表示一个java.lang.Thread实例

      但是JavaThread不仅于此,它还保存了其他线程状态的追踪信息.

      JavaThread用普通对象指针来引用保存了它所关联的java.lang.Thread实例

      java.lang.Thread实例也用原始整数的方式保存了JavaThread实例的引用

      JavaThread实例也保存了OSThread(操作系统线程)的引用.

      一思考,感觉还挺有意思的,c++代码是基于操作系统的,java代码是用c++代码实现的

      所以Java的线程要映射到c++的线程,c++的线程要映射到操作系统的线程

      大概是酱紫的

         Java层面     |     HotSpot VM层面     | 操作系统层面
      java.lang.Thread | JavaThread -> OSThread | native thread
      

      接下来讲一个Java线程的生命周期

      1. java线程启动,,同时也会创建JavaThread实例和OSThread的实例,以及一个本地线程
      2. 当所有的vm状态准备好时,启动本地线程
      3. 本地线程启动,也就执行的Thread里面的run方法
      4. run方法返回时
        1. 处理未处理的异常
        2. 终止该线程(这步会导致释放所有以分配的内存)
        3. 检测该线程结束时是否vm也要终止
  3. 线程状态

    VM可以使用许多内部状态来表示线程现在正在做什么?

    从Hotspot vn 的角度上看,线程主要有以下四种状态

    1. 新线程:线程正在初始化的过程中
    2. 线程在Java中,线程正在执行java代码
    3. 线程在jvm中,线程正在jvm中运行
    4. 线程堵塞:线程由于某些原因:获取锁,等待条件满足,睡眠,阻塞性IO而被堵塞.

    还有一些其他的状态信息,这些信息有助于调试所用,主要由vm内部的c++对象,osThread

    维护,主要有这么几种

    1. MONITOR_WAIT:线程正在获取竞争锁
    2. CONDVAR_WAIT:线程正在等待hotSpot vn 使用的内部条件变量(没有和任何java对象关联)??
    3. Object_WAIT java线程正在执行WAIT()方法
  4. vm 的内部线程

    哪怕是一个hello world 程序,都会导致jvm创建大量的线程.

    这些线程由vm库以及vm内部线程所产生,vm内部线程如下所示

    1. vm线程:它是以一个c++单例对象,负责vm操作
    2. 周期任务线程:也是一个c++单例对象,也叫WatcherThread,模拟计时器中断使得vm可以执行周期性操作
    3. 垃圾搜集线程:支持并发,串行,并行的垃圾搜集
    4. JIT编译器线程:负责运行时编译,将字节码编译成机器码
    5. 信息分发线程:等待接收进程发来的信号并将信息分发给java的信息处理方法
  5. 安全点

    安全点是什么?不知道

    但是能知道的是,当hotSpot vm到达安全点时,所有Java执行线程都会被堵塞,本地代码也不能返回到Java代码.

    当到达安全点时,jvm可以进行很多内部操作,其中一种就是GC。

    在安全点时,所有线程都被显式的处于堵塞状态。这会造成应用程序的延迟。。

9.2.9 其他零碎知识

jvm中的c++堆:除了vm内存管理器和垃圾收集器所维护 java堆之外,jvm还维护了一个c++堆,用于存储Hotspot vm的内部对象和数据

Java本地接口:也就是俗称的JNI,它允许Java代码和其他语言编写的程序和库进行协作.

VM致命错误处理:

OutOfMemoryError是常见的vm致命错误

还有一个Segmentation Fault是常见的致命错误

(Linux和Solaris,Windows等价的错误是Access Violation)

当发生vm的致命错误时,通常会生成hs_err_pid.log的错误日志文件.一般来说,它会生成在vm的启动目录下

这个文件的内容主要是内存镜像,可以很清楚的看到vm奔溃时的内存布局

提供-xx:ErrorFile可以设置日志文件的路径

9.2.10 垃圾收集器

终于到了比较重要的一章:垃圾收集器.要知道,垃圾收集器运行的方式和执行的效率会对应用造成极大的影响

所以,性能优化中,垃圾收集器是需要着重了解的一章

首先了解一下一个垃圾收集器的算法:分代垃圾收集算法.

这个算法的设计基于两个观察事实

  1. 大多数分配对象的存活时间都很短
  2. 存活时间长的对象很少引用存活时间短的对象

这两个观察事实被称为弱分代假设,基于这个假设,jvm将堆分为两个物理区,这就是分代

物理区1:新生代:大多数创建对象都会分配在新生代中.对于整个jvm堆来说,新生代垃圾搜集比较频繁而且效率很高

​ 这是因为新生代空间小,而且里面对象活不长,一波新生代垃圾搜集(记为Minor GC)就可以带着大量的狗带对象.

物理区2:老年代:Minor GC执行多遍之后仍然坚挺的对象会被放到老年代中.

​ 老年代的空间比较大,但是占用增长的速度会很慢.

​ 因此,相对于Minor GC而言,老年代的垃圾搜集(记为Full GC)执行频率会比较低,但是一旦发生,就够应用程序喝 一壶的了

物理区3:永久代,这是vm物理区域的第三部分,虽然被称为代,但是其实不应该把它看做分代层次的一部分

也就是说,老年代的对象就别想跑到永久代的物理区域中存活了,该GC掉还是得GC掉

永久代一般用于存储元数据,比如说类的数据结构,保留字符串

Minor GC还使用了一个叫做卡表的小策略,用于识别新生代的存活对象.

因为很多新生代的存活对象都是在老年代的存活对象有它的引用,它才可以存活下来的.

那要检查新生代的存活对象岂不是要扫描整个老年代,找出持有新生代对象的引用的老年代对象?

不用.老年代可以以512字节为块分成若干个卡.把这些卡当做数组的每一个元素,组成一个数组,就是卡表了.

每当老年代对象持有的新生代对象引用发生变化时,vm就必须将该老年代对象所在的卡标记为脏

然后在Minor GC中,只会扫描脏卡来识别新生代的存活对象

分代垃圾收集的优势

什么优势呢?就是啊,每一个分代都可以根据特性选择最适合它的垃圾收集器

对于新生代来说,使用的垃圾收集器一般选择速度快的,因为Minor GC执行频繁,而且只浪费一点内存空间

对于老年代来说:使用的垃圾收集器选择空间利用效率高的,但是速度慢的.因为老年代占的空间比较大.不过Full GC执行的频率低,所以也不是什么大问题

分代垃圾收集的优势仅当你的应用符合弱分代假设,它才可以发挥作用,否则适得其反.

不过,实践中,不符合弱分代假设的应用很少见.

##### 9.2.11新生代和老年代

上面所讲的新生代还不够仔细,其实,新生代还包含三个独立区域

  1. Eden区:一般新创建的对象都会放在这里,不过也有例外,比如说,big对象就会直接放在老年代中

  2. 两个Survivor区:这里存放的对象至少经过一次Minor GC,而且在跑路到老年代中还有不止一次的Minor GC

    其他文献会给这两个Survivor起名,一个叫From 一个叫To

那么Minor GC主要干了什么事呢?

它首先会清空Eden区,并将存活的对象转到一个未被使用的Survivor区,同时,另一个Survivor区存活下来的对象也会复制到那个未被使用的Survivor区,然后,被占用的Survivor区存活时间长的对象会被转移到老年代.

这会引发一个问题,当未被使用的Survivor区无法接收太多的,来自Eden区和另一个Survivor区的存活对象的话.

就会导致溢出,多余的对象会被转移到老年代中,这个叫过早提升,会导致老年代空间占用增长迅速.

而一旦老年代满了的话,MInor GC就放不入存活的对象了.所以Minor GC之后通常就会进行Full GC

这会导致遍历这个java堆,也被称为提升失败

所以Minor GC之后呢,Eden区和其中一个Survivor区总是空的.

PS:整个垃圾收集的过程都是在复制存活对象,所以这种垃圾收集器被称为复制,垃圾收集器

那么Full GC又干了什么事呢?

Full GC 收集整个堆,包括新生代,老年代,永久代,关于详细的说明,下面会讲到

##### 9.2.12 快速内存分配

快速内存分配需要内存分配器和垃圾搜集器的准确配合.

垃圾收集器会记录搜集的空间,这个空间就可以给内存分配器参考,参考是否有空闲的内存区域可以容纳新的内存分配请求.

更详细一点的说,Minor GC之后,Eden和其中一个Survivor区总是空的.

这样的话,内存分配器就可以使用一个叫指针碰撞的黑科技,只需要检查最后一个分配的对象,记为top,和内存区末端

之间是否有空闲的空间,如果有的话,新分配的内存空间就放在top的后面.

类比一下瓶子装沙子,每次Minor GC后都会把沙子完全倒空,新来的沙子就直接洒在旧沙子的上面.

这样的话,就能保证瓶子的每一个空间都能得到有效的利用

不过,这个例子不太恰当..如果倒沙子的过程不干净的话.唔..不太能说明问题呢

另外,快速内存分配其实要考虑一个线程安全的问题的,如果有多个线程同时请求内存分配.

那么就要给内存分配这块加锁.这样的话,执行效率太慢了.

Hotspot使用一种叫做线程本地分配缓冲区技术(TLAB),为每一个线程分配缓存区(Eden的一部分),这样每一个缓存区只有一个线程在分配内存,可以采取指针碰撞的技术快速分配内存,不用加锁了.

当然,如果为线程分配的缓存区不够用咋办,这个时候,就需要加同步锁,不过很少见.

9.2.13 Serial垃圾收集器

接下来的几章将讲解垃圾搜集器的几种类型

这章就先讲Serial收集器.

这种收集器在进行Minor gc时,采取的算法跟之前描述的垃圾收集算法是一致的,这里的就不讲了

但是在Full GC 中,采取的是滑动压缩标记-算法,所以也被称做压缩标记垃圾收集器

这个算法是这么处理的:

先收集老年代存活的对象,将它们推向堆的头部,然后留下堆尾部一段连续的内存空间.

这使得对于老年代的内存分配仍然可以采取指针碰撞的方式.

这种垃圾收集器适用于停顿要求不高,和在客户端运行的应用.它只用一个处理器进行垃圾收集(Serial之名由此而来)

它只需要几百兆java堆就能有效管理很多应用,而且在最差的情况下也能保持比较少的停顿..

同一机器运行多个jvm实例时也常用这个垃圾收集器

ps:jvm进行垃圾收集时,最好只使用一个处理器处理,虽然这比较慢,但对其他jvm影响最小.

这一点Serial搜集器做的很好

9.2.14 Parallel收集器(ThroughPut收集器)

该垃圾收集器可以加大应用程序的吞吐量,减少垃圾收集的开销.

它使用的算法和Serial垃圾收集器是一样的

也就是新生代使用之前描述的垃圾搜集算法

老年代使用标记,压缩方式

但是Minor GC 使用的是并行方式,可以使用所有可用的处理器资源.

这种垃圾收集器广泛用于

  1. 需要高吞吐量的应用
  2. 极端情况下Full GC 引起的停顿时间要求高
  3. 运行在多处理器系统之上的应用.

与Serial处理器想比,Parallel处理器改善了垃圾收集器的效率,从而也提高了应用的吞吐性

-xx:+UseParalleloldGC or ``xx:+UseParallelGC`选项可以开启Parallel处理器

前者是新生代和老年代的垃圾收集都采取多线程。

后者只有新生代的垃圾收集使用多线程,老年代还是使用单线程。

有些HotspotVM是不支持第一种选项的,你只能用第二种选项,也就是只有新生代使用多线程的垃圾收集。

另外,开启这个垃圾收集器,额外赠送一个自适应大小调整的特性哦,能自动调整Eden和Survivor空间的大小

9.2.15 Mostly-Concurrent收集器

-*XX:+*UseConcMarkSweepGC 开启

该垃圾收集器致力于低延迟的收集

该垃圾收集器在新生代垃圾收集和之前描述的,没什么特殊的

但是在Full GC 中,则是尽可能的并发执行,每一个垃圾收集周期只有两次小的停顿

一个垃圾收集周期会有四个阶段

  1. 开始标记阶段:该阶段会标记所有外部可达的对象

  2. 并发标记阶段,该阶段会标记从上一个阶段的对象可以到达的对象.

    其实在并发标记之后,重新标记之前,还有一个并发预清楚标记阶段

    该阶段主要抢了重新标记的一些工作.

    也就是重新遍历在并发标记阶段改动的对象.

    虽然这个阶段之后重新标记仍然会进行,但是它减轻了重新标记的工作量,减少了由此产生的停顿

  3. 重新标记,由于在并发标记阶段,应用可能还在运行,一些已经标记的对象的引用可能会发生变动.

    该阶段追踪那些变动的对象并进行最后的标记.

    怎么追踪呢?还记得之前谈过的卡表吗,同理的

  4. 并发清除

    在这个阶段中,会清除这个Java堆,释放没有迁移的对象.

    但是由于并发,最后清除的空闲空间是不连续的,无法再用指针碰撞技术快速分配内存.

    只能用一个数据结构(HotSpot使用空闲列表)记录空闲的空间.

    这使得Minor GC 提升新生代对象时,会产生额外的开销

Mostly-Concurrent收集器的缺点

  1. 如前所诉,这种垃圾收集器的Minor GC效率比较低

  2. 需要更大的java堆,因为在整个垃圾收集周期中,只有清除阶段才会释放堆空间.

    而其他阶段,应用可能还在分配内存

  3. 在三个标记阶段中,尽管这个垃圾收集器可以保证找到所有的存活对象,但是并不能保证能找出所有的垃圾对象

    在标记期间的垃圾对象,可能在本次周期清除阶段被清除,也有可能不被清除.

    侥幸躲过一次垃圾收集周期的垃圾被称为浮动垃圾.

  4. 由于最后的清除阶段缺少压缩,会导致空间碎片化,因此垃圾收集器无法最大程序的利用所有可用的内存空间

  5. 如果在一个垃圾收集周期时,还未回收到足够的空间,而老年代已经满了.

    那么收集器就会退而求其次,使用代价昂贵的Stop-The-World进行空间压缩.

    就像之前说的那两个垃圾收集器一样

该垃圾收集器适用于

  1. 需要快速响应的应用,对吞吐量要求不是很高的应用
  2. Full GC停顿要求时间少
9.2.16: Garbage_first收集器

该收集器(缩写为G1)是为了替代上一个垃圾收集器.

它是一个并行的,并发的增量式压缩低停顿的垃圾收集器

这种垃圾收集器的堆布局和其他垃圾收集器的堆布局有着极大的不同的.

它把java堆分成相同尺寸的块(称为区域).

G1的几个特性

  1. 虽然G1也分代,但是整体上并没有分为老年代和新生代

  2. G1的每一代是一组区域(可能不连续),这使得它可以灵活分配哪一个是新生代

  3. G1的垃圾收集是把区域中的存活对象转移到另一个区域.然后收集前者(通常前者占的空间更大)

  4. G1大部分只收集新生区域(这些形成了G1的新生代),这种收集过程相当于Minor GC

  5. G1有时也会并发标记那些非新生区域,而且是那种有很多垃圾的区域.

    然后回收它们,这通常会产生大量的可用空间

    这个策略就是:优先回收垃圾对象最多的区域,这也就是这个垃圾收集器的由来

9.2.17 应用程序对垃圾收集器的影响
  1. 内存分配

    应用内存分配的速率越快,垃圾收集触发的就越频繁.

    因为速率越快,就意味着分代占用的内存会很快挤满,新生代挤满来一发Minor GC

    老年代挤满来一发Full GC

  2. 存活数据的多少

    一般说来,每个分代存活的对象越多,垃圾收集器要做的事也就越多

    因为Minor GC 是复制存活对象

    Full GC 是标记-复制-存活对象

    复制这么多的存活对象,你说垃圾收集器它累不累啊?

  3. 老年代的更新

    如果老年代的对象的引用发生了变化,就会产生一个Old-To-Young的引用

    如果这在CMS的一次回收周期中发生了.这就会导致在预清除或者重新标记阶段就产生的一个需要的便利的对象

    一些对于优化垃圾收集器的效率的编码建议

    1. 慎用对象池化,池化对象会长时间存活,占用老年代空间,初始化对其写操作也会增加老年代引用更新的数据
    2. 不合适的数组类数组尺寸.比如说ArrayList的数组尺寸过小,它内部的数组可能会频繁的扩充.导致不必要的内存
9.2.18 垃圾收集总结

其实也不算小结了,一些额外的知识会在这里补充。

  1. 生存代的含义

    一个对象经历垃圾收集的次数可以认为是对象的年龄。

    在堆内存中,不同年龄的对象就组成了代。

    比如说,在系统启动过程中,分配了第一组对象。

    随后经历40次垃圾收集后,又分配了第二组对象。

    最后又经历了40次收集,分配了第三组对象。

    那么,在系统经历这80垃圾收集后。

    第一组对象的年龄是80岁。

    第二组对象的年龄是40岁

    第三组对象的年龄是1岁。

    整个堆内存代的个数就是3,,因为堆上所有对象只有三个不同的年龄

    代对系统优化的意义

    在大多数系统中,代的个数最终会稳定下来。因为持久对象对象已经分配完成,而短命的对象不会对代的个数产生影响。

    一旦你要发现代的个数总是在增长,那么很有可以系统发生了内存泄漏。

    因为应用程序总是在不断的分配对象,但是对象又没有清除掉。代的个数就会持续增长

9.3 JIT编译器

编译是指从源代码生成机器码的过程.

传统的编译是先把源代码编译成二进制目标文件,然后再链接成可执行文件或者库文件.

Java的编译有所不同,它是先把Java源代码编译成类文件,然后送到虚拟机里面

(或者搜集到jar文件里面再送进虚拟机)

虚拟机再动态的将字节码转成机器码.

编译器工作的流程

  1. 由前端接收源代码-转成中间代码(IR)

    生成的中间代码通常是编译器优化最集中的地方

  2. 后端接收这些中间代码-转成机器码

    这个过程包括指令选择,以及寄存器的分配

    指令选择可以由编译器作者直接处理指令选择,或者程序自己自动选择指令

    寄存器分配是指将寄存器分配给程序中的所有变量

    但是程序所有变量太多,寄存器数目太少,不能全部分配怎么办.

    这就牵扯到一个寄存器分配算法

    1. 轮循调度算法,对于简单的代码生成,用这个就行了,但也仅限于简单代码生成

    2. 图着色算法:这个主要是在堆中维护一个图表数据结构,里面用来表示哪些变量被同时使用,并且它们可以使用哪个寄存器.

      如果活跃的变量数超过了寄存器数,那么不重要的变量会被移动到栈中,使得其他变量可以使用寄存器.

      图着色算法可以使寄存器的使用率达到最高,并且多余的值很少被卸载到栈中.

      但是图算法花费的时间和空间都比较昂贵

    3. 线性扫描寄存器分配:它的目标是单趟扫描所有指令时指派寄存器.

      并且指派还不错.

      不过缺点是不能保证在变量的生命周期中,它都留在一个寄存器内.

    值从寄存器取下来叫值卸载,也叫寄存器卸载

9.3.1 类的继承性分析

在面向对象的代码中,代码往往被分成一个个小方法.如果能将这些小方法内联起来,

代码运行效率就可以有很大的提升

为什么,如果不内联的话,正常代码要调用另一个方法的时候,需要保存现场,并记忆执行的地址.

调用完毕后要恢复现场,回到原来的地址继续运行,这样会有额外的开销

方法内联的意思就是把将要调用的方法,方法体转移到调用的地方..

这样就没有调用一说了,直接执行方法体就相当于调用了.

但是Java在处理内联方法上有点小问题,首先看下面一段代码

public interface Animal {
    public void eat();
}
public class Cat implements Animal{
    public void eat() {
        System.out.println("cat eat fish");
    }
}
public class Test{
    public void methodA(Animal animal){
        animal.eat();
    }
}

编译器想内联methodA中的animal.eat();方法,但是编译器知道这个方法实际上是哪个吗?

不知道,只有当运行的时候才能知道animal.eat();到底指的是dogeat()还是cateat

那么这个时候编译器就通过CHA(类的继承性分析)假设当前只有一个类,也就是Cat这个类实现这个eat方法.

于是就把methodA中的animal.eat();方法和cat的eat内联起来.

当然,这种内联优化是相当激进了,一旦有新的子类加载进来,并且发现它实现了eat这个方法.

那么Hotspot VM就会通过逆优化把这个CHA假设丢弃.已经编译好的代码也会替换成解释器执行

9.3.2 编译策略

由于JIT没有时间编译程序中的所以方法,所以一开始,程序都是在解释器上运行的.

如果一个方法调用太多次,那么可能就会编译这个方法.

那调用多少次呢,这就涉及到一个阀值,这个阀值通过方法调用计算器和回边计数器来计算

方法调用计数器在方法每次被调用时,都会加1

回边计数器在方法执行过程中,从后面的代码行跳到前面的代码行(比如说循环.经常跳回循环开始的地方)

就会加1.

这两个计数器每次递增,解释器都会和阀值进行比较,以确定要不要编译这个方法..

当发起编译请求时,如果编译器不忙,那么就可以开始编译,此时解释器还可以继续执行代码(计数器要清0)

如果你想等编译代码过程中,不要执行代码,可以使用-Xbatch或者-XX:BackgroudCompilation堵塞执行代码,直到编译完成

当编译的代码完成时,编译代码就会和方法关联,以便下次调用这个方法能执行编译好的代码.

不过上面的这些策略对于一个长时间的循环代码来说并没有什么卵用.

这时候就要用到一种栈上替换的技术,当循环代码长时间执行导致回边计数器溢出时

编译器就会编译循环开始的第一个字节码,直至整个循环(而不是从方法的第一个字节码开始,编译整个方法)

然后用栈上替换的黑科技让循环充分利用编译代码.

9.3.3 逆优化

逆优化是指将若干个内联的编译帧转换成等价的解释器帧的过程.

这个过程可以将编译代码从多种乐观优化中回退回来(典型的乐观优化就是类的继承性分析)

或者当编译器遇到罕见陷阱时,也会使用逆优化.

JIT的逆优化会在安全点上保存一些额外的元数据,一些额外的元数据可以构件一组解释器帧使代码可以从解释器中继续运行.

9.3.4 Client JIT编译器介绍

这个编译器的目标是更快的启动时间和快速编译,使客户不会为了应用糟糕的响应时间而纠结

它的历史

早期:概念类似与解释器,跟解释器一样会为每种字节码生成模板,也维护了一个栈布局,类似与解释器帧

​ 仅支持类的字段访问方法内联.

Java 1.4:支持全方法内联,支持类的继承性分析(CHA),逆优化支持

Java5.咸鱼一条,没有重大改进

Java6:重大改进.IR(中间代码)改成了SSA分格,简单局部寄存器分配升级为线性扫描寄存器分配,内存优化

9.3.5 Server JIT编译器

目标:应用性能达到极致,吞吐量也达到最高..

所以可见这个编译器就要不遗余力的进行优化,接下来就是它表演的时刻了

  1. 极力内联,这通常会造成大方法,导致编译的时间边长
  2. 使用扩展的优化技术,涵盖了大量的极端情况,要满足这些情况,就只能为每一个它可能遇到的字节码都生成优化代码

9.3.6 静态单赋值-程序依赖图

这个看的有点懵,先占个位置,以后涨经验后再补

9.3.7 正在开发的编译器

其实已经开发完成了吧,这个新的编译器名称叫做-混合式JIT编译器。

它糅合了Client JIT编译器和Server JIT编译器的特性,期望能达到快速启动,以及使用更多的优化技术。

9.3.8 jdk各个版本的默认值以及自适应调优

  1. jdk 1.4.32 的默认值

    垃圾收集器:使用Serial收集器

    JIT编译器:Client

    Java堆初始化是4MB,最大值是64MB

  2. jdk 5 默认值

    在jdk5时,引入了一个新特性-自动调优 使 Hotspot VM 可以根据系统的配置选择不同的默认值

    对于ThroughPut收集器来说,还可以根据程序运行的情况,自动调整堆的大小和内存分配的速率

    如果系统配置至少有2GB的物理内存和至少2个虚拟处理器的系统,那么Hotspot VM 采用服务类机器的默认配置

    1. 垃圾收集器:ThroughPut 垃圾收集器
    2. JIT编译器:Server
    3. 初始(最小)堆大小:物理内存的1//64(上限为1GB),最大为物理内存的4/1

    要注意的是,32位的系统不适用服务类机器的定义。

    这些系统使用默认的配置是

    1. 垃圾收集器 Serial收集器
    2. Client编译器
    3. 最小堆内存4MB 最大堆内存 32MB

    想知道你当前的机子使用那些默认值吗?

    可以键入该命令java -XX:+PrintCommandLineFlags -version查看

    结果类似于

    -XX:InitialHeapSize=128791168 -XX:MaxHeapSize=2060658688 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
    java version "1.8.0_161"
    Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
    Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)
    

    可以看到

    1. -XX:+UseParallelGC 使用 ThroughPut(Parallel)垃圾收集器
    2. 初始堆内存128791168字节,最大堆内存2060658688字节
    3. 使用Server JIT编译器,可以通过这个信息Java HotSpot(TM) 64-Bit Server VM 看到
  3. jdk 6 默认值

    jdk6对非服务器类机器进行了优化,当JVM认为机器是非服务器类机器时,就会启动优化

    判断条件:物理内存低于2GB,虚拟处理器少于2个

    优化的内容是

    1. 编译器:还是Client
    2. 堆尺寸进行了优化
    3. 调整了垃圾收集器的设置

9.3.9 自适应java堆调整

当JVM自动优化选择了ThroughPut垃圾收集器,就会开启一个自适应堆调整的优化

就是通过评估对象的分配速率和生命周期,试图优化新生代和老年代的堆大小。

使得存活期小的对象能被及时回收,允许时间长的对象适时提升,避免不必要的Survivor区的复制。

另,程序员可以指定初始值的新生代大小

-Xmn -XX:NewSize -XX:MaxNewSize -XX:NewRatio -XX:SurvivorRatio

自适应Java堆调整不一定适用于所有项目,对于项目的调整性波动,以及对象分配速率的迅速变动,对象生命周期的剧烈变化,自适应java堆调整反而会掉坑,这个时候,就应该关掉自适应调整优化了

-XX:-UseAdaptiveSizePolicy这个命令行参数就可以关掉了。

另外,如果第二个-变成+号,就变成了开启自适应优化了

第十章 JVM性能监控

10.1概况

本章讲述了JVM的性能监控,展示了JVM的监控工具,介绍了观察数据中的应该留意的数据模式。

然后根据数据选择调优方式。

JVM监控包括垃圾收集,JIT编译和类加载

10.2 定义

本章所讲的性能监控,在此之前应该有谈过了。

它是一种非侵入式的,在生产环境,测试环境,或者开发环境中实施的一种带有预防性或者主动性的活动。

当测试人员爆出性能问题时,首先会进行性能监控,然后就是性能分析,最后性能调优

性能监控应该在生产环境中时时刻刻开着。

10.3 JVM监控之-垃圾收集

监控JVM的垃圾收集很重要,因为它对应用的吞吐量和延迟有着极大的影响 .

那垃圾收集要收集的数据有哪些呢?

  1. 当前使用的垃圾收集器
  2. Java堆的大小
  3. 新生代和老年代的大小
  4. 永久代的大小
  5. Minor GC 的持续时间
  6. Minor GC 的频率
  7. Minor GC的空间回收量
  8. Full GC 的持续时间
  9. Full GC的频率
  10. 每个并发垃圾收集周期的空间回收量
  11. 垃圾收集前后Java堆的占用量
  12. 垃圾收集前后老年代和新生代的空间占用量
  13. 垃圾收集器前后永久代的占用量
  14. 是否老年代或者永久代的占用触发Full GC
  15. 应用是否显示调用了System.gc()

这些数据可以由HotSpot报告,这个报告不需要什么开销,在生产环境也可以使用

报告垃圾收集数据的命令行:-XX:+PrintGCDetails可以打印很多有价值的垃圾收集信息。

10.3.1 Minor GC 数据详解

在jdk1.8 版本的中,打印的Minor GC 信息如下

0.143: [GC (Allocation Failure) 
[PSYoungGen: 31744K->5117K(36864K)] 
31744K->16253K(121856K), 0.0328695 secs] 
[Times: user=0.11 sys=0.01, real=0.03 secs] 
  1. 0.143:代表时间戳,实际上,你只是用-XX:+PrintGCDetails这个命令行的话不会输出这个.

  2. GC (Allocation Failure) GC表示是Minor GC 本次Minor GC触发的原因是Allocation Failure

    简而言之,就是内存分配失败(当然,新生代满了还怎么分配内存呐?)

  3. PSYoungGen表示新生代使用的是多线程垃圾收集器Paraller Scavenge

    其他的新生代垃圾收集器就不叫这名了。

    ParNew 是多线程垃圾收集器ParNew,一般配合CMS垃圾收集器

    DefNew则是Serial垃圾收集器

  4. 第二行的31744K->5117K(36864K)中,箭头左面是新生代收集之前的占用量,右边自然就是新生代收集之后的占用量了

    而且注意,Minor GC后Eden为空,所以箭头后的5117K也就是Survivor区的占用量

    括号里面的36864K不是指新生代的占用量,而是Eden和一块被占用的Survivor的容量的和(即新生代的大小)

    疑惑点:如果是这么说的话,括号里面的数字代表了Eden和其中的一块的Survivor的大小

    那么这个值应该是不变的,但是日志GC中,这个值却经常改变?

    猜测原因:

    1. 每次Minor GC,被占用的Survivor区都会切换,所以造成值的变动

    2. GC过程中,新生代的大小会经常微调,所以值会发生变动

    加了-XX:-UseAdaptiveSizePolicy

    后再次运行后发现括号里面的值就不变了,说明猜测2是正确的

    疑惑点:

    作者说Eden和一块被占用的Survivor的容量的和就是新生代的大小

    咦,新生代的大小不是要计算两个Survivor+Eden的容量吗?

    RednaxelaFX也有这个疑问,但是他把这个疑问当做是作者的BUG 他给出的修正方案是说括号里面的数值是指两个Survivor+Eden的容量 ​

  5. 第三行的31744K->16253K(121856K)就是整个堆的信息,

    箭头左边是垃圾收集前整个java堆占用量

    箭头右边是垃圾收集后整个java堆的占用量

    括号就是Java堆的总量(新生代+老年代)

    总量减去上面的新生代大小,就可以计算老年代的大小了呢

  6. 0.0328695 secs就是垃圾收集花费的时间,准确的说,就是Minor GC的持续时间

  7. Times: user=0.11 sys=0.01, real=0.03 secs这一行就是CPU的使用时间

    user=0.01代表用户态CPU使用了0.11s

    sys=0.01代表系统态CPU使用了0.01s

    合计是real=0.03 secs

  8. 隐藏的信息还有Minor GC的发生频率,只要拿下一次Minor GC的时间戳减去上一次Minor GC的时间戳即可

10.3.1 Full GC 数据详解

Full GC的日志详情如下

2.016: [Full GC (Ergonomics) 
[PSYoungGen: 5120K->0K(100864K)] 
[ParOldGen: 150987K->139997K(302080K)] 
156107K->139997K(402944K), 
[Metaspace: 2652K->2652K(1056768K)], 1.5891538 secs] 
[Times: user=3.87 sys=0.02, real=1.59 secs] 
  1. 标签Full GC 代表这是一个Full GC 收集 (Ergonomics暂时未知)

  2. PSYoungGen: 5120K->0K(100864K)和之前说的是完全一样

  3. [ParOldGen: 150987K->139997K(302080K)] 是老年代信息

    其中ParOldGen表示老年代使用的是xx垃圾收集器?

    150987K->139997K(302080K)箭头左边表示垃圾收集前老年代的占用量,箭头右边就是收集后的占用量

    括号里面是老年代的大小

  4. 156107K->139997K(402944K)这个就是java堆的使用情况,是垃圾收集前后新生代和老年代占用量的累计

  5. [Metaspace: 2652K->2652K(1056768K)], 1.5891538 secs]

    Metaspace这个在jdk8之前,应该是PSPermGen即永久代,现在jdk8去掉了永久代,继任者是Metaspace

    箭头前后分别代表垃圾收集前Metaspace的占用量,括号自然就是总量了

    Metaspace这个内存区域使用的是本地内存,理论上,物理内存有多大,这个区的内存就有多大

    但是实际还是要给它做一下容量限制的,可以有vm 自己决定,也可以你自己决定

    一旦使用量超过了,就会诱导Full GC

    这个区主要放类的加载数据,之前放在永久代的字符串已经放在Java堆了。

  6. Full GC中需要着重注意老年代和永久代(or Metaspace)在垃圾收集前后的占用量,如果占用量接近于容量

    就说明本次GC是由于某代引起的。

  7. 最后一行就不用多说了,跟Minor GC 是一样的

小知识点:如果你的程序只有Full GC日志。很有可能是新生代和老年代的大小失去平衡,导致程序只进行Full GC ,这可以从GC日志看到.

 2010-12-06T15:10:11.231-0800: [Full GC
 [PSYoungGen: 196608K->146541K(229376K)]
 [ParOldGen: 262142K->262143K(262144K)]
 458750K->408684K(491520K)
 [PSPermGen: 26329K->26329K(32768K)],
 17.0440216 secs]
 [Times: user=11.03 sys=0.11, real=17.04 secs]
 2010-12-05T15:10:11.853-0800: [Full GC
 [PSYoungGen: 196608K->148959K(229376K)]
 [ParOldGen: 262143K->262143K(262144K)]
 458751K->411102K(6291456K)
 [PSPermGen: 26329K->26329K(32768K)],
 18.1471123 secs]
 [Times: user=12.13 sys=0.12, real=18.15 secs]

可以发现,每次Full GC后新生代的不会清空,这是因为老年代的空间已经不足以接受从新生代提升过来的对象了。所以一些对象会退回新生代,导致Full GC后新生代还有占用。

而且细心一点,每次FUll GC 后,老年代的空间没有多大变化,也就是FUll GC基本是在做无用功。

10.3.2 使用CMS垃圾收集器的GC日志

之所以要单独把CMS垃圾收集器的日志拿出来讲,是因为这货的Full GC 日志长得跟上面的,不一样

Minor GC还好说,只是收集器的型号变了而已。

Full GC的话,看看下面的日志吧

6.390: [GC (CMS Initial Mark) 
	[1 CMS-initial-mark: 148809K(183408K)] 153706K(221488K), 0.0030657 secs]
    [Times: user=0.01 sys=0.00, real=0.00 secs] 
6.393: [CMS-concurrent-mark-start]
6.710: [CMS-concurrent-mark: 0.316/0.316 secs] 
	[Times: user=0.62 sys=0.00, real=0.32 secs] 
6.710: [CMS-concurrent-preclean-start]
6.750: [CMS-concurrent-preclean: 0.040/0.040 secs]
	[Times: user=0.08 sys=0.00, real=0.04 secs] 
6.750: [CMS-concurrent-abortable-preclean-start]
9.191: [CMS-concurrent-abortable-preclean: 2.441/2.441 secs] 
	[Times: user=4.89 sys=0.00, real=2.44 secs] 
9.191: [GC (CMS Final Remark)
	[YG occupancy: 21728 K (38080 K)]9.191: [Rescan (parallel) , 0.0362882 secs]9.227: 		[weak refs processing, 0.0000158 secs]9.227: 
	[class unloading, 0.0002186 secs]9.228: 
	[scrub symbol table, 0.0003964 secs]9.228:
    [scrub string table, 0.0001122 secs]
    [1 CMS-remark: 148809K(183408K)] 170537K(221488K), 0.0371413 secs] 
    [Times: user=0.12 sys=0.00, real=0.04 secs] 
9.228: [CMS-concurrent-sweep-start]
9.305: [CMS-concurrent-sweep: 0.076/0.076 secs] 
	[Times: user=0.15 sys=0.00, real=0.07 secs] 
9.305: [CMS-concurrent-reset-start]
9.308: [CMS-concurrent-reset: 0.003/0.003 secs]
	[Times: user=0.01 sys=0.00, real=0.01 secs] 

整个CMS垃圾收集周期从初始标记开始,到并发重置结束

也就是上面日志的CMS Initial MarkCMS-concurrent-reset

中间的各个阶段也有相应的各个解释

  1. CMS-concurrent-mark表示并发标记阶段的结束

  2. CMS-concurrent-precleanCMS-concurrent-abortable-preclean为重新标记做准备

    也就是之前提到的并发预清楚标记阶段

  3. CMS-remark就是重新标记阶段结束

  4. CMS-concurrent-sweep这个就是清除阶段了

截出来日志并没有包含Minor GC ,但实际上,我观察的其他CMS周期中间是有Minor GC的日志的

类似于

xxxx: [GC (Allocation Failure) 
xxxx: [ParNew: 29510K->4224K(38080K), 0.1203834 secs] 
	73413K->80196K(122752K), 0.1206216 secs] 
	[Times: user=0.37 sys=0.01, real=0.12 secs] 

这些日志穿插在CMS周期,这说明,哪怕是在并发垃圾收集周期时仍然可以进行Minor GC

日志需要注意的地方

  1. 着重注意CMS周期中Java堆占用的减少量,特别是并发清除开始和结束时的Java堆减少量,

这可以在CMS-concurrent-sweep-startCMS-concurrent-sweep的中间找MinorGC的日志看出来

不过并不是每次CMS周期都会有Minor GC ,这事吧,得看人品

所以我觉得作者提出的这个思路并不好。但是看Java堆减少的量确实很重要

如果在整个并发清除前后,Java堆的占用并没有任何改变。

说明CMS的整个周期基本做了无用功,不过也有可能提升到老年代的对象太多,都超过了并发清除的速度了

但是不管是哪两种,都说明JVM需要调优了,关于CMS的收集器调优,请继续看下去

CMS的另一个监控-晋升监控

关于CMS垃圾收集器的监控,还有一项需要注意,那就是晋升分布

晋升分布是一项显示Survivor区对象年龄的直方图,当对象的年龄超过晋升阀值,就会提升到老年代

晋升分布监控的重要性下面会讲解

最后,聊一下什么是并发模式失败,这是在CMS收集器下会发生的一种不好的情况

并发模式失败有两种情况,这两种情况也都是并发模式失败的定义

情况1:当对象提升到老年代的速度过快,但是老年代空间告急

情况2: 老年代的空间碎片化严重,以致没有空间存放从新生代提升到老年代的对象时

并发模式失败时,GC日志会打印concurrent model failure

当发生并发模式失败时,老年代会来一发Stop the World的GC,并压缩老年代空间.

你就这么眼睁睁的看着并发模式失败?不,这个时候,你就要对JVM进行调优了

请查看下面章节:特别是低延迟程序细调

10.3.4 垃圾收集日志的其他要素

  1. 显式垃圾收集

    意思就是本次GC是由于代码中存在System.gc()引起。这个在GC日志可以很清楚的看到

    3.216: [GC (System.gc()) [PSYoungGen: 7K->64K(36864K)] 637K->693K(121856K), 0.0006149 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    

    看见没,上面的System.gc()几个大字闪闪发亮(大雾~)

10.4 JIT编译器监控

JIT编译器虽然加快的运行速度,但它的计算也需要内存和CPU,所以有必要对JIT编译器进行监控

监控的内容包括

  1. 那些方法被优化了(内联了)
  2. 某些情况被逆优化了,或者重新优先了

怎么监控JIT编译器

​ 请出我们的命令行参数-XX:+PrintCompilation

​ 它输出了一下格式的 编译日志(JDK8)

   9321   40  s    3       java.lang.StringBuffer::append (13 bytes)   made not entrant
   9321 3159       4       java.lang.StringBuilder::toString (17 bytes)

第一列是时间戳,编译相对于JVM启动的时间戳

第二列是编译ID

第三列是类型,可以为空或者为下列值

&以栈上替换的方式编译

*|n编译的是本地方法

s编译的是同步方法

!编译的方法有异常处理器

b解释器被堵塞直至编译结束

1编译器没有做完整优化,只是第一层编译

第三列是tiered_level当采用的是tiered compilation编译器时才会打印

第四列是被编译的方法名称,格式是:classname::method

第五列是被编译的代码大小,这里的大小是java字节码大小

第六列是去优化的类型,可以为空或者以下值

made not entrant 编译活动遇到罕见陷阱,需要处理已卸载类的引用,以及乐观优化的回退,这个JIT编译方法可能还有活动,但不允许有新的活动了

made zombie 说明这个编译方法不再有更多的活动了,这通常是某个类被卸载,而引用这个类的其他方法都不再存活了,就会变成made zombie,一旦JIT编译器确信没有其他方法引用了这个zombie方法,这个zombie方法就会从代码缓存中释放

10.5 类加载监控

在jdk8之前,Hotspot将类的加载数据放到永久代中,当永久代空间不够,又发生类的加载活动时,就有一些不常用的类会从永久代中卸载,这个过程会触发Full GC 因此,对类加载活动进行监控是有必要的。

类的加载和卸载都是由类加载器实现了

jdk8之后,类的元数据信息放在了MetaSpace中

示例日志没有找到,就说一点吧。

如果在Full GC中发现类被卸载了,说明永久代大小需要调大点

-XX:PermSize=xxM 和 -XX:MaxPermSize=xxM

这两个命令行选项可以调整永久代的大小,为了避免Full GC会扩大永久代的可分配空间

建议-XX:PermSize 和 -XX:MaxPermSize设置为同一个值?

疑问,还是不懂呢

另外,其实永久代的垃圾收集可以开启并发收集,但是只能和CMS收集器一起使用

另,有一些工具可以图形化的监控类的加载,在下方的工具章会讲到

顺便提一下共享类的加载,共享类的作用是:可以在同一个系统,不同的jvm中共享类,这样可以减少内存占用。不用每一个jvm都加载同样的类了。

10.6 快速监控锁竞争

快速定位Java应用的锁竞争,常用的方式是用jstack抓取线程转储信息。

使用方式jstack -l 3776 后面带java应用的pid

什么,不知道pid,jps了解一下?

这种方法只是用于监控,对于分析嘛,没辙

日志大概是酱紫的


"Thread-1" #10 prio=5 os_prio=0 tid=0x000000005871b000 nid=0x1f68 waiting for mo
nitor entry [0x0000000058e7f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.cxh.thread.DeadLock.run(DeadLock.java:45)
        - waiting to lock <0x00000000d619bc28> (a java.lang.Object)
        - locked <0x00000000d619bc38> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
        - None

"Thread-0" #9 prio=5 os_prio=0 tid=0x000000005871a000 nid=0x1938 waiting for mon
itor entry [0x00000000583ff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.cxh.thread.DeadLock.run(DeadLock.java:34)
        - waiting to lock <0x00000000d619bc38> (a java.lang.Object)
        - locked <0x00000000d619bc28> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
        - None

这个日志说明线程Thread-1Thread-0,都拿到锁了

Thread-1拿到了这个地址的锁0x00000000d619bc38 正在等待0x00000000d619bc28锁的释放

Thread-0拿到了这个地址的锁0x00000000d619bc28 正在等待0x00000000d619bc38 锁的释放

看出什么问题了嘛?没错,这就是典型的死锁,而且线程转储文件还能知道是哪个源代码出现了死锁

如:Thread-1 ,遇到死锁的位置是:com.cxh.thread.DeadLock.run(DeadLock.java:45)

可以说非常有用了。

不过死锁毕竟比较少见,更多的情况是激烈的锁竞争

可以在线程转储文件看到很多个线程都有这么一句话waiting to lock <0x00000000d619bc28>

等待获取锁的地址都是一样的,也就是说,多个线程在竞争同一个锁。

而这个锁现在估计被一个线程占用了locked <0x00000000d619bc28>

然后,知道怎么做了吧,追踪到源代码吧骚年

第十一章:JVM命令行命令以及解释

11.1 GC相关命令行

  1. -XX:+ScavengBeforeFullGC 该命令行可以在Full GC 之前,先来一发Minor GC ,减轻Full GC的工作

    这个选项是布尔类型的,意味着,其实你可以-XX:+ScavengBeforeFullGC FullGC 之前不会Minor GC

    Full GC之前来一次Minor GC 有什么用呢?

    答:在两个GC执行间隙,应用可以继续运行,这样减少了最大停顿时间,但也使整体停顿时间拉长

  2. -XX:+PrintGCTimeStamps打印自JVM启动以来的秒数

  3. -XX:+PrintGCDateStamps打印符合ISO8601标准的时间戳

    这些时间戳可以推断出Minor GC和Full GC的持续时间和频率

  4. -Xloggc:<filename>可以将垃圾收集的日志存储在文件中,以便离线分析

    此日志结合-XX:+PrintGCDetails可以在输出的日志文件中,打印出时间戳(秒数)

  5. -XX:+PrintGCApplicationConcurrentTime 可以报告应用在安全点操作之间的运行时间

    -XX:+PrintGCApplicationStoppedTime 这个是堵塞java线程的时间

    使用了这两个选项后的GC日志

    1.248: Application time: 0.1672635 seconds
    1.248: [GC (Allocation Failure) [PSYoungGen: 31128K->5118K(36864K)] 31128K->10950K(121856K), 0.0339035 secs] [Times: user=0.05 sys=0.00, real=0.03 secs] 
    
    1.283: Total time for which application threads were stopped: 0.0341715 seconds, Stopping threads took: 0.0000153 seconds
    

    第一行表示Java应用运行大约0.1672635秒,Minor GC大约停顿0.0341715 秒

    两个值比对一下,可以得出Minor GC 大约有2%的开销

    关于这两个命令行参数还有需要了解的,之后再去查资料

  6. -XX:+PrintCommandLineFlags

    这个可以打印出JVM启动的命令行参数,可以用于查看堆的初始值和最大值

    打印的结果如下

    -XX:InitialHeapSize=132301888 
    -XX:MaxHeapSize=2116830208 
    -XX:+PrintCommandLineFlags 
    -XX:+UseCompressedClassPointers 
    -XX:+UseCompressedOops 
    -XX:-UseLargePagesIndividualAllocation 
    -XX:+UseParallelGC
    
  7. -XX:MaxTenuringThreshold=<n>可以设置新生代的晋升阈值

  8. -XX:UseParNewGC 可以在新生代使用parNew垃圾收集器

  9. -XX:+PrintTenuringDistribution 可以打印出对象年龄的分布

  10. -XX:SurvivorRatio=<ratio>这个是单个Survivor空间和Eden空间的大小的比率,默认值为8

11.2 分代内存设定

  1. 设定新生代+老年代空间的内存空间 新生代+老年代最小值(初始值):-Xmx<n>[g|m|k] 新生代+老年代最大值:-Xms<n>[g|m|k] 关注吞吐量和延迟的java程序应该把这两个值设定为一样。 因为这个内存空间无论是扩展还是缩减都是要Full GC的。

  2. 设定新生代的内存空间 -XX:NewSize=<n>[g|m|k] 这个可以设定新生代内存的初始值,也是最小值 其中是设定的大小,g,m,k指的是度量的单位 新生代空间的大小不会小于这个值 既然设定了最小值,那自然也要同时设定一下新生代的最大值-XX:MaxNewSize=<n>[g|m|k] 同理,新生代的空间也不会超过这个值

ps:只是说初始值和最大值在手动设定时应该同时指定,但不是强制要求

  1. 设定新生代的大小 最后,有一个东东,可以同时把新生代的初始值,最小值,最大值都设定为同一个值 -Xmn<n>[g|m|k] 新生代的大小会根据这个值进行设定

注意:如果-Xms(新生代和老年代的最大值)和-Xmx(新生代和老年代的最小值)

并没有设置为同一个值,使用-Xmn时,Java堆大小的变化并不会影响到新生代的大小。

所以,只有当-Xms和-Xmx设定为同一个值时,才使用-Xmn选项

不是很懂。。既然使用了-Xmn<n>来指定了新生代的大小,那么理论上,就不希望新生代的大小发生变化才对。

老年代的大小会根据新生代的大小来隐式设定。

老年代空间的初始值为:-Xmx - -XX:NewSize 即新生代+老年代的最小值-新生代的最小值

老年代空间的大小:如果-Xms和-Xmx设定为同一个值,并且使用了-Xmn,

或者-XX:NewSize-XX:MaxNewSize的值是一样的,则大小为:-Xmx或者-Xms的值减去-Xmn

大白话:如果新生代和老年代的最值都设定成一样,并且新生代的大小是固定的。

那么老年代的大小就是新生代+老年代的最值减去新生代的大小。

  1. 设定永久代的大小

    码出命令行:-XX:PermSize:<n>[g|m|k] 这个是永久代初始值和最小值

    码出命令行:-XX:MaxPermSize:<n>[g|m|k] 这个是永久代最大值

    关注性能的Java应用程序应当将永久代的最大值和最小值设定为同一个值,这样就不会因为要扩充永久代的大小而进行Full GC了。

小小的总结

  1. 如果没有显式的指定Java堆的大小,则Jvm会使用一种自动调优的功能自动设置这些Java堆的容量。

  2. 理论上:当HotSpot VM发现老年代的空间不足以存放提升的对象时u,就会发生Full GC

    实际上:如果HotSpot VM发现老年代的空间不足以存放下一次Minor GC提升的对象时,就会发生Full GC

    后者代价要比前者的代价轻,因为从失败的对象提升中恢复是一种很昂贵的操作

  3. 如果Full GC缘于老年代空间已满,则老年代和永久代都会进行GC

    如果Full GC缘与永久代已满,则老年代和永久代也都会进行GC

第十二章 工具

这章主要讲解的是关于性能优化的一些工具

12.1 GCHisto 工具

首先请出我们的一号选手:GCHisto,专攻文字方向

这货太古老了,而且已经停止维护,所以pass

12.2 JConsole

这是我们的二号种子选手JConsole

它是一个JMX兼容的GUI工具,可以连接运行中的jvm(jdk5或者更高版本)

注意:用java 5 jvm 启动java程序时,命令行只有添加-Dcom.sun.management.jmxremote

JConsole才连接然后监控这个jvm

虽然书上说Java6 或者更高版本的JVM不需要此属性,但是我在运行的时候,还是需要加这个参数的

当目标程序运行时,就可以在本机键入jconsole开启监控面板了

  1. 首先当然要连接到目标程序,我想都懂得怎么连接吧。。

  2. 连接上目标程序后,点开内存选项卡Menory可以监控JVM垃圾收集。

    左上方的图表可以切换监控的堆分代区,根据不同的垃圾收集器,分代名显示可能有所不同

    一般常见的就是

    PS Old Gen 看名字就是我们的老伙计:老年代

    PS Eden Gen 眼熟不,新生代的Eden区

    PS Survivor Space 这个就是新生代的Survivor区了

    Code cache HotSpot VM用该内存存储经过JIT编译器编译好的代码

    MetaSpace 元空间,存放类数据的

    Compressed Class Space 压缩类空间

    堆内存:Jconsole 把新生代和老年代合称为堆内存

    非堆内存:Code cache和MetaSpace (也许Compressed Class Space也算一个?)合称为非堆内存

    下方的详细信息显示了JVM内存的数据指标,包括

    Used(已使用):当前已使用的内存量

    Commited(已分配):保证可用jvm使用的内存量,这个内存量会随着时间发生变动,JVM会将内存释放回系统

    ​ 使已分配的内存少于启动时的初始量。

    ​ 已分配的内存总是等于或者大于内存的使用量

    Max(最大值):内存管理系统可用的最大内存存,如果内存使用量超过了该值,或者不超过该值,比如说

    ​ 虚拟内存低,也会导致内存分配失败

    GC Time:Stop the world 垃圾收集的累计时间和包括并发回收周期的垃圾收集调用的总数

    ​ 可能显示多行,一行显示一个JVM使用的垃圾收集器

    **需要注意的 **

    PS Survivor Space是否长时间都是满的,如果是,说明Survivor已经溢出,对象在未老化之前就已经提升到老年代

12.3 VisuaIVM 工具

特性:继承了上面的Jconsole,采用了NetBeans的插件架构。很容易添加组件或者插件

如何启动:如果你已经将JDK的安装目录设置为环境变量了,则只需要执行这个命令jvisualvm

​ 搞定

12.3.1 界面信息(以一开始启动为准)

左侧有Application面板,有四个可以展开的节点

  1. Local 会列出VisuaIVM可以监控的本地应用

  2. Remote 会列出VisuaIVM可以监控的远程主机,以及远程主机上面的应用

  3. VMCoredumps

  4. Snapshots 快照,VisuaIVM可以为java应用拍摄状态快照,这些快照会存储在这个节点中

    方便和其他快照进行比对

连接到java应用后,会打开一个新的面板

这个面板有什么东西要根据你的Java版本,本地还是远程,以及插件决定,但是一般来说,会有下面几个子选项窗口

  1. Overview 概述

    显示程序的概要信息

  2. Monitor 监控

    显示堆,永久代的使用情况,以及线程数和类加载信息

  3. Threads 线程

    显示应用程序有哪些线程正在运行,还用颜色标注了正在运行,休眠,堵塞,等待或者锁竞争

    在观察锁竞争时,这个窗口很有用

    另外,这个窗口还可以执行线程转储,很简单,点击线程dump按钮即可

    注意:生成的线程转储如果不保存的话,关闭VisuaIVM就会删除这个线程转储信息

    怎么保存,很简单,看见左侧的线程转储节点,右击它,有一个另存为,点亮它,就可以保存了。

    打开转储文件也超简单,只要在菜单的File-OpenFile 选择转储文件即可

12.3.2 如何VisuaIVM下监控远程主机

  1. 远程系统必须运行jstatd jdk5 or jdk6 分发版有这个 jre没有

    jstatd会启动Java RMI 服务器,监控Hotspot VM的停止,并为远程监控提供关联和接口

    且由于RMI服务器会暴露监控接口,所以必须设置安全策略文件和安全管理器。

    安全策略文件需要考虑授予的访问级别,避免对监控的jvm造成大影响

    且策略文件必须符合java安全策略规范

    策略文件示例如下

    grant codebase "file:${java.home}/../lib/tools.jar" {   
        permission java.security.AllPermission;
    };
    

    这个策略文件允许jstatd运行时不考虑任何异常

    假设上面的策略文件命名为jstatd.policy

    则运行以下命令使用该策略文件启动jstatd

    jstatd -J-Djava.security.policy=jstatd.policy

    启动完毕后,下一步

  2. 在本地系统运行jps+RemoteHostName,验证是否能关联到远程主机的jstatd

    这个因为jps如果加了主机名参数,就会试图链接远程系统的jstatd

    如果没加参数,就会返回本地能被监控的java应用

  3. 最后在VisuaIVM的Application面板上上找Remote,并add Remote 输入远程主机的主机名,就可以连接

12.3.3 如何在VisuaIVM对远程主机执行GC以及堆转储

首先,远程主机上面运行的java程序需要在启动时,追加这几个参数

-Dcom.sun.management.jmxremote.port=<port number>

-Dcom.sun.management.jmxremote.ssl=<true | false >

-Dcom.sun.management.jmxremote.authenticate=<true | false >

其次,在VisuaIVM的菜单File>add JMX Connection里面

输入主机名和端口,例如:localhost:8080

如果远程应用的authenticate设为true了,还需要在下面的勾选安全凭证,并输入用户名和口令

到了这一步,相信你已经打开了新的面板了,里面包含线程,内存的几个子选项窗口,可以执行GC和线程转储了,这里就不用再多说了吧

12.3.4 VisuaIVM的性能分析

VisuaIVM为远程应用和本地应用提供了性能分析,包括内存和cpu(远程进行不了内存分析)

但是注意在生产环境中慎用,因为它会加大应用程序的负担。

sample(抽样器)还可以生成堆转储,线程转储,这对不能进行内存分析的远程应用来说很有效

在抽样器运行中,还可以生成快照,快照里面可以显示所有线程的调用栈

  1. CPU分析

    其实这个应该叫做CPU抽样分析,只需要在抽样器里面点击CPU即可开始分析

    它能根据方法耗费的时间从大到小进行排序(当然反过来也行)

    还可以生成快照,快照的功能有很多,像前面所讲的,有显示所有线程的调用栈。

    还有显示热点(它是一个方法列表,按照方法消耗的自用时间进行排序)

    以及组合,同时显示调用栈和热点

12.3.5 VisuaIVM的VisualGC插件

VisuaIVM一开始并没有安装这个插件,所以你需要手动安装,其实也简单啦,菜单栏上面有一个工具>插件

点亮他,就可以安装VisualGC插件了。

这个插件的功能有:监控垃圾收集,类加载以及JIT编译

当你安装好这个插件时,在你监控Application面版中的应用时,右侧就会显示一个VisualGC的窗口页

根据监控应用所选择的垃圾收集器,VisualGC窗口页可以显示两到三个面板。

使用ThroughPut垃圾收集器,显示两个面板:Space(区域),Graphs(图表)。

使用Serial或者CMS收集器,会显示第三方面板(Histogram)直方图(显示存活对象以及年龄的数据)

为什么ThroughPut显示不出直方图,据作者所说ThroughPut使用其他机制维护Survivor区的对象。

所以不会维护Survivor区对象的年龄.

感觉怪怪的,ThroughPut收集对象的策略和Serial一样,只不过是并发的而已,为什么不会维护对象的年龄?

Histogram有两个子面板

  1. Parameters :显示当前Survivor的大小,以及对象提升到老年代的控制参数。

    每次Minor GC 过后,如果对象仍然存活,其年龄就会+1

    当年龄超过晋升阈值时,对象就会提升到老年代。

    晋升阈值每次Minor GC都会进行计算

    还有一个叫最大晋升阈值,这个是对象能存活在Survivor的最大年龄。

    不过,对象能晋升到老年代,是基于晋升阈值而非最大晋升阈值

    如果经常发生晋升阈值小于最大晋升阈值,则说明对象提升的速度过快。这通常是因为Survivor溢出.

    一旦溢出,Survivor中最老的对象会提升到老年代,直至Survivor的大小小于desired Survivor Size

    desired Survivor Size 即所需的Survivor大小,一般情况下,是Survivor容量的一半

  2. Histogram面板显示最近一次Minor GC之后活动的Survivor的对象的年龄的分布照

    如果监控的JVM是1.5 update6 的,则有16个相同尺寸的区域,每一个区域包含一种可能的对象年龄

    我觉得吧,应该每一个区域对于一个年龄吧。

    如果监控的JVM是老版本的的,则可能会有32个相同尺寸的区域。

    在应用运行中,你可以看到对象在各个年龄段上移动。

    如果晋升阈值小于最大晋升阈值,你可以看到比晋升阈值更大的区域没有使用,因为在这些区域的年龄的对象早已被提升到老年代了

12.1.3 JMC工具

这个工具书里面没提到,但是相当知名了,有空需要看下

12.2 Performance Analyzer

这个工具功能十分强大,其功能不仅可以分析java应用,还可以分析其他基于c,c++,fortran的应用。

作为Java的性能分析器,Performance Analyzer更擅长方法分析,Java Monitor 对象分析

在方法收集上,它可以统计消耗在系统态CPU,用户态CPU,以及锁竞争和其他事务上的时间。

在Performance Analyzer进行性能分析有几个步骤,以下讲解

  1. 运行Performance Analyzer工具的Collect命令

  2. 运行java应用,收集性能采样数据

    以上两步可以精简到一步完成 collect -j on java GCTest

    运行GCTest主程序并收集性能数据

  3. 通过Performance Analyzer的GUI或者命令行工具er_print进行性能分析,查看收集到的性能数据,分析其结果

    假设第二步生成的性能测试文件夹为test.2.er

    那么进行性能分析的话只需要运行analyzer test.2.er/即可

12.2 .1 支持平台

Performance Analyzer理论上,只要你运行的java虚拟机支持Java虚拟机工具接口,都可以用工具进行性能分析。放心,Hotspot官方的虚拟机,只要版本为java 5 update 4 及以上的版本,都支持这个接口。

但是操作系统的话,Windows就不支持了。

12.2.2 collect命令

collect命令执行后默认会在当前执行的文件夹生成一个test.1.er的文件夹。

test.1.er后面的数字会随着调用collect命令次数的增加而增加。

Collect命令行参数

  1. -o 可以指定创建的样本文件名

  2. -d 可以指定样本文件名存放的路径

  3. -p

    后面可以带的参数有

    lo 采样时间间隔增加到100毫秒

    hi 采样时间间隔减少到很小,即频繁采样

    数字 即就是设定采样的时间间隔。

    注:频繁的采样即采样的时间间隔越短,生成的采样文件就越大。

    不仅占用磁盘空间,分析工具读取样本文件的时间也会越长

    一般来说,默认值10毫秒采样一次的时间间隔就已经足够了

  4. -y 这个用法暂时TF

  5. -h 这是个高级选项,可以收集CPU的计数信息并将其和执行的应用代码进行关联。

    这个选项可以查看哪个方法进行了代价昂贵的操作。

    除非应用是计算性密集型应用,否则还是先尽量分析其他要素吧

12.2.3 查看性能数据

其实就是用Analyzer载入之前Collect命令生成的样本文件。

假设Collect命令生成的样本文件是test1.1er的话,

则使用Analyzer载入样本文件的命令如下

analyzer test.1.er/

执行该命令后,会打开一个GUI,首先显示的是概述.

函数部分

点击左侧的面板,可以跳到函数区域

这里会同时报告独占和包含用户态CPU的利用率(被我关掉了)

独占时间是指执行某方法的时间,不包含该方法调用其他方法的时间开销

包含时间是指执行某方法的时间,包含子调用方法的总和

查看右侧,有摘要(详细信息)

上侧工具栏按钮

如图所示

第一个是收集性能测试数据,其实就是Collect的GUI版

第二个是打开样本文件

其他都懂得

调用树

一般而言,对性能分析最好的入手点就是调用树

它以层级的方式展示了方法的调用关系,以及这些调用关系在应用程序上花费的时间。

可以让我们以 较高层次上了解应用程序在哪个用例上花费了较多的时间

第十三章:Java应用性能分析

回顾一下性能分析的定义?

嗯,就是一种在测试或者开发中,收集应用的一些性能数据的活动。

该过程会对应用的吞吐性和响应性造成一定的影响,因此不建议在生产环境使用。

本章将介绍内存分析(堆分析)和方法分析的基本概念

  1. 方法分析能提供在java应用中,java方法花费的时间信息
  2. 内存分析提供在java应用中,内存的使用信息,包括活跃对象,已分配对象的数目,以及大小,甚至分配对象时的栈追踪信息,也能提供

13. 1 性能分析术语

  1. 性能分析器:是一个工具,将应用程序运行的行为呈现给用户,既包括jvm内部的行为,也包括应用程序的行为
  2. 性能文件:存储性能分析器分析应用时产生的性能数据
  3. 开销:指性能分析器收集数据时花费的时间,不同于执行应用程序的时间

笔者的话,不写了,等待复制,写这些没用。。

第十四章:性能分析技巧

14.1 性能优化的几个方法

  1. 使用更高效的算法

  2. 减少锁的占用

  3. 为算法生成更有效率的代码

    就是说,编译器编译源代码后的机器码执行效率要高。

    衡量生成的机器码的效率的指标叫CPI指标(执行一条CPU指令所消耗的时钟数)

    让编译器生成更高效的代码,减少CPI,都有利于提升应用程序的性能

CPI和执行路径长度有一点细微的差别

执行路径长度与算法的优劣度相关,着眼于生成更短的CPU指令序列

CPI与编译器生成更高效的代码有光。着眼于执行每条CPU指令消耗更小的CPU时钟周期数

载入例子:在执行载入指令时,发生了CPU高速缓存未命中。CPU不得不去内存中读取自己想要的数据。

这样这个指令消耗的时钟周期数就会大大增加。

但是,如果我们在载入操作前,先来一个预读指令,把载入指令想读的数据预先load到缓存中。

这样载入指令就能直接从cpu缓存中拿数据,该指令消耗的时钟周期就会大大减少。

不过,由于新增了一个预读指令,导致程序执行路径长度,CPU指令数都增加了。

这是一个特殊的例子,执行路径变长,但是CPU时钟周期利用率更高

14.2 系统态CPU使用

之所以要监控系统态CPU,是因为一旦CPU时钟周期用于执行操作系统或者内核代码。

这期间的CPU就无法执行应用程序。

所以要尽量减少系统态CPU的使用,让CPU的时钟周期更多用于执行应用程序上。

不过,对于那些系统态或者内核态上消耗时间极少的应用程序,也就没必要去监控系统态CPU的使用情况了。

系统态CPU的数据也是性能分析要搜集的数据之一,通过Oracle Solaris Performance Analyze

可以查看采集到的系统态CPU的使用情况,具体是

。。。。。

14.3 锁竞争

在早期的JVM实现中,对Java monitor对象的操作通常会委托给操作系统的monitor对象。

这导致一旦发生锁竞争的时候,会触发系统调用,导致系统态CPU的使用率会很高。

现在的JVM,更多的时候,是执行用户态代码来对monitor对象进行操作。

除非万不得已,比如说锁竞争非常严重,才会发生系统调用,系统态CPU使用率就会攀升。

对于锁竞争的分析,Oracle Solaris Performance Analyze也非常有用。

。。。。

14.4 Volatile的使用

Volatile修饰的字段,能保证线程读取该字段的值,一定是上次线程写入的值。

因为不同线程中,共享同一个变量,是通过将变量的值拷贝到线程的工作内存中来实现的。

当写入的时候,会将变量的值写入到工作线程中,然后同步刷新到主内存中,通知其他缓存区,该变量的副本现在失效。

当读取的时候,会从工作线程的内存中读取,如果数据失效或是没有,就会从主线程中读取。

但是写入的时候,不一定能立刻就将该值写入到主内存中。

所以另一个线程可能会读到老的数据。

使用Volatile,强制每次读写时,都从主线程中拿或者写入。

但是无法保证原子性,比如说对Volatile修饰的字段进行自增;

自增过程有三个操作。

1.获取值

2.自增

3.写回。

所以不是原子操作(要么同时完成,要么同时不完成)。

使用volatile字段有一个性能缺点,就是它会限制现代JVM的JIT编译器对其进行优化。

因其volatile字段的值必须在应用程序的所有线程和CPU缓存中都必须保持同步。

为了做到这一点,在出现volatile字段的地方,都会加入一条cpu指令:内存屏障 membar or fence

一旦volatile字段值发生变化就会触发cpu缓存更新。

对于一个拥有多cpu缓存,性能要求很高的应用程序,频繁更新volatile可能会导致性能问题。

然而,并不是所有应用程序都会更新这个字段。所以这种性能问题很少见。

但总有一些例外发生。那怎么分析性能问题是不是因为volatile字段的频繁更新引起的呢?

又要请出我们的性能分析大佬Performance Analyzer软件了。

。。。。

14.5 调整数据结构的大小

在java应用程序中,我们经常使用StringBuffer或者SringBuild以及Collection类来存储数据。

无论是StringBuffer还是SringBuild,底层使用的数据结构都是char[].

随着新元素不断的加入到StringBuffer,底层的char[]大小也要随之调整。

怎么调整呢?就是把原有char[]里面东西搬运到一个更大容量的char[]里面,然后原有的char[]数据将会被丢弃。

由垃圾搜集器来处理。

Collection类也是类似的原理。

为什么要说这个呢?因为不恰当的数据结构的大小也有可能会引发性能问题。

14.5.1 StringBuilder或者StringBuffer大小的调整。

StringBuilder底层数组大小的调整也会引发性能问题。

这是因为进行底层数组扩充要产生大量垃圾char对象,而且还要执行额外的cpu指令。

要看到底是不是这货引起的问题。

可以进行堆性能分析。

如果发现活动对象的数量最多 且最常分配的对象是char[],就有必要跟踪一下char[]对象的分配栈跟踪。

继续栈跟踪下去,发现在有关底层数组扩充的地方,占用了很多的char[]对象。

//TODO 考虑截图

这时候,就可以考虑在使用StringBuffer的地方,在构造函数中,为其指定一个初始大小。

这样,只要之后添加的数据不要超过这个初始大小,就不会底层数组扩充了。

在java6最近优化中,它根据StringBuilder的使用情况,优化内部字节数组的大小。

14.5.2 Collection类大小的调整。

Collection类的某些子类的实现其底层也是依赖于数组的。

比如说,ArraysList,Vector,HashMap.

一旦元素的增长达到某个上限,对底层数组大小的扩充就很容易出现性能问题。

这个性能问题引发的原理跟StringBuffer是一样的,都是要消耗额外的CPU指令,并且分配较多的数组。

但是Collection类的底层数组扩充所导致的性能问题不仅仅只是如此。

扩充底层数组还会影响到这个数组字段的访问时间和解引用字段的时间。

且扩充数组后的新数组,可能会分配到另一块内存块。和Collection类的其他字段不在同一个同一块内存。

这样可能会导致CPU高速缓存未命中。

一般来说,对象及其字段通常会同时访问,所以将对象及其字段尽可能放在内存的同一块区域可以减少CPU高速缓存未命中。

总而言之,对Collection类大小调整的影响,已经不仅仅只是会增加CPU指令。而且还会对内存管理器造成影响。

查看Collection类的大小调整会不会对应用程序的性能造成影响,同样也可以使用xxxx

14.5.6 增加并行性

如果你的cpu是多核,多硬件线程的,那么增加并行性可以显著的改善应用程序的性能。

所谓增加并行性,就是要把原本单线程的工作,按照多线程的方式进行重构,以支持多线程。

但并不是所有单线程的程序都能重构成多线程。这里面要受到两个限制

  1. 程序逻辑的限制。比如说,有一个循环,并且循环内的大部分工作都和循环迭代无关。

    举个例子,对5000个对象循环update,update的过程并不受循环的影响。

    这种情况,就可以把单线程循环的工作切分到多个线程中执行。

  2. 使用多线程后,要保证线程安全,数据不会因为多个线程的写入而破坏掉。

有关单线程转成多线程,可以查看我的git仓库-JavaSe。

ps:在拥有更高的时钟周期,但是CPU核数相对较少的系统,增加并行性并不能看的很显著的效果。

大概也就提升个3,4秒这样子的。

虚拟处理器的数目越多,并行处理的能力就越强,性能提升潜力就越大。

14.5.7 其他分析技巧

有时候,尽管已经经过努力降低了系统态CPU的使用率,解决了锁竞争以及其他的性能问题。

但是无法满足服务级别的性能或者扩展性问题。

这个时候,对程序的逻辑和算法做一次分析有一个很有益的尝试。

可以使用Performance Analyzer的Call Tree 选项卡来查看应用程序的调用栈树。

由此解答一些比较抽象的问题。

比如说程序执行一次事务,用例,工作要花多长时间。

使用的算法和数据结构是否已经是最优的

注意:通过call tree来分析性能数据时,通常主要关注独占指标。

另外,还可以使用TimeLine时间线视图。

//TODO 具体怎么用。。。

用这个TimeLIne视图,可以寻找单线程阶段或者操作转换成多线程的机会。

第十五章:JVM性能调优入门

15.1 本章概述

本章将介绍jvm调优的基础知识,和调优的一般流程

15.2调优流程

  1. 确定系统需求。

    系统需求关注于系统的运行方面,比如说吞吐性,响应时间,内存消耗,启动时间,可用性,可管理性。

    与功能需求不一致,功能需求更多关注按什么方式运行,产生什么输出。

    为什么确定系统需求呢?

    因为性能调优涉及到方方面面的取舍,比如说,减少内存消耗通常会影响到系统的吞吐量和延迟性。

    了解哪些性能调优是重要的,哪些是不重要的,在jvm调优中,是至关重要的

  2. 选择jvm部署模式

    你是想把项目部署在单个jvm上面,还是多个jvm上面呢?

  3. 选择jvm的运行环境,jvm提供了多种运行环境选项可供使用。

    比如说JIT编译器,就有Client的,也有Server的,具体使用哪一种,就看你的需求啦

  4. 垃圾收集器调优的过程。

    通过优化垃圾收集,达到优化内存使用,吞吐性,延迟性的要求。。

    一般的流程是先调优到满足应用的内存使用需求,然后在是时间延迟的要求,最后是吞吐量的要求。

PS:调优是一个不断迭代的过程,一次调优不太可能就达到目的。所以要进行多次迭代的调优。

但是如果经过多次调优,其结果仍不能让人满意,这个时候,尝试改变JVM的部署模式吧。

15.3 介绍下常见的系统需求

15.3.4 可用性

可用性需求指的是,应用程序的一个组件挂了,整个应用程序或应用程序的一部分还能继续提供服务吗?

可能性是多少?

怎么提高可用性呢?可以利用应用程序组件化,在多个jvm中运行或者在多个jvm中运行多个应用程序的实例都可以提高可用性。但是提高可用性意味着要管理更多的jvm,由此会导致复杂性的增加以及管理成本的提升。

可用性需求的一个例子:应用程序的部署应该确保即使软件组件发生了不可意料的失效,也不会导致整个应用程序完全失效。

15.3.5 可管理性

可管理性指的是对应用程序进行监控,管理而产生的操作性开销。

同时也包含了配置应用程序的难易程度。

一般来说,使用jvm的数目越少,运行,监控应用程序的运营成本就越低。

以此同时,使用的jvm数越少,配置也就更简单。

然而提供可管理性会降低应用程序的可用性。

可管理性需求的一个典型例子是:由于人力资源有限,应用程序的部署要尽量减少jvm的使用数。

15.3.6 吞吐性

吞吐性是指单位时间内能处理多少工作量。

设计吞吐性需求时,一般是不考虑他对延迟性或者对响应时间的影响。

因为增加吞吐性的话,一般都会导致延迟的增加或者内存使用的增加

吞吐性需求的一个典型例子是:应用程序每秒要完成2500次事务。

//TODO 其实不太理解,延迟如果增加,意味着完成这项工作要花更多时间。

那么每秒还能完成2500次事务吗?

15.3.7 延迟及响应性。

延迟性,或者说响应性,是指应用程序收到请求开始执行到执行完毕所消耗时间的度量。

一般来说,设计延迟性或者响应性通常不考虑程序的吞吐性。因为通常情况下,提高应用程序的响应性会降低吞吐性或者更多的内存消耗。(两者可能同时发生)

延迟性或者响应性的需求举例:应用程序应在60毫秒内完成交易请求的处理工作。

15.3.8 内存占用

内存占用指的是在同等程序的可用性,可管理性,吞吐性,延迟性下,运行应用程序所需要消耗的内存大小。

内存占用一般用应用程序运行时需要的java堆大小或者总内存大小来表述。

增加java堆的容量来增大可用内存可以提高应用程序的吞吐量和响应性。

反之,减少可用内存的话,吞吐量和延迟性都会受到影响。

应用程序的内存占用限制了固定内存的机器上能同时运行的应用程序实例数。

需求举例:应用程序需要在8GB的内存上以单例运行。或者在24gb内存的机器上,以三个实例方式运行。

15.3.9 启动时间

很容易理解吧,就是应用程序初始化所需要的时间。

另外一个指的关注的指标是:现代jvm完成应用程序热区优化,初始化所需要的时间。

应用程序初始化的时间需要考虑到很多因素

  1. 初始化载入类的数量

  2. 初始化创建的对象数量,以及这些对象如何初始化

  3. Hotspot VM运行环境的选择,JIT编译器是Client模式还是Server模式

    一般来说,使用Client可以得到更短的启动时间,但是牺牲了生成更好的机器码的机会。

    使用Server模式启动时间会变长,但是能生成高度优化的机器码

需求例子:应用程序要求在15秒内启动完毕

15.4 用应用程序的系统需求进行分级

就是说,在应用程序干系人眼里,哪些系统需求是最重要的,哪些是次重要的。

有些时候,在调优过程中,你不得不对一些系统需求进行取舍。所以,着眼于最重要的系统需求。

可以让调优过程更有目标性。

比如说,boss说可管理性比可用性更重要。那你部署项目的时间,可能就只使用一个jvm去部署。

而且,明确哪些是最重要的系统需求,应该作为应用程序架构设计的一部分。

15.6 选择JVM的部署方式

JVM部署方式指的是把应用程序部署在单个的JVM实例上,还是多个JVM实例上。

系统需求的优先级列表,以及潜在的约束,决定了最适宜的部署方式。

潜在的约束是,举例来说的,你的应用程序用到了第三方的本地库,但是这个库没有64位版本的,它只能在32位系统上运行,那么你的程序也不得不只能部署在32位的系统上。 使用更小的java堆调优

15.6.1 单JVM部署模式

单个JVM部署的话,由于不需要管理太多的JVM,所以管理成本会减低。

且每个部署的JVM都会有相应的内存占用,使用单个JVM避免了这种占用。使应用程序所占用的总内存减少。

使用单JVM部署的挑战是,一旦应用程序发生灾难性的的故障,应用程序将不可用。所以,使用单JVM部署的应用通常会遇到单点故障问题。

15.6.2 多JVM部署方式

有两大好处

  1. 提高了可用性,如果一个组件发生故障无法使用,不会导致整个应用程序都无法使用。

  2. 更高吞吐量和更低延迟的可能性,因为在多JVM的部署模式下,每个JVM所占用的内存会较小。

    这样垃圾收集产生的停顿也会越小。

    而Java应用程序延迟性的一大因素就是垃圾收集,垃圾收集花费的时间少了,应用程序的延迟性就提高了。

    此外,多JVM部署模式下,可以把负荷分发到多个JVM实例上,从而处理更高的负荷,提高吞吐性。

    注意:使用多JVM时,要将不同的JVM绑定到不用的处理器集上,这样可以避免由于应用程序线程和jvm线程分别绑定到不同的CPU缓存而导致的跨硬件线程的迁移。这种迁移会加大CPU缓存未命中和抖动的几率。

使用多JVM部署方式的挑战是:监控,管理,维护多JVM的代价较大。

15.6.3通用建议

实际上并不存在最好的JVM部署方式,总是要根据系统需求来确定应用程序要使用什么样的部署方式。

此外,如果应用程序需要大量的内存运行,这大量的内存需求甚至超过了32位JVM的处理能力。

那么,你就不可避免的要使用64位的JVM.

使用64位JVM时要确认,应用程序使用的第三方模块是否都支持64位。

此外,如果应用程序使用了JNI本地接口,无论是第三方的软件组件。还是应用程序的某些组件,你都要确保用64位JVM来编译它们。

作者的经验是:使用JVM数越少越好。这样管理的成本会比较低。消耗的总内存也比较小。

15.7 选择JVM的运行模式

为java应用程序选择jvm的运行模式本质上是在做选择题,你需要判断并选择一种jvm运行模式以适合客户端类或者服务器类的应用程序。

15.7.1 Client模式和Server模式。

Hotspot提供了两种JVM运行模式,分别是Client和Server模式。

Client模式的特点在于启动速度快,占用内存小,JIT编译器生成的代码也足够快。

Server模式则提供了更复杂的机器码生成优化功能,这对Server应用程序非常重要。

大多数Server模式的JIT编译器都要消耗一定的时间来收集应用程序的行为信息,以便生成更高效的机器码

PS:其实还有第三组JVM的运行模式,叫TIered Server模式。结合了前两者的优点。

15.7.2 64位和32位JVM

到底要使用64位的JVM还是32的JVM,要根据应用程序运行时所需要占用的内存来考虑。

同时还要考虑应用程序的第三方组件是否支持64位系统

15.7.3 垃圾收集器

Hotspot VM 提供了Serial收集器,Throughput收集器,CMS收集器,G1收集器。

一般情况下,使用Throughput收集器就可以满足应用程序的停顿时间要求,即使确实有必要使用CMS收集器,那也是调优工作的晚期了。

15.8 垃圾收集调优基础

这一章要讲的是影响垃圾收集性能的三个属性,进行垃圾收集调优需要遵循的三个基本原则,以及垃圾收集调优需要采集的信息。

15.8.1 影响垃圾收集性能的三个属性

  1. 吞吐量:是评价垃圾收集器能力的重要指标之一。

    指不考虑因垃圾收集器引起的停顿,垃圾收集器能够带给应用程序什么样的性能提升

  2. 延迟:这个就是要考虑到垃圾收集器收集垃圾时引起的停顿时间。要尽量缩短这个时间,避免应用程序在运行时发生抖动。

  3. 内存占用,垃圾收集器流畅运行所需要的内存空间。

这其中任何一个属性的提高总是以牺牲其中一个属性,甚至是两个属性为代价。

不过,在应用程序中,极少情况会有这三个属性都很重要的情况。大多数情况都是要求一个属性的性能最优就行了。其余两个属性没那么重要。

当然,要明确那种属性是最重要的,这就要求你对应用程序的系统需求足够了解,能做出取舍出来。

15.8.2 垃圾收集的三个基本原则

  1. 每次MInorGC都尽可能收集更多的垃圾对象

  2. 处理吞吐性和延迟性问题时,垃圾收集器能使用的java堆内存越大,垃圾收集的效果就越好。

    应用程序运行起来也就越流畅。我们称之为GC内存最大化原则。

  3. 在三个属性(吞吐量,延迟,内存占用)中选择两个进行垃圾收集器的性能调优。我们称之为GC调优的3选2原则。

15.8.3 GC日志和命令行选项(安全点了解下)

这里不再多讲GC日志怎么看,以及相关命令行是什么,上面都有了。

这里讲点特殊的,就是安全点。

在针对高延迟进行调优时,有必要了解是不是由于应用程序进入安全点而导致的延迟性问题。

这需要两个命令行选项

-XX:+PrintGCApplicationConcurrentTime 可以报告应用在安全点操作之间的运行时间

-XX:+PrintGCApplicationStoppedTime 这个是堵塞java线程的时间

仔细观察GC日志,查看延迟是不是由于安全点引起

15.9 确定内存占用

这一步的工作主要是确定应用程序正常运行需要多少Java堆。

这可以通过采集应用程序的活跃数据计算,得出正常运行需要的Java堆大小。

确定内存占用后,如果不满足系统需求,可能就需要改动写系统需求,或者修改应用程序以满足系统需求。

活跃数据值的是,Java应用程序处在稳定态时,FUll GC 后,java堆的大小

15.9.1 约束

主要是看操作系统能给Java应用程序提供多少物理内存。

主要情况有两种

  1. 这个操作系统只有一个JVM,且只有一个应用程序在运行吗?

    如果是,该机器的所有物理内存都可以分配给JVM

  2. Java应用程序会部署到同一台机器上面的多个JVM实例上吗?

    或者该机器会有其他进程或者Java应用共享吗?

    如果是,你需要为每一个JVM实例和进程分配可用的物理内存

无论是哪种情况,都要为操作系统预留一部分内存。

15.9.2 堆大小调优着眼点

这一节,我们主要是通过调整Java的堆的大小,使之进入稳定态,用于下一节来计算活跃数据的大小。

首先要确定一下程序大概需要多少Java堆,如果不确定,可以有JVM动态调优

其中,我们使用的是Throughput垃圾收集器。

然后,就开始把GC日志加上,开始调优吧。。。

调优过程中,如果发现有OutOfMemorError错误,就要把程序停下来,查看导致内存溢出的是哪个分代。

有可能是老年代溢出Java heap space,那么下次运行就把老年代的空间加大一点。

有可能是永久代溢出PermGen space,下次运行就把新生代的空间加大一点。

直到不会产生内存溢出了,此时程序已经进入稳定态了,下一步开始统计应用程序中的活跃数据吧。。

15.9.3 计算活跃数据的大小

所谓计算活跃数据的大小,其实就是当应用程序处于稳定态时,计算应用程序的老年代+永久代的空间大小

这个活跃数据的计算,可以通过GC日志来查看。

主要收集

  1. 应用程序处于稳定时,老年代占用的Java堆大小
  2. 应用程序处于稳定时,永久代占用的Java堆大小

为了更好的度量活跃数据的大小,应该在多次Full GC后再去查看Java堆的占用情况,另外,要注意在Full GC发生时,程序正处于稳定态。

如果Full GC不经常发生,可以用第三方工具,发送命令给JVM,执行GC

15.9.4 初始堆空间大小配置

这一节将介绍如何根据活跃数据大小,来确定Java堆的初始大小。

建议多取几次Full gc数据,这样对java堆的大小的估算就会越准确。

根据活跃数据的大小来确定初始Java堆的大小时,还要考虑到Full GC的影响。

推荐的做法是基于最差延迟进行估算。

在初始堆空间大小配置中,有几个通用法则:

1: 将Java堆的初始值-Xms和最大值-Xmx设置为老年代活跃数据的3~4倍。

​ 假设说有一个应用,它在稳定态时测得老年代空间占用295MB。

​ 那么该应用程序的初始值建议为295*3=885 最大值建议为295*4=1180

2:永久代的初始值和最大值,应该比永久代活跃数据大1.2~1.5倍

​ 例如,有一个应用处于稳定态期间,发生Full GC后,永久代占用为32MB

​ 那么该应用的永久代初始值就可以规定为32*1.2=38最大值可以规定为32*1.5=48

3:新生代空间应该为老年代空间活跃数据的1~1.5倍

​ 例如说有一个应用,它在稳定态时测得老年代空间占用295MB。

​ 那么这个应用的新生代空间大小就可以建议为295~295*1.5=442

15.9.5 其他考量因素

要注意的是,GC日志的内存占用≠Java应用程序内存占用。

事实上,Java应用程序内存占用不仅仅只有Java堆的内存占用。

还包括线程栈的内存占用,如果线程栈的线程越多,那么消耗的内存也就越多

以及还有第三方库的分配内存占用。

为应用程序估算内存要把这些因素一一考虑在内。

了解Java应用程序总内存使用情况,可以使用操作系统提供的工具进行查看。

如windows的任务管理器,如Linux的top命令

还有。。。

确定内存占用这一过程中,可能最后得出来应用程序所需内存,达不到内存需求。

这时候,要么修改内存占用需求,要么调整应用程序,进行Java堆的分析。

在这一步计算出来的Java堆大小只是一个出发点,后期可能要根据不同的情况进行修改。

15.10 调整延迟,响应性

目标:成为神奇宝贝大师。。。开玩笑~~

目标:达到应用程序的延迟性需求。包括:

  1. 优化Java堆大小的配置
  2. 评估GC的持续时间和频率
  3. 是否需要切换垃圾收集器,以及切换之后的内存调优

结果

第一种结果:成功调优,延迟性需求满足了,进行下一步的调优

第二种结果:残念,延迟性达不到需求,改下需求?亦或是改下应用程序

​ 要改正应用程序的话,可以堆分析,减少对象分配和对象保持,改变JVM的部署方式,并减少GC的频率

15.10.1 数据收集

这一步需要收集一些数据,以用来优化工作。

收集的数据包括

1: 测量Minor GC的持续时间

2:测量Minor GC的频率

3:测量Full GC的最差(最长)延续事件

4:测量最差情况下,FUll GC的频率

这些数据中

1:Minor GC的数据决定了新生代的大小应该怎么优化

2:Full GC的数据决定了老年代的大小该如何优化,以及需不需要切换垃圾收集器

一旦决定需要切换垃圾收集器,也需要对切换后的垃圾收集器进行调优

收集到以上的数据之后,还需要知道应用的系统性需求,以比较应用的延迟性严不严重

这些系统性需求包括

1: 应用可接受的平均停滞时间(与Minor GC的停顿时间进行比对)

2:可接受的Minor GC 频率(与Minor GC发生的频率进行比对)

3:应用程序干系人可接受的应用程序最大停顿时间(与最差情况下的FULL GC 持续时间进行比对)

4:应用程序干系人可接受的最大停顿发生的频率(这个基本就是FULL GC的频率)

一旦这些数据收集清楚,就可以动手优化新生代和老年代了。

15.10.2 优化新生代的大小

根据前面所述,Minor GC发生的频率和持续时间可以用来确定新生代的大小。

Minor GC所需要的时间和新生代中可访问的对象大小呈正相关,可访问的对象越多,Minor GC就需要更多的时间

Minor GC发生的频率与新生代空间大小有关,相同的对象分配速率下,新生代空间越小,就越快被填满.

分析GC日志时

如果Minor GC的间隔时间越长,修正的方法时减少新生代空间大小

如果Minor GC经常发生(Minor GC频率高),要考虑增加新生代空间

什么,你问我怎么查看Minor的持续时间和频率,第十章的Minor GC数据详解有谈到

拿到应用处于稳定态的Minor GC的数据后,开始比对

比对1:Minor GC的频率比对系统需求的可接受的Minor GC 频率。

如果大于延迟性要求,则可以适当增加新生代的大小。之后再运行测试。

增加量的计算,假设当前新生代的大小设定为2048MB.

然后Minor GC大概是每2.147秒发生一次,那么近似可以认为,2.147秒填满了2048MB.

假设对象的分配速率是恒定的,那么每秒就增加了953.889 MB

5秒的话,就可以填满4700MB.

也就是说,Minor GC的频率要提升到5秒,则新生代的大小应该为4700MB

其实这么算不太可靠,书上好像也不是这么算的,大概推测下哈

TODO 有空请教高端人士

比对2:Minor GC的持续时间比对 应用可接受的平均停滞时间

如果大于延迟性要求,则需要减少新生代大小。

注意:调整新生代的大小时,要尽量保证老年代的大小不变

所以一般调整新生代大小要-Xms,-Xmx -Xmn 三管齐下

调整新生代大小时,要谨记几个基本原则

  1. 老年代大小不应该小于活跃数据大小的1.5倍

  2. 新生代空间的大小应该至少为Java堆大小的10%,通过-Xmx和-Xms可以设定java堆的大小。

  3. 增大Java堆大小时,注意不要超过JVM可用的物理内存数。

    当然,你要这么作死也行,JVM允许你这么作死。

    不过这样的话,一旦占用的内存超过了Java堆的大小,到时候就会动用到虚拟内存。

    到时候卡顿就很惨了。

最后,如果只考虑Minor GC引起的延迟,然后调整新生代大小无法满足延迟性和平均停顿时间要求。

就只能修改应用程序或者改下JVM的部署模式

如果通过以上调整达到了应用程序的延迟性需求,那么可以考虑下一步的优化

15.10.3 优化老年代的大小

这一节的目标的是评估FUll GC的最差停滞时间,和FUll GC 的频率

其中最差停滞时间,就是处于稳定态下,Full GC的持续时间

而Full GC的频率(FUll GC间隔的时间)

FUll GC 频率的计算公式为:老年代空闲空间/每秒老年代的增长大小(对象的提升率)

其中老年代的空闲空间的计算公式为:老年代大小-老年代活跃数据的大小

对象的提升率计算公式为:每次MinorGC老年代的增长大小/MinorGC的频率(几秒一次)

对象的提升率的含义就是每秒老年代增长多大

每次MinorGC老年代的增长大小计算公式:本次Minor GC的老年代占用-上一次Minor GC的老年代占用

其实关键也就是要计算每次Minor GC后,老年代的占用大小,然后相邻的两个值互减0就是增长率

OK,计算出来Full GC的频率了,但是达不到应用程序的最差Full GC频率的要求,怎么办

增大老年代的大小,这样可以帮忙降低Full GC的频率。

增大老年代的大小请注意保持新生代大小不变。

经过几次迭代迭代的老年代空间调整后,能满足应用程序的最差延迟性要求,JVM自身的调优就已经搞定了

但是万一达不到延迟性要求咋办?

骚年,要不试试CMS垃圾收集器,但是使用这个垃圾收集器也是需要调优的,下面就是对这个垃圾收集器进行调优。

15.10.4 为CMS调优

使用CMS垃圾收集器时,可以使垃圾收集器线程和应用程序线程实现最大的并行度,有效降低FUll GC的频率和持续时间。

CMS调优的几个难点

  1. 需要更细粒度的调优

  2. 对象从新生代提升到老年代的速率问题

  3. 并行老年代垃圾收集线程回收空间的速率

  4. CMS垃圾回收过程中产生老年代的空间碎片,如何解决

    为什么会产生碎片,可以参考上文

CMS调优的几个重点

  1. 对新生代需要更细致的调整,尤其是大小

  2. 何时启动老年代并发垃圾收集周期进行调整

CMS调优需要避免的

  1. 避免使用CMS过程中,老年代耗尽,引发Stop the world 的垃圾收集。

    通常,CMS垃圾收集器导致的Stop the world 的垃圾收集持续时间会更久,导致最差延迟时间会更长。

CMS调优的一个通用原则:将老年代空间增大20%~30% 以尽量避免昂贵的Stop the world。

小特点:从Throughput垃圾收集器转到CMS垃圾收集器,如果发生Minor GC,持续的时间可能会变长。

CMS碎片解决方案:

CMS为什么会产生垃圾碎片呢?因为并发清除阶段后,存活的数据之间会有空隙,这就形成了碎片

如何解决

  1. Stop the World垃圾收集。结论:想都不用想,虽然它能暂时解决碎片化问题,但是延迟太高了

  2. 为GC申请最大内存原则,内存大到足以避免由于堆内存碎片引起的Stop the World。至于碎片。。

    我内存都这么大了,还在乎那小小的碎片空间?

  3. 减少对象从新生代提升到老年代。即Minor GC回收原则。

    这个可以通过晋升阈值进行控制,下一章将讲解。。

15.10.5 Survivor空间介绍

Survivor之前新生代学过了,这里就不再多讲了。

根据上文所述,为了减少对象从新生代提升到老年代,就需要调整Survivor的大小。

让它有足够的空间,容纳存活对象比较长的时间,直到对象老化。

这样可以避免Survivor空间溢出所导致的过早提升。

那么,本章的重点就是调整Survivor的大小了。

首先介绍一个命令行

-XX:SurvivorRatio=<ratio>这个是单个Survivor空间和Eden空间的大小的比率。

如果记单个Survivor空间为X,Eden空间为Y,年轻代大小为Size;

则有以下式子

X/Y = 1/ratio;

2X+Y=Szie;

解得:(2+ratio)X=Szie

也就是说 X=Size/2+ratio

根据这个式子可以得知

减小ratio,会增大Survivor的大小,同时减少Eden空间的大小。

反之,增加ratio的大小,会减少Survivor的大小,同时增大Eden空间的大小

前提Size,也就是新生代的大小不变

另外,Eden空间越小,Minor GC的频率就越高,对象老化的速度就越快

15.10.6 解析晋升阈值

首先晋升其实意味着提升,也就意味对象提升到老年代空间。

晋升阈值就是对象年龄的底线,一旦对象的年龄超过了晋升阈值,就会提升到老年代。

而对象年龄又是什么,说白了就是对象老化前经历Minor GC的次数。

一开始创建对象时,对象的年龄是0,经过一次Minor GC后,对象的年龄+1。以此类推。

PS:如果你使用的是CMS垃圾收集器,或者使用-XX:UseParNewGC,那么新生代的垃圾收集器就是ParNew.

这个垃圾收集器每次Minor GC 都会重新计算一下晋升阈值。

好了,基本概念就讲到这。

几个概念和问题:

  1. 那么晋升阈值依据什么计算的呢?

    依据Minor GC后存活对象的大小和目标Survivor空间大小。

  2. 内部计算出来的晋升阈值不能,也不会超过最大晋升阈值

  3. 可以使用-XX:MaxTenuringThreshold=<n>来指定最大晋升阈值

    不建议将最大晋升阈值设定成0,这将导致刚刚分配的对象在下一次Minor GC后直接提示到老年代。

    引起老年代空间急速增长,造成频繁的Full GC

    也不建议把最大晋升阈值设置的超大,这会使对象长期的存在Survivor空间中,直至最后溢出。

    然后成功触发了过早提升,一些孙子辈的对象也被迁移到了老年代养老院了。严重影响对象老化机制的有效性。

  4. 如果Survivor空间占用的大小等于或者小于Hotspot VM 预期维护的值时(这是好事啊)。

    那么Hotspot VM 会将最大晋升阈值作为计算出来的晋升阈值(理想状态)

    反之,如果Survivor占用的空间大小大于Hotspot VM 预期维护的值时,

    那么,HotSpot会计算一个低于最大晋升阈值的晋升阈值来保证目标Survivor空间的占用。

    目标Survivor空间的占用是HotSpot在Minor GC后仍然维护的Survivor空间的占用

    通过命令行-XX:TargetSurvivorRatio=<percent>可以对该值进行调整。

    默认是50%,也就是占用一半的Survivor空间。

    一般不建议对这个参数进行调整,太大的值无法应对急速增长的Minor GC存活对象。

    会导致提升对象的时机比预期的更早。

    不过如果你的应用有一个稳定对象分配速率,可以适当提高占用率到80%左右

    PS:我有一个小疑问,占用50%的Survivor空间,不就意味这剩下的50%Survivor空间完全用不上了吗?

    还有,为什么增大Survivor空间占用率会导致更早的提升对象?

晋升阈值的意义

晋升阈值影响了对象从新生代提升到老年代的速率,是个比较重要的参数。

特别是对于CMS垃圾收集器而言,要尽量减少老年代的增长,降低碎片出现的可能性。

避免甚至杜绝昂贵的Stop the world 的垃圾收集。

15.10.7 监控晋升阈值

先甩出一行命令行-XX:+PrintTenuringDistribution这个可以监控对象的年龄分布。

其实吧,用VisuaIVM的VisualGC插件也是可以监控对象的年龄分布的。

以此确定最优的最大晋升阈值。

这个命令行的输出是酱紫的

Desired survivor size 2228224 bytes, new threshold 1 (max 6)
- age   1:    3743248 bytes,    3743248 total
- age   2:     499808 bytes,    4243056 total
- age   3:     206144 bytes,    4449200 total

解释:

第一行可以获知:晋升阈值为6,由max 6标识,通过new threshold 1可知VM内部计算出来的晋升阈值为1

2228224 bytes 是Survivor空间大小乘以目标存活率得到的空间大小,也是VM期望的Survivor的空间大小

目标存活率是HotSpot VM 预计目标空间在Survivor空间中占有的百分比。

第二行及下面的几行就是对象年龄的列表,每个年龄的对象及其占用的空间单独列为一行。

​ 在本例中中,对象年龄为1的占用空间为3743248字节。

​ 同时,在该行的末尾,还会显示对象总的大小,如第一行就是3743248

​ 第二行就是4243056

其实发现没,对象年龄总的大小是前一行的总大小+这一行年龄的对象的占用大小

注意了:通常情况下,如果观察到的晋升阈值持续小于最大晋升阈值,或者Survivor空间大小小于

总的存活对象大小(可以通过上面日志的最后一行的最右列看出来)

则说明Survivor过小,需要调整之。

15.10.8 调整Survivor空间的大小

在调整Survivor空间大小时,有一个重要原则:调整Survivor空间大小,应保持Eden空间不变

这是因为一旦Eden空间发生变化时,Minor GC 的频率也会发生改变。这不是我们所期待的

要在调整Survivor空间时,Eden空间保持不变,那么就要调整新生代的大小。

如何调整Survivor空间的大小?

调整Survivor空间大小,应该依据程序的稳定态输入的晋升分布日志,以最后一行的最后一列的总对象大小来调整。同时,如果你正在为CMS垃圾收集器的Survivor进行调优,应该认识到,CMS垃圾收集器默认情况下会使用约50%的目标Survivor空间。

以这个对象晋升分布图为例

Desired survivor size 8388608 bytes, new threshold 1 (max 15)
- age 1: 16690480 bytes, 16690480 total 

存活对象的总大小是16690480字节,那么Survivor空间大小应该设置为16690480*2=33380960,也就是32MB

因为CMS垃圾收集器默认情况下会使用约50%的目标Survivor空间,所以要*2

假设这个对象晋升分布图是根据这个命令行参数生成的

-Xmx1536m -Xms1536m -Xmn512m -XX:SurvivorRatio=30

根据上面的命令行参数,可以算出Eden:480MB Survivor:16MB(公式在上面,需要变换)

Survivor空间增大到32MB,那么每一个Survivor都要增长16MB,合计需要增长32MB。

保持Eden空间不变,则新生代需要为增长的Survivor买单,即增长到512+32=544MB

那么,就可以计算ratio的值了:32mb/480mb=1/15 也就是说ratio为15。

根据计算出来的结果,新的命令行参数是

-Xmx1568m -Xms1568m -Xmn544m -XX:SurvivorRatio=15

使用这个值可以更有效的老化存活对象,避免Survivor的溢出。

假设使用了这个新的命令行,产生的对象晋升分布如下

Desired survivor size 16777216 bytes, new threshold 15 (max 15)
- age 1: 6115072 bytes, 6115072 total
- age 2: 286672 bytes, 6401744 total
- age 3: 115704 bytes, 6517448 total
- age 4: 95932 bytes, 6613380 total
- age 5: 89465 bytes, 6702845 total 

可以看到,计算出来的晋升阈值已经是最大晋升阈值了

而且,最后一行最后一列的总对象大小6702845小于期望的Survivor空间大小16777216

也表明对象老化是有效的,没有Survivor溢出

通过以上调优,观察Minor GC的持续时间和频率是否满足要求。

如果持续时间过长,可能需要减少新生代的大小。

如果无法满足应用程序的Minor GC的延迟性和频率要求。可能需要回顾系统需求,或者整改程序。

如果运气不错,通过以上调整能达到应用程序的Minor GC的延迟性要求。可以进行下一步

15.10.9 初始化CMS收集周期

接下来我们要开始调优CMS收集器了,来减少最差情况的延迟并且最小化最差延迟发生的频率。

这一步的目标:保持老年代空间的恒定,避免发生Stop the world的垃圾收集

成功的CMS调优,是能以对象从新生代提升到老年代的同等速度,对老年代的对象进行垃圾收集。

达不到这个速度的,称为失速,失速的结果会发生Stop the world垃圾收集。

避免失速的关键是结合比较大的老年代的空间和足够快的初始化CMS垃圾收集周期。

原则:CMS周期的初始化基于老年代的内存占用。

一般而言,如果CMS周期启动的太慢,就会导致失速。如果CMS周期启动的太快,又会导致无用的消耗,影响应用吞吐性。但是,一般前者比后者的后果要恶劣的多,所以最好还是早启动CMS周期。

一般情况下,CMS垃圾收集器会根据老年代空间占用来决定何时启动CMS收集周期。但是,有时候它做的不是特别好,还是会导致Stop the world垃圾收集。

如果你在GC日志遇到了并发模式失败Concurrent mode failed

这种情况,我们就需要手动调整CMS垃圾收集周期了。

-XX:CMSInitiatingOccupancyFraction=<percent>

设定的值可以指定在老年代占用达到多少百分比时启用CMS垃圾收集周期

注意:你设定的值只在第一次CMS收集周期有效。后面的CMS收集周期又是自适应了

要想永久生效就要搭配这个命令行-XX:+UseCMSInitiatingOccupancyOnly

那么问题来了,percent百分比要怎么设定,随意设置吗?

不,这个百分比设定的空间占用值应该大于老年代空间和活跃数据大小之比

所谓老年代空间和活跃数据大小之比,说白了,就是活跃数据的占用百分比

如果这个百分比小于活跃数据占用的百分比,就会一直进行CMS收集周期,进入死循环。

因此,这个百分比设定的值,应该至少是活跃数据大小的1.5倍

举个例子,现在假设有一个应用程序的命令行参数是-Xmx1536m -Xms1536m -Xmn 512m

也就是说,堆的最大值和最小值为1536m,新生代大小为512m

可以推导出老年代空间大小为:1536-512=1024mb

假设应用程序的活跃数据是350MB,则根据上面的说法,CMS垃圾收集应该在空间百分比达到

350*1.2/1024=51%启动CMS垃圾收集周期。

于是,改造后的命令行是酱紫的

-Xmx1536m -Xms1536m -Xmn 512m
-XX:CMSInitiatingOccupancyFraction=51
-XX:+UseCMSInitiatingOccupancyOnly

至少是1.5倍,不过你也可以发挥自己的创造力,自己设定。

可以参考几个原则:

  1. 如果老年代空间消耗的慢,可以稍晚起点CMS周期
  2. 如果老年代空间消耗迅速,应该在较快的时间启动CMS周期,但是也不应该低于活跃数据占用的比例。
  3. 如有必要,增大老年代空间的大小

有几个错误示例演示了CMS周期启动的太早或者太晚的表现

第一种情况:CMS周期启动得太晚

[ParNew 742993K->648506K(773376K), 0.1688876 secs]
[ParNew 753466K->659042K(773376K), 0.1695921 secs]
[CMS-initial-mark 661142K(773376K), 0.0861029 secs]
[Full GC 645986K->234335K(655360K), 8.9112629 secs]
[ParNew 339295K->247490K(773376K), 0.0230993 secs]
[ParNew 352450K->259959K(773376K), 0.1933945 secs] 

可以发现在CMS-initial-mark初始化标记后,立马就进行了Full GC,缘与老年代空间满了。太晚的CMS周期跟不上老年代的增长速度

第二种情况:CMS周期启动的太早

[ParNew 390868K->296358K(773376K), 0.1882258 secs]
[CMS-initial-mark 298458K(773376K), 0.0847541 secs]
[ParNew 401318K->306863K(773376K), 0.1933159 secs]
[CMS-concurrent-mark: 0.787/0.981 secs]
[CMS-concurrent-preclean: 0.149/0.152 secs]
[CMS-concurrent-abortable-preclean: 0.105/0.183 secs]
[CMS-remark 374049K(773376K), 0.0353394 secs]
[ParNew 407285K->312829K(773376K), 0.1969370 secs]
[ParNew 405554K->311100K(773376K), 0.1922082 secs]
[ParNew 404913K->310361K(773376K), 0.1909849 secs]
[ParNew 406005K->311878K(773376K), 0.2012884 secs]
[CMS-concurrent-sweep: 2.179/2.963 secs]
[CMS-concurrent-reset: 0.010/0.010 secs]
[ParNew 387767K->292925K(773376K), 0.1843175 secs]

在CMS-remark到CMS-concurrent-sweep过程中,都是在进行并发清除,但是你发现没有

中间打印出来的Minor GC堆内存的占用情况,堆内存都没怎么大变化。

到最后并发清除阶段后,堆内存占用387767K,然后你再看看CMS-remark那会,堆占用374049K

可以发现,在中间的并发清除基本清除不了多少数据,反而还增加了。

CMS基本是做无用功的

15.10.10 显式的垃圾收集器

使用CMS时,如果你GC日志中观察到了System.gc()触发的Full GC,有两种解决方法

  1. 使用此命令行-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 让VM遇到System.gc().转成CMS并发垃圾收集

    如果你的JVM版本低于java6,则只能使用此命令行-XX:+ExplicitGCInvokesConcurrent

    理论上,使用这两个命令行都可以完成功能,但是建议使用第一个

  2. 可以使用这个命令行-XX:+DisableExplicitGC 忽视System.gc()

    慎用,有些显式垃圾收集是不得已为之的,禁用可能会导致影响性能。

    除非有明确的理由,否则不要轻易禁用它,调用它。

15.10.11 并发永久代垃圾收集

FUll GC也有可能源于永久代空间耗尽。

监控GC日志,查看Full GC时永久代的内存使用情况就可以判定了

如一下GC 日志

2010-12-16T17:14:32.533-0600: [Full GC
 [CMS: 95401K->287072K(1048576K), 0.5317934 secs]
 482111K->287072K(5190464K),
 [CMS Perm : 65534K->58281K(65536K)], 0.5319635 secs]
 [Times: user=0.53 sys=0.00, real=0.53 secs]

可以发现Full GC 打印出来的永久代占用空间65534K,很接近永久代大小65536K。

而且老年代占用95401K,离老年代空间大小104 8576K用尽还很晚。

综上所诉,本次Full GC的罪魁祸首是永久代空间耗尽

注:CMS垃圾收集周期默认不会收集永久代。当然我们可以开启

-XX:+CMSPermGenSweepingEnabled

如果你的java版本是java6以下的,那只能用这个命令行参数了-XX:+CMSClassUnloadingEnabled

另外,使用这个命令行可以指定在永久代空间占用达多少百分比时启用CMS垃圾收集

-XX:CMSInitiatingPermOccupancyFraction=

当然,这个百分比仅在第一次有效,要想永久生效,还要加这个命令行

-XX:+UseCMSInitiatingOccupancyOnly

15.10.12 调优CMS停顿时间

CMS有两个阶段是Stop the world阶段。

一个是初始标记阶段:CMS-initial-mark 一个是重新标记阶段CMS-remark

前者虽然单线程,然则极少花费较长时间。后者多线程,可以通过下面的命令行选项指定线程数

-XX:ParallelGCThreads= <n>

在Java 6 update 32开始,如果虚拟处理器个数小于8,也就是Runtime.availableProcessors() 小于8.

那么ParallelGCThreads默认会等于availableProcessors的值

你也可以手动设定,建议把该值设定的比默认值要低,否则由于大量垃圾收集线程运行,应用程序的性能会受到影响

能优化重新标记阶段的停顿时间的命令行选项还有

-XX:+CMSScavengeBeforeRemark

可以在重新标记阶段前,来一发Minor GC ,清除掉一些引用老年代空间的新生代对象,减轻重新标记工作量

重新标记标记的是存活对象,此举将老年代的一些存活对象,变成垃圾数据,所以可以减轻标记的工作量

还有一个命令行参数

-XX:+ParallelRefProcEnabled

这个参数适用于应用程序有大量的引用对象或者可终结对象需要处理,可以减少垃圾收集的可持续时间。

不过可终结对象是什么意思?听说这个参数有BUG,在jdk 6u25 和jdk 6u26 不要使用

此外,原理也不是很懂

15.10.13 结尾

做到这里,希望你程序的延迟性和已经达标,否则可以尝试再看下下面的黑科技。

要不然就只能再回顾下系统需求,对应用程序进行修改,可能还需要进行性能分析来定位问题域。

或者考虑JVM部署方式。

完成了这一步之后,可以看下面的吞吐性调优了

15.11 应用程序吞吐量调优

这是最后一步了,各位,努力哈

吞吐性调优的输入是应用程序的吞吐性要求,以及应用程序能使用的内存。

然后观测应用的吞吐性,如果应用的吞吐性已经达标,则整个调优过程可以完美结束,如果不能,则需要进行额外的JVM调优。

本章就是介绍JVM层面的吞吐性调优。这一节里,你可以使用Throughput垃圾收集器,也可以使用CMS垃圾收集器。下一节将介绍,如果使用CMS垃圾收集器,如何进行吞吐性的调优

15.11.1 CMS收集器吞吐性调优

为了使CMS获得更高的吞吐性性能,需要额外使用几个配置选项,这些配置选项和下面因素以及组合密切相关

  1. 使用15.13其他性能命令行选项的命令行参数
  2. 增加新生代的大小:这可以降低Minor GC的频率,从而减少一段时间内Minor GC的次数
  3. 增大老年代的大小:这样减少CMS垃圾收集周期的频率,从而减少碎片产生的可能性,进一步减少昂贵的full gc
  4. 按照之前的教程,对新生代进行调优,使新生代的对象更有效的老化,降低老年代的增速,减少CMS周期。。
  5. 适当延迟CMS垃圾收集周期,也是为了降低CMS垃圾收集的频率,但是小心不要发生并发模式失败哦

上面任何一个选项都可以减少垃圾收集消耗的CPU周期数,从而让更多的CPU周期用于执行应用程序。

对于提高吞吐量,又不想因为Stop the world导致延迟,前两个选项可以考虑一下

一个原则是,CMS包括Minor GC造成的开销应该小于10% ,通过调优,可以将该值降低到1%~3%

不过如果本来开销就在3%或者更少,那么调优工作就起不了多大作用了。

15.11.2 Throughput 收集器调优

Throughput调优的目标是尽量避免Full GC ,甚至是杜绝Full GC。

要达成这个目标,就要优化对象老化频率,通过调整Survivor的空间可以实现对象老化的优化。

也可以调整Eden区来降低Minor GC的频率,从而让对象的老化时间越长。

如果实在,对象的老化频率不理想,导致一些非长期存活对象提升到了老年代,可以适当增加老年代空间的大小,以应对这种情况。

Throughput垃圾收集器提供的吞吐性性能是诸多垃圾收集器中,效果最好的。

它默认提供了一个自适应功能,可以根据对象的存活率和对象的分配调整Eden区和Survivor区,以获得较好的对象老化频率。这对于大多数应用程序来说已经足够,但是,对于吞吐性要求苛刻的应用来说,可能就需要你手动调整新生代两个区的大小,这个时候,就需要禁用掉自适应调优

-XX:-UseAdaptiveSizePolicy

只有使用了Throughput垃圾收集器,这个命令行参数有效,其他收集器,这个命令行参数是无效的,空操作

再介绍一个命令行,这个命令行呢,可以打印出详细的Survivor空间占用日志,无论是Survivor溢出,还是新生代提升到老年代的信息,里面都有

-XX:+PrintAdaptiveSizePolicy

该命令行打印出来的GC日志大致如下

2010-12-16T21:44:11.444-0600:
 [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:
 survived: 224408984
 promoted: 10904856
 overflow: false
 [PSYoungGen: 6515579K->219149K(9437184K)]
 8946490K->2660709K(13631488K), 0.0725945 secs]
 [Times: user=0.56 sys=0.00, real=0.07 secs]

该日志由-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-UseAdaptive SizePolicy

+PrintAdaptiveSizePolicy 命令行参数友情赞助

解释:第三行的survived后面的数值就是Minor GC 后 TO Survivor的空间占用大小

​ 第四行的promoted 就是对象从新生代提升到老年代的大小

​ 第五行的 overflow: false就是Survivor现在是不溢出的

介绍了上面的两个命令行参数后,就要开始进行调优工作了,下面是调优的过程

  1. 在 GC日志中寻找稳定态情况下的Full GC日志。

  2. 观察稳定态下面的Full GC 日志,看是否有短期存在的对象被提升到了老年代。

    如果有,观察老年代的空间大小是否为活跃数据的1.5倍

    再次强调一点,活跃数据的大小是通过Full GC后老年代的空间占用确定的

  3. 确认了老年代空间可用的情况下,开始着手分析稳定态发生的Minor GC了

    首先请看Survivor是否溢出,如果溢出,需要对Survivor进行调优,这是下一节要讲的内容

15.11.3 Survivor空间调优

调整Survivor的空间的目标在于短期存活对象在提升到老年代对象之前,尽可能长时间的老化这些对象

通过寻找存活对象的最大值结合Survivor空间的占用,就可以得到一个最优的Survivor空间的大小。

所以,首先你要寻找Full GC之间 ,所有Minor GC中最大的存活对象的大小。

不幸的是,调整·Survivor的大小不是简单的把Survivor设置为某个值就OK了。

你还要保证Eden区不变。由此就要增加新生代的大小。同时增大新生代的大小不能改变老年代的大小。

所以,最终,你是要增大整个堆的大小的。

然后,接下来要计算老化最大存活对象所需要的最小Survivor空间:

用最大存活对象大小除以目标Survivor空间占用的百分比(TargetSurvivorRatio=)

例子:

假设有一个应用的命令行选项是

-Xmx13g -Xms13g -Xmn4g -XX:SurvivorRatio=6
-XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy
-XX:PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy

可知,Java堆的总大小是13g,新生代大小为4g,老年代大小为:13g-4g=9g

每个Survivor空间的大小为512MB(用之前的公式计算)

Eden区大小为3g

现在通过GC日志发现最大存活对象大小为:473MB

由于该应用程序没有设置TargetSurvivorRatio百分比,所以默认值为50%

根据以上理论,最小的Survivor空间大小应该设置为473*50% = 946MB

算出这个值后,接下来就是要制定新的命令行参数了

  1. SurvivorRatio应该设定为SurvivorSize/EdenSize = 946/3072mb(3g)~=0.3 近似取3

  2. 之前Survivor为512MB,现在增加为946MB,取1gb,增加量是1gb(两个Survivor呢),

    为了保持Eden区不变,新生代需增加4g+1g=5g,为了保证老年代不变,整个堆应增加1gb

    所以最后的结论是:-Xmx14g -Xms14g -Xmn5g -XX:SurvivorRatio=3

当这个配置让Survivor对象老化最有效时,就是吞吐量达到峰值的时刻。

可能你的内存不允许你增大堆内存,最终导致无法在维持Eden空间大小不变时增大Survivor空间的大小。

我们还有其他方案可以选择。

就是多次取稳定态时Minor GC的最大存活对象,计算它的最小值,最大值,中位值,平均值标准偏差。

这些计算可以帮助我们判断应用程序对象分配的速率是否稳定,或者对象分配是否有较大波动,如果没有

比如说最大值和最小值相差不大,标准差很小,可以尝试把目标Survivor占用的比例调大点。

本来是50%,调到60,70,甚至是90%.

对于内存限制的情况,这是一个可以考虑的选项。

15.11.4 调优并行垃圾收集线程

这个调优也可以优化应用的吞吐性。

之前写过,有一个命令行选项可以指定并行垃圾收集线程的线程数:-XX:ParallelGCThreads= <n>

可以将该值设置成默认值以下,以避免太多垃圾收集线程影响性能。

15.11.5 在NUMA系统上部署

如果应用在NUMA系统上部署

NUMA系统,即非一致性内存架构系统

有一个可以和Throughput垃圾收集器公用的命令行选项

-XX:+UseNUMA

仅JVM的部署跨CPU,不同CPU访问内存的拓扑有所不同,导致访问时间也有所差别才推荐使用这个命令行。

15.11.6 小结

如果你的调优已经到了这一步,仍然还是无法满足应用的吞吐性要求。那么你可以尝试一下15.13小节提到的其他性能命令行选项。

尝试之后还是不行,那只能回顾下系统需求,或者更改应用程序,或者改变JVM的部署方式。

15.12 极端示例

有些情况,上面介绍的调优手段是没多大作用的,本小节将探讨这种极端情况。

极端情况1:有些应用大对象的分配速率很高,但是长期存活的对象数目很少。

​ 那么,这种应用的新生代大小要比老年代大小要大

有一种说法,如果分配的对象太大,那么分配出来时不会进入新生代,而是直接进入老年代。

极端情况2: 有一些应用只有极少数的对象提升,那么这种应用的老年代大小不需要设置的比活跃数据大太多。

因为老年代的增长速度缓慢

极端情况3:有一些应用程序的延迟性要求很高,那么你可以使用很小的新生代空间和很大的老年代空间。

这样可以把Minor GC引起的延迟性降到很小,同时很大的老年代空间可以减小老年代的空间碎片。

不过如此Minor GC的频率会很快

15.13 其他性能命令行选项

这一节讲的是之前没说过的命令行选项,主要是一些通过优化JIT编译生成码,还有一些可以改善,优化Hotspot VM和应用程序员的性能。

15.13.1 实验性(最大,最近)优化

要启动这个优化。可以开启这个命令行-XX:+AggressiveOpts

这个命令行囊括了一些比较激进的性能优化方式,可能不太稳定。

但使用它可以获得一定的性能提升。

如果你的应用看重性能过于稳定,可以尝试使用这个命令行。

15.13.2 逃逸分析

逃逸:指的在某个线程创建的对象,在另一个线程可以被访问,那么可以说该对象逃逸了

逃逸分析:如果Java对象不发生逃逸,那么可以采用其他方法进行调优,这种调优分析就是逃逸分析

逃逸分析可以使用这个命令行选项开启

-XX:+DoEscapeAnalysis

这个命令行选项使用了以下的优化技术

以下优化技术的适用范围都是非逃逸对象,就是说,这个对象,只能在本线程访问,其他线程访问不了。

  1. 对象展开:可以在直接回收的空间上而不是Java堆中分配对象字段。

    比如说将对象的字段分配在栈中或者寄存器中,而不是Java堆

  2. 标量替换:可以把对象中,字段类型为简单类型的字段,直接在寄存器中分配。分配后,每次访问到这些字段时,不用通过解引用对象指针将字段载入到CPU寄存器中。而是直接从寄存器取

    该优化的作用是减少内存的访问次数

  3. 栈上分配:这是可以直接在线程的栈帧上分配对象,而不是Java堆中。

    因为非逃逸对象不会被其他线程访问,因此可以直接在线程的栈帧上分配。

    这个优化可以减少对象在Java堆分配数目,进而减少垃圾收集的频率

  4. 消除同步:如果线程持有了非逃逸对象上的锁,由于这个对象不可能被其他线程所访问到,所以这个锁几乎是没有作用的,因此JIT编译器可以优化掉这个锁

  5. 消除垃圾收集的读/写屏障:非逃逸对象由于不可能被其他线程访问到,也就是说这种对象只能在线程的根节点上访问,那么在其他对象存储这种对象的地址时,就不需要读写屏障,因为不涉及到线程安全问题。

    读写操作都是单线程的,自然也就不需要读写屏障了

15.13.3 偏向锁

这是一种偏向于最后获得对象锁的线程的优化技术。当只有一个线程锁定该对象,没有锁传统的情况下。其开销近似于Lock-Free。

通过-XX:+UseBiasedLocking可以开启此功能,在jdk需要手动开启,jdk6后默认就已经开启了偏向锁。

这个偏向锁优化在大多数应用上工作良好。

但是在某些情况,比如说当前获取锁的线程,并不能持有该锁到最后。

使用这个选项的效果就不好。

典型的例子是锁活动由工作线程池和工作线程所主导的应用程序。对于此类应用程序,建议取消偏向锁优化

-XX:-UseBiasedLocking

如果不能确定你的应用是否属于此类应用,可以分别收集开启优化和不开启优化,两种情况下的性能数据。

进行比较。

15.13.4 大页面支持

计算机内存被划分为这种有固定大小的块。

程序访问内存的过程会将虚拟内存的地址转换为物理内存地址,这个转换时通过页表来完成的。

为了减少每次内存访问页表的代价,通常使用一个快速缓存区,来缓存虚拟内存到物理内存的转换。

这个缓冲区就被称为转译快查缓存(TLB)

这个缓存当然比页面查询要快很多咯。

但是TLB只能容纳固定数量的条目,而条目只能映射到一定范围的内存地址区间。

如果这个内存地址区间不大,那么地址转译请求在TLB的失效的可能性就会比较大。

TLB失效:当一个地址转译请求无法在TLB找到对应的匹配项时,就称之为TLB失效

一旦TLB失效,就需要遍历内存的页面,这是一项昂贵的操作。

所以总而言之,我们要扩大条目的内存地址区间,避免TLB失效。

所幸,扩大条目的内存地址区间很简单,系统的页面越大,这个区间就越大。

如果使用了大页面的话,就可以降低TLB失效的可能性了。

不过,想使用大页面支持的话,还需要针对不同的操作系统,做出不同的配置

1.Solaris系统大页面支持

Solaris系统的大页面支持默认是开启的,如果你有强迫症的话,也可以使用-XX:+UseLargePages来进行配置

Solaris系统使用大页面无需对操作系统进行任何配置。

一些常见的处理器的页面大小

SPARC T-系列支持256MB的页面

Inter或者AMD系统上,支持的页面大小有4KB到2MB、4MB(通过页面大小扩展)

最新的AMD 43和Intel Xeon/Core系统页面大小可以达到1GB

使用Solaris的pagesize -a命令可以获得该平台支持的页面大小列表

使用这个命令行-XX:LargePageSizeInBytes=<n>[g|m|k]可以配置JVM使用固定的页面大小

如果底层系统没这么大的页面,则JVM会使用平台默认的页面大小

2.linux的大页面支持

linux要想获取大页面支持的话,除了使用-XX:+UseLargePages命令行外,还需要对操作系统进行配置

如果你没有对操作系统进行配置就直接使用了-XX:+UseLargePages,Hotspot还是接受这个命令,但是它会报告无法获得大页面,最后还是使用了底层系统默认支持的页面大小。

对linux操作系统进行配置的过程,请参阅linux文档,这里不讲

3. windows的大页面支持

巨硬的系统想使用大页面支持的话,需要修改安全设定:为运行该Java应用的用户将页面锁定在内存中。

这个设置可以通过组策略编辑器完成,操作步骤如下

  1. 运行gedit.msc

    经验证是gpedit.msc命令才是,书本写错?

  2. 在打开的窗口中,展开左边的

    计算机配置(compute Configuration)-->windows设置(Windows Setting)---> 安全设置(Security Setting)

    --->本地策略(Local Policy)--->用户权限分配(User Right Assignment)

  3. 在右边的面板点击锁定内存页(Lock page in menory)

  4. 然后在弹出的本地安全设置(Local Security Policy Setting)对话框点击Add(添加)按钮

  5. 在弹出来的选择用户或组Select Users or Groups对话框中添加用户的账户名

  6. OK,应用,退出,搞定

  7. 重启系统,然后愉快的使用-XX:+UseLargePages

第十六章:基准测试

什么是基测:就是为了测量计算机系统一个或者几个方面的性能而开发的程序 特别对于Java应用程序而言,基准测试是为了测量Java应用程序一个或几个方面的性能而特别开发的Java程序

测试的对象:可以是整个硬件或者软件栈,也可以是Java应用程序的某个小的方面 后者由于测试的的内容较狭窄或更专注。所以常常被称为微基准测试。 而基准测试更用于更宽泛的系统级别调优

16.1 基准测试的问题

  1. 注意区分基准测试和微基准测试。基准测试测试的是整个硬件或者软件栈,微基准测试的是应用的某个方面。

  2. 基准测试要考虑到预热阶段,且设计的预热阶段耗时要适当。

    预热阶段指的是JVM收集应用程序的数据,并根据数据进行适当动态优化。

    程序在基准测试时,可能JIT编译器正在将字节码转成机器码,所以消耗一定的CPU周期。

    这样基准测试采集回来的数据极有可能是不准确的。

    为了确保JIT编译器能对Java代码进行充分的优化,以避免产生额外的性能消耗。

    推荐的做法是在基准测试和微基准测试中包含预热阶段,并给与一定时间确保时JIT编译器到达稳定状态。

    保持预热阶段和实际采样阶段执行的代码路径一致是非常重要的

    推荐使用此命令行-XX:PrintCompilation,这是一个JIT编译器的日志。 每一条都是JIT编译器优化/逆优化的日志,当日志不在产生此类JIT编译器日志后,表示JIT编译器优化工作已经完成。此时才可以进行基准测试数据收集。

    接下里的Java程序介绍了基准测试中预热阶段的处理

        public static void main(String[] args) {
        	//预热测试数
            int warmUpCycles = 1000000;
            //基准测试数
            int testCycles = 50000000;
            SimpleExample se = new SimpleExample();
            System.err.println(“Warming up benchmark ...”);
            //先进行预热阶段
            long nanosPerIteration = se.runTest(warmupCycles);
            System.err.println(“Done warming up benchmark.”);
            System.err.println(“Entering measurement interval ...”);
            //正式测试
            nanosPerIteration = se.runTest(testCycles);
            System.err.println(“Measurement interval done.”);
            System.err.println(“Nanoseconds per iteration : +
                                nanosPerIteration);
        }
        /**
        * 运行基准测试的方法
        * @param iterations 测试的次数
        */
        private long runTest(int iterations) {
            long startTime = System.nanoTime();
            //要运行多少次基准测试
            for(int i=0;i<iterations;i++){
                //实际要基准测试的代码
            }
            long elapsedTime = System.nanoTime();
            return (elapsedTime – startTime)/iterations;
        }
    

    还有要注意一点的是,基准测试同样也要在不使用-XX:PrintCompilation情况下测试几遍。

    观察加与不加这个命令行参数,所得到的测试数据一致否。

    如果不一致,可能是创建基准测试或者微基准测试时受到了其他因素的影响。

第十七章:WEB应用的系统调优

本章的目的是:监控和调优web容器

在讨论Web容器的监控和调优之前,有些容器配置你需要先行设置好。这关乎容器的一些性能。

  1. 部署模式

    如Tomcat容器有两种模式,一种是开发模式,一种的生存模式。

    开发模式可以使容器里面的Jsp立即加载,但是损失了性能。

    生产模式更改jsp文件需要重新部署,这样虽然复杂,但是提高了性能。

  2. 安全管理器,有些容器可能有安全管理器,这样可以决定代码是否有权访问被保护的字段。

    当代码是不可信任时,才需要开启此安全管理器。

    开启安全器会降低应用的吞吐量

  3. 选择Client JIT编译器还是Server编译器,一般情况,选择Server编译器是理想之选,而这也是服务型机器JVM的默认选择。

  4. 选择什么垃圾收集器还是跟以前一样,吞吐量的选择Throughput,延迟性的选择CMS。

    不过在web应用中,绝大多数对象只在请求中得以保持,请求结束后,就变成了垃圾对象。

    所以新生代要注意设置好它的大小。

  5. 有些web容器里面嵌套了RMI服务器,来响应客户端的请求。 这些RMI服务器默认N秒会调用一次分布式垃圾收集,通过System.gc()调用。

    当然你可以调整一下,有两个命令行参数

    ``

·