JVM 调优和深入了解性能优化

  • JVM 调优的本质
  • GC 调优原则
  • 调优的原则
  • 目的
  • GC 调优
  • 调优步骤
  • 日志分析
  • 阅读 GC 日志
  • -XX:+UseSerialGC
  • -XX:+UseParNewGC
  • -XX:+UseConcMarkSweepGC
  • -XX:+UseG1GC
  • GC 调优实战
  • 项目启动 GC 优化
  • 项目运行 GC 优化
  • 推荐策略
  • 逃逸分析
  • 采用了逃逸分析--对象在栈上分配:
  • 没有逃逸分析---对象都在堆上分配(触发频次 GC,加重负担):
  • 常用的性能评价/测试指标
  • 响应时间
  • 并发数
  • 吞吐量
  • 关系
  • 常用的性能优化手段
  • 避免过早优化
  • 进行系统性能测试
  • 寻找系统瓶颈,分而治之,逐步优化
  • 前端优化常用手段
  • 浏览器/App
  • 减少请求数
  • 使用客户端缓冲
  • 启用压缩
  • 资源文件加载顺序
  • 减少 Cookie 传输
  • 友好的提示(非技术手段)
  • CDN 加速
  • 反向代理缓存
  • WEB 组件分离
  • 应用服务性能优化
  • 缓存
  • 缓存的基本原理和本质
  • 合理使用缓存的准则
  • 分布式缓存与一致性哈希
  • 以集群的方式提供缓存服务,有两种实现:
  • 一致性哈希:
  • 数据倾斜:
  • 集群
  • 异步
  • 同步和异步,阻塞和非阻塞
  • 常见异步的手段
  • 多线程
  • 消息队列
  • 程序
  • 代码级别
  • 选择合适的数据结构
  • 选择更优的算法
  • 编写更少的代码
  • 资源的复用
  • 单例模式
  • 池化技术
  • 存储性能优化
  • 结果集处理


JVM 调优的本质

并不是显著的提高系统性能,不是说你调了,性能就能提升几倍或者上十倍,JVM 调优,主要调的是稳定。如果你的系统出现了频繁的垃圾回收,这个 时候系统是不稳定的,所以需要我们来进行 JVM 调优,调整垃圾回收的频次。

GC 调优原则

调优的原则

  1. 大多数的 java 应用不需要 GC 调优
  2. 大部分需要 GC 调优的的,不是参数问题,是代码问题
  3. 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多;
  4. GC 调优是最后的手段

目的

  • GC 的时间够小
  • GC 的次数够少
  • 发生 FullGC 的周期足够的长,时间合理,最好是不发生。

注:如果满足下面的指标,则一般不需要进行 GC

  • MinorGC 执行时间不到 50ms;
  • MinorGC 执行不频繁,约 10 秒一次;
  • FullGC 执行时间不到 1s;
  • FullGC 执行频率不算频繁,不低于 10 分钟 1 次;

GC 调优

调优步骤

日志分析
  1. 监控 GC 的状态
    使用各种 JVM 工具,查看当前日志,分析当前 JVM 参数设置,并且分析当前堆内存快照和 gc 日志,根据实际的各区域内存划分和 GC 执行时间,觉得是 否进行优化;
  2. 分析结果,判断是否需要优化
    如果各项参数设置合理,系统没有超时日志出现,GC 频率不高,GC 耗时不高,那么没有必要进行 GC 优化;如果 GC 时间超过 1-3 秒,或者频繁 GC,则 必须优化;
  3. 调整 GC 类型和内存分配
    如果内存分配过大或过小,或者采用的 GC 收集器比较慢,则应该优先调整这些参数,并且先找 1 台或几台机器进行 beta,然后比较优化过的机器和没有 优化的机器的性能对比,并有针对性的做出最后选择;
  4. 不断的分析和调整
    通过不断的试验和试错,分析并找到最合适的参数
  5. 全面应用参数
    如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。
阅读 GC 日志

主要关注 MinorGC 和 FullGC 的回收效率(回收前大小和回收比较)、回收的时间。

-XX:+UseSerialGC

以参数-Xms5m-Xmx5m-XX:+PrintGCDetails-XX:+UseSerialGC 为例:

[DefNew:1855K->1855K(1856K),0.0000148secs][Tenured:2815K->4095K(4096K),0.0134819secs]4671K
DefNew 指明了收集器类型,而且说明了收集发生在新生代。

1855K->1855K(1856K)表示,回收前 新生代占用 1855K,回收后占用 1855K,新生代大小 1856K。

0.0000148secs 表明新生代回收耗时。

Tenured 表明收集发生在老年代

2815K->4095K(4096K),0.0134819secs:含义同新生代

最后的 4671K 指明堆的大小。

-XX:+UseParNewGC

收集器参数变为-XX:+UseParNewGC,日志变为: [ParNew:1856K->1856K(1856K),0.0000107secs][Tenured:2890K->4095K(4096K),0.0121148secs]

收集器参数变为-XX:+UseParallelGC 或 UseParallelOldGC,日志变为: [PSYoungGen:1024K->1022K(1536K)][ParOldGen:3783K->3782K(4096K)]4807K->4804K(5632K),

-XX:+UseConcMarkSweepGC

CMS 收集器和 G1 收集器会有明显的相关字样

-XX:+UseG1GC

GC 调优实战

项目启动 GC 优化

  1. 开启日志分析 -XX:+PrintGCDetails 发现有多次 GC 包括 FullGC
  2. 调整 Metadata 空间 -XX:MetaspaceSize=64m 减少 FullGC
  3. 减少 Minorgc 次数,增加参数 -Xms500m GC 减少至 4 次
  4. 减少 Minorgc 次数,调整参数 -Xms1000m GC 减少至 2 次
  5. 增加新生代比重,增加参数 -Xmn900mGC 减少至 1 次
  6. 加大新生代,调整参数 -Xms2000m -Xmn1800m 还是避免不了 GC,没有必要调整这么大,资源浪费

项目运行 GC 优化

使用 jmeter 同时访问三个接口,index、time、noblemetal
使用 40 个线程,循环 2500 次进行压力测试,观察并发的变化

jmeter 的聚合报告的参数解释:

java 优化占用内存过高 java jvm优化_后端

  1. 使用单线程 GC -XX:+UseSerialGC
  2. 使用多线程 GC-XX:+UseParNewGC

    多线程的吞吐量有一定的上升
  3. 使用 CMS-XX:+UseConcMarkSweepGC

    CMS 采用了并发收集,所以 STW 的时间较小,吞吐量较单线程有一定提高,最大请求时间 MAX 有明显的下降。
  4. 使用 G1-XX:+UseG1GC

    G1 这里的吞吐量是最大的,最大请求时间 MAX 有明显的下降。
    一行代码导致频繁 GC,吞吐量下降很快

推荐策略

  1. 新生代大小选择
  • 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,新生代收集发生的频率也是最小 的.同时,减少到达老年代的对象.
  • 吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8CPU 以上的应用.
  • 避免设置过小.当新生代设置过小时会导致:
    1.MinorGC 次数更加频繁
    2.可能导致 MinorGC 对象直接进入老年代,如果此时老年代满了,会触 发 FullGC.
  1. 老年代大小选择
  • 响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可 以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;
  • 如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
  • 并发垃圾收集信息、持久代并发收集次数、传统 GC 信息、花在新生代和老年代回收上的时间比例。

吞吐量优先的应用:
一般吞吐量优先的应用都有一个很大的新生代和一个较小的老年代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而 老年代尽存放长期存活对象

GC 调优是个很复杂、很细致的过程,要根据实际情况调整,不同的机器、不同的应用、不同的性能要求调优的手段都是不同的,小编也无法告诉大 家全部,即使是 jvm
参数也是如此,比如说性能有关的操作系统工具,和操作系统本身相关的所谓大页机制,都需要大家平时去积累,去观察,去实践,小编在这个专题上告诉大家的除了各种 java 虚拟机基础知识和内部原理,也告诉大家一个性能优化的一个基本思路和着手的方向。

逃逸分析

是 JVM 所做的最激进的优化,最好不要调整相关的参数。
 牵涉到的 JVM 参数
-XX:+DoEscapeAnalysis:启用逃逸分析(默认打开)
-XX:+EliminateAllocations:标量替换(默认打开)
-XX:+UseTLAB 本地线程分配缓冲(默认打开)

如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大
的性能提高。

采用了逃逸分析–对象在栈上分配:

java 优化占用内存过高 java jvm优化_java 优化占用内存过高_02

没有逃逸分析—对象都在堆上分配(触发频次 GC,加重负担):

java 优化占用内存过高 java jvm优化_后端_03

常用的性能评价/测试指标

一个 web 应用不是一个孤立的个体,它是一个系统的部分,系统中的每一部分都会影响整个系统的性能

响应时间

提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间。

常用操作的响应时间列表:

操作

响应时间

打开一个站点

几秒

数据库查询一条记录(有索引)

十几毫秒

机械磁盘一次寻址定位

4 毫秒

从机械磁盘顺序读取 1M 数据

2 毫秒

从 SSD 磁盘顺序读取 1M 数据

0.3 毫秒

从远程分布式换成 Redis 读取一个数据

0.5 毫秒

从内存读取 1M 数据

十几微秒

Java 程序本地方法调用

几微妙

网络传输 2Kb 数据

1 微妙

并发数

同一时刻,对服务器有实际交互的请求数。
和网站在线用户数的关联:1000 个同时在线用户数,可以估计并发数在 5%到 15%之间,也就是同时并发数在 50~150 之间。

吞吐量

对单位时间内完成的工作量(请求)的量度

关系

系统吞吐量和系统并发数以及响应时间的关系:

理解为高速公路的通行状况:
吞吐量是每天通过收费站的车辆数目(可以换算成收费站收取的高速费),
并发数是高速公路上的正在行驶的车辆数目,
响应时间是车速。 车辆很少时,车速很快。但是收到的高速费也相应较少;随着高速公路上车辆数目的增多,车速略受影响,但是收到的高速费增加很快;
随着车辆的继续增加,车速变得越来越慢,高速公路越来越堵,收费不增反降;
如果车流量继续增加,超过某个极限后,任务偶然因素都会导致高速全部瘫痪,车走不动,当然后也收不着,而高速公路成了停车场(资源耗尽)。

常用的性能优化手段

避免过早优化

不应该把大量的时间耗费在小的性能改进上,过早考虑优化是所有噩梦的根源。
所以,我们应该编写清晰,直接,易读和易理解的代码,真正的优化应该留到以后,等到性能分析表明优化措施有巨大的收益时再进行。 但是过早优化,不表示我们就可以随便写代码,还是需要注重编写高效优雅的代码。

进行系统性能测试

所有的性能调优,都有应该建立在性能测试的基础上,直觉很重要,但是要用数据说话,可以推测,但是要通过测试求证。

寻找系统瓶颈,分而治之,逐步优化

性能测试后,对整个请求经历的各个环节进行分析,排查出现性能瓶颈的地方,定位问题,分析影响性能的的主要因素是什么?内存、磁盘 IO、网络、 CPU,还是代码问题?架构设计不足?或者确实是系统资源不足?

前端优化常用手段

浏览器/App
减少请求数
  • 合并 CSS,Js,图片,
  • 生产服务器提供的 all 的 js 文件
  • http 中的 keep-alive(http1.1 中默认开启)包括 nginx
使用客户端缓冲

静态资源文件(css、图标等)缓存在浏览器中,有关的属性 Cache-Control(相对时间)和 Expires 如果文件发生了变化,需要更新,则通过改变文件名来解决。

启用压缩

浏览器(zip),压缩率 80%以上。
减少网络传输量,但会给浏览器和服务器带来性能的压力,需要权衡使用。

资源文件加载顺序
  • css 放在页面最上面,js 放在最下面。这样页面的体验才会比较好。
  • 浏览器会加载完 CSS 才会对页面进行渲染
  • JS 只要加载后就会立刻执行。(有些 JS 可能执行时间比较长)
减少 Cookie 传输

cookie 包含在每次的请求和响应中,因此哪些数据写入 cookie 需要慎重考虑(静态资源不需要放入 cookie)

友好的提示(非技术手段)

有时候在前端给用户一个提示,就能收到良好的效果。毕竟用户需要的是不要不理他。

CDN 加速

CDN,又称内容分发网络,本质是一个缓存,而且是将数据缓存在用户最近的地方。无法自行实现 CDN 的时候,可以根据经济实力考虑商用 CDN 服务。

反向代理缓存

将静态资源文件缓存在反向代理服务器上,一般是 Nginx。

WEB 组件分离

将 js,css 和图片文件放在不同的域名下。可以提高浏览器在下载 web 组件的并发数。因为浏览器在下载同一个域名的的数据存在并发数限制。

应用服务性能优化
缓存
  • 网站性能优化第一定律:优先考虑使用缓存优化性能
  • 优先原则:缓存离用户越近越好
缓存的基本原理和本质

缓存是将数据存在访问速度较高的介质中。可以减少数据访问的时间,同时避免重复计算。

合理使用缓存的准则
  • 频繁修改的数据,尽量不要缓存,读写比 2:1 以上才有缓存的价值。
  • 缓存一定是热点数据。
  • 应用需要容忍一定时间的数据不一致。
  • 缓存可用性问题,一般通过热备或者集群来解决。
分布式缓存与一致性哈希
以集群的方式提供缓存服务,有两种实现:
  1. 需要更新同步的分布式缓存,所有的服务器保存相同的缓存数据,带来的问题就是,缓存的数据量受限制,其次,数据要在所有的机器上同步,代价 很大。
  2. 每台机器只缓存一部分数据,然后通过一定的算法选择缓存服务器。常见的余数 hash 算法存在当有服务器上下线的时候,大量缓存数据重建的问题。 所以提出了一致性哈希算法。
一致性哈希:
  • 首先求出服务器(节点)的哈希值,并将其配置到 0~2 的 32 次方的圆(continuum)上。
  • 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
  • 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过 232 仍然找不到服务器,就会保存到第一台服务器上。

一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。

数据倾斜:

一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题,此时必然造成大量数据集中到 NodeA 上,而只有极少量会定位到 Node B 上。为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点, 称为虚拟节点。具体做法可以在服务器 ip 或主机名的后面增加编号来实现。例如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “NodeA#1”、 “NodeA#2”、“NodeA#3”、“NodeB#1”、“NodeB#2”、“NodeB#3”的哈希值,于是形成六个虚拟节点:同时数据定位算法不变,只是多了一步虚拟节点到 实际节点的映射,例如定位到“NodeA#1”、“NodeA#2”、“NodeA#3”三个虚拟节点的数据均定位到 NodeA 上。这样就解决了服务节点少时数据倾斜的问 题。在实际应用中,通常将虚拟节点数设置为 32 甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

集群

可以很好的将用户的请求分配到多个机器处理,对总体性能有很大的提升

异步
同步和异步,阻塞和非阻塞
  • 同步和异步关注的是结果消息的通信机制
  • 同步:同步的意思就是调用方需要主动等待结果的返回
  • 异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。
  • 阻塞和非阻塞主要关注的是等待结果返回调用方的状态
  • 阻塞:是指结果返回之前,当前线程被挂起,不做任何事
  • 非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。
  1. 同步阻塞:同步阻塞基本也是编程中最常见的模型,打个比方你去商店买衣服,你去了之后发现衣服卖完了,那你就在店里面一直等,期间不做任何事(包
    括看手机),等着商家进货,直到有货为止,这个效率很低。jdk 里的 BIO 就属于 同步阻塞
  2. 同步非阻塞:同步非阻塞在编程中可以抽象为一个轮询模式,你去了商店之后,发现衣服卖完了,这个时候不需要傻傻的等着,你可以去其他地方比如 奶茶店,买杯水,但是你还是需要时不时的去商店问老板新衣服到了吗。jdk 里的 NIO 就属于 同步非阻塞
  3. 异步阻塞:异步阻塞这个编程里面用的较少,有点类似你写了个线程池,submit 然后马上 future.get(),这样线程其实还是挂起的。有点像你去商店买衣服, 这个时候发现衣服没有了,这个时候你就给老板留给电话,说衣服到了就给我打电话,然后你就守着这个电话,一直等着他响什么事也不做。这样感觉 的确有点傻,所以这个模式用得比较少。
  4. 异步非阻塞:好比你去商店买衣服,衣服没了,你只需要给老板说这是我的电话,衣服到了就打。然后你就随心所欲的去玩,也不用操心衣服什么时候 到,衣服一到,电话一响就可以去买衣服了。jdk 里的 AIO 就属于异步
常见异步的手段

Servlet 异步
servlet3 中才有,支持的 web 容器在 tomcat7 和 jetty8 以后。

多线程
消息队列
程序
代码级别

一个应用的性能归根结底取决于代码是如何编写的

选择合适的数据结构

java 优化占用内存过高 java jvm优化_调优_04


选择 ArrayList 和 LinkedList 对我们的程序性能影响很大,为什么?因为 ArrayList 内部是数组实现,存在着不停的扩容和数据复制。

选择更优的算法

举个例子,如何判断一个数是否为 n 的多少次方

/*
 *类 说 明 : 选 择 更 优 的 算 法
 */
public class BetterAlg {
    // 如 何 判 断 一 个 数 是 否 为 n的 多 少 次 方
    public static void main(String[] args) throws Exception {
        int n = 2;
        Scannerscanner = newScanner(System.in);
        while (scanner.hasNext()) {
            // 控 制 台 输 入
            int input = scanner.nextInt();
            while (true) {
                if (input == n) {
                    System.out.println("是(" + n + ")的次方");
                    break;
                }
                if (input % 2 != 0) {
                    System.out.println("不是(" + n + ")的次方");
                    break;
                } else {
                    input = input / 2;
                }
            }
// if((input&(input-1))==0){ 
// System.out.println(" 是 ("+n+") 的 次 方 ");
// }else{ 
// System.out.println(" 不 是 ("+n+") 的 次 方 "); 
// } 
        }
    }
}

java 优化占用内存过高 java jvm优化_后端_05

编写更少的代码

同样正确的程序,小程序比大程序要快,这点无关乎编程语言。

资源的复用

目的是减少开销很大的系统资源的创建和销毁,比如数据库连接,网络通信连接,线程资源等等。

单例模式

Spring 中的 bean

池化技术
存储性能优化
  • 尽量使用 SSD
  • 定时清理数据或者按数据的性质分开存放
结果集处理

用 setFetchSize 控制 jdbc 每次从数据库中返回多少数据。