什么是系统调优?
- 吞吐与延迟 : 有些结论是反直觉的,指导我们关注什么
- 没有量化就没有改进:监控与度量指标,指导我们怎么去入手
- 80/20原则:先优化性能瓶颈问题,指导我们如何去优化
- 过早的优化是万恶之源:指导我们要选择优化的时机
- 脱离场景谈性能都是耍流氓:指导我们对性能要求要符合实际
业务系统特性
分类:计算密集型、数据密集型
计算密集型:逻辑判断多,cpu占用高
数据密集型:IO频繁,网络传输多,
业务系统本身是无状态的,其最终状态需要持久化到数据库,一般来说DB操作最耗时间,占比最大
性能优化一般要存在瓶颈问题,而瓶颈问题都遵循80/20原则。既我们把所有的整个处理过程中比较慢的因素都列一个清单,并按照对性能的影响排序,那么前20%的瓶颈问题,至少会对性能的影响占到80%比重。换句话说,我们优先解决了最重要的几个 问题,那么性能就能好一大半。
量化指标
- 可用性:一个业务系统,三十天有中有某一天150分钟不可用和三十天中每天5分钟不可用,那个可用性高?
- 延时性:一次请求消耗多少时间?如何降低?有什么手段?代价是什么?
- 吞吐量: 一般对于交易类的系统我们使用每秒处理的事务数(TPS) 来衡量吞吐能力,对于查询搜索类的系统我们也可以使用每秒处理的请求数 (QPS)。相对而言,同时能提供多少服务?AIO,BIO,NIO?
低延时、高可用,高吞吐如何选择?能做到全覆盖吗?不能!怎么办?
最大暂停时间: max(GCi)
堆使用效率:HEAP_SIZE / Heap(GC)
“调优”是一个诊断和处理手段,我们最终的目标是让系统的处理能力,也就是“性能”达到最优化。
在计算机中大部分关于新能的方案都是在解决各个环节的处理速度不一致的问题。
优化时机
过早的优化是万恶之源 ,我们需要考虑在恰当的时机去优化系统。在业务发展的早期,量不大,性能没那么重要。我们做一个新系统,先考虑整体设计是不是OK,功能实现是不是OK,然后基本的功能都做得差不多的时候(当然整体的框架是不是满足性能基准,可能需要在做项目的准备阶段就通过POC(概念证明)阶段验证。),最后再考虑性能的优化工作。因为如果一开始就考虑优化,就可能要想太多导致过度设计了。而且主体框架和功能完成之前,可能会有比较大的改动,一旦提前做了优化,可能这些改动导致原来的优化都失效了,又要重新优化,多做了很多无用功。
优化步骤
第一步:制定指标,收集数据。
第二步:分析问题,找到瓶颈。找瓶颈,然后分析解决瓶颈问题,通过这些手段,找当前的性能极限值。压测调优到不能再优化了的 TPS和QPS, 就是极限值。
第三步:制定方案,调整配置:知道了极限值,我们就可以按业务发展测算流量和系统压力,以此做容量规划,准备机器资源和预期的扩容计划。
第四步:逐步改进,持续观察。最后在系统的日常运行过程中,持续观察,逐步重做和调整以上步骤,长期改善改进系统性能。
优化的方法
业务优化:需求与技术能力权衡、重要性分析、优先核心业务优化,非核心业务降级处理
技术优化:
- 架构优化:服务拆分,分布式高可用,业务相关性高
- 业务系统优化:代码优化:时间、空间,降低冗长调用链。JVM优化:减少GC的频率和Full GC的次数,提高可用性。
- 数据库优化:SQL优化、非必要的数据不必查出来,直接在数据库层面过滤,减少IO与ORM的序列化反序列化
JVM调优
JVM内存模型简介
jvm需要哪些内存
- 堆:老年代、年轻代(新生带,交换区X2)
- 栈:线程执行压入字节指令
- 非堆:元数据
- JVM自身:JVM自身就是个进程,本身需要run起来
使用字节码演示
public class Main {
public static void main(String[] args) {
String a = "a" + "b";
String b = new String("ab");
String c = a + b;
}
}
这段代码时如何运行的?
0 ldc #7 <ab> # 编译优化:字符串在常量池,我们去找找
2 astore_1
3 new #9 <java/lang/String> # new指令:在堆中创建对象
6 dup
7 ldc #7 <ab>
9 invokespecial #11 <java/lang/String.<init>>
12 astore_2
13 new #14 <java/lang/StringBuilder> # 编译优化:java不支持运算符重栽,字符串+是语法糖
16 dup
17 invokespecial #16 <java/lang/StringBuilder.<init>>
20 aload_1
21 invokevirtual #17 <java/lang/StringBuilder.append>
24 aload_2
25 invokevirtual #17 <java/lang/StringBuilder.append>
28 invokevirtual #21 <java/lang/StringBuilder.toString>
31 astore_3
32 return
### 字节码解释执行,线程栈,js?
字节码对照
12 07 // ldc #7
4c // astore_1
bb00 09 // new #9
59 // dup
1207 // ldc #7
b700 0b // invokespecial #11
4d // astore_2
对照表:www.cnblogs.com/tsvico/p/12…
cafe babe 0000 0034 002b 0a00 0200 0307
0004 0c00 0500 0601 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 063c 696e
6974 3e01 0003 2829 5608 0008 0100 0261
6207 000a 0100 106a 6176 612f 6c61 6e67
2f53 7472 696e 670a 0009 000c 0c00 0500
0d01 0015 284c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b29 5607 000f 0100 176a
6176 612f 6c61 6e67 2f53 7472 696e 6742
7569 6c64 6572 0a00 0e00 030a 000e 0012
0c00 1300 1401 0006 6170 7065 6e64 0100
2d28 4c6a 6176 612f 6c61 6e67 2f53 7472
696e 673b 294c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 4275 696c 6465 723b 0a00
0e00 160c 0017 0018 0100 0874 6f53 7472
696e 6701 0014 2829 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 0700 1a01 0015
636f 6d2f 6a76 6d2f 6f6f 6d2f 7465 7374
2f4d 6169 6e01 0004 436f 6465 0100 0f4c
696e 654e 756d 6265 7254 6162 6c65 0100
124c 6f63 616c 5661 7269 6162 6c65 5461
626c 6501 0004 7468 6973 0100 174c 636f
6d2f 6a76 6d2f 6f6f 6d2f 7465 7374 2f4d
6169 6e3b 0100 046d 6169 6e01 0016 285b
4c6a 6176 612f 6c61 6e67 2f53 7472 696e
673b 2956 0100 0461 7267 7301 0013 5b4c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b01 0001 6101 0012 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 0100 0162 0100
0163 0100 104d 6574 686f 6450 6172 616d
6574 6572 7301 000a 536f 7572 6365 4669
6c65 0100 094d 6169 6e2e 6a61 7661 0021
0019 0002 0000 0000 0002 0001 0005 0006
0001 001b 0000 002f 0001 0001 0000 0005
2ab7 0001 b100 0000 0200 1c00 0000 0600
0100 0000 0600 1d00 0000 0c00 0100 0000
0500 1e00 1f00 0000 0900 2000 2100 0200
1b00 0000 7500 0300 0400 0000 2112 074c
bb00 0959 1207 b700 0b4d bb00 0e59 b700 <-- 这里
102b b600 112c b600 11b6 0015 4eb1 0000
0002 001c 0000 0012 0004 0000 0008 0003
0009 000d 000a 0020 000b 001d 0000 002a
0004 0000 0021 0022 0023 0000 0003 001e
0024 0025 0001 000d 0014 0026 0025 0002
0020 0001 0027 0025 0003 0028 0000 0005
0100 2200 0000 0100 2900 0000 0200 2a
可以想象一下,堆类似于我们业务系统的数据库,栈相当于业务系统,唯一区别在于业务系统操作数据库需要将数据加载到本地操作,是否可以借鉴?
堆外内存资料:www.javadoop.com/post/metasp…
GC简介
算法
如何确定垃圾
引用计数:对每个对象的引用进行计数,每当有一个地方引用它时计数器 +1、引用失效则 -1,引用的计数放到对象头中,大于 0 的对象被认为是存活对象。虽然循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。
可达性:从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。目前 Java 中主流的虚拟机均采用此算法
如何回收
标记-清除(mark-sweep)
回收过程主要分为两个阶段,第一阶段为追踪(Tracing)阶段,即从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象,第二阶段为清除(Sweep)阶段,即回收器检查堆中每一个对象,并将所有未被标记的对象进行回收,整个过程不会发生对象移动。整个算法在不同的实现中会使用三色抽象(Tricolour Abstraction)、位图标记(BitMap)等技术来提高算法的效率,存活对象较多时较高效
- Marking(标记): 遍历所有的可达对象,并在本 地内存(native)中分门别类记下。
- Sweeping(清除): 这一步保证了,不可达对象 所占用的内存,在之后进行内存分配时可以重用。
标记-复制(mark-copy)
将空间分为两个大小相同的 From 和 To 两个半区,同一时间只会使用其中一个,每次进行回收时将一个半区的存活对象通过复制的方式转移到另一个半区。有递归(Robert R. Fenichel 和 Jerome C. Yochelson提出)和迭代(Cheney 提出)算法,以及解决了前两者递归栈、缓存行等问题的近似优先搜索算法。复制算法可以通过碰撞指针的方式进行快速地分配内存,但是也存在着空间利用率不高的缺点,另外就是存活对象比较大时复制的成本比较高。
标记-清除-整理(mark-sweep-compact)
这个算法的主要目的就是解决在非移动式回收器中都会存在的碎片化问题,也分为两个阶段,第一阶段与 Mark-Sweep 类似,第二阶段则会对存活对象按照整理顺序(Compaction Order)进行整理。主要实现有双指针(Two-Finger)回收算法、滑动回收(Lisp2)算法和引线整理(Threaded Compaction)算法等。
串行GC(Serial GC/ParNewGC)
- 串行 GC 对年轻代使用 mark-copy(标记-复制) 算法,对老年代使用 mark-sweep-compact(标记-清除-整理)算法。
- 两者都是单线程的垃圾收集器,不能进行并行处理,所以都会触发全线暂停(STW),停止所有的应用线程。
- 这种 GC 算法不能充分利用多核 CPU。不管有多少 CPU 内核,JVM 在垃圾收集时都只能使用单个核心。
- CPU 利用率高,暂停时间长。简单粗暴,就像老式的电脑,动不动就卡死。
- 该选项只适合几百 MB 堆内存的 JVM,而且是单核 CPU 时比较有用。
- ParNewGC 改进版本的 Serial GC
并行GC
- 年轻代和老年代的垃圾回收都会触发 STW 事件。
- 在年轻代使用 标记-复制(mark-copy)算法,在老年代使用 标记-清除-整理(mark-sweep- compact)算法。
- -XX:ParallelGCThreads=N 来指定 GC 线程数, 其默认值为 CPU 核心数。
- 并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效使用,能达到 更高的吞吐量:
- 在 GC 期间,所有 CPU 内核都在并行清理垃圾,所以总暂停时间更短;
- 在两次 GC 周期的间隔期,没有 GC 线程在运行,不会消耗任何系统资源。
并发GC
CMS -- Mostly Concurrent Mark and Sweep Garbage Collector
其对年轻代采用并行 STW 方式的 mark-copy (标记-复制)算法,对老年代主要使用并发 mark-sweep (标记-清除)算法。
CMS GC 的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,主要通过两种手段来达成 此目标:
- 不对老年代进行整理,而是使用空闲列表(free-lists)来管理内存空间的回收。
- 在 mark-and-sweep (标记-清除) 阶段的大部分工作和应用线程一起并发执行。
也就是说,在这些阶段并没有明显的应用线程暂停。但值得注意的是,它仍然和应用线程争抢 CPU 时间。默认情况下,CMS 使用的并发线程数等于 CPU 核心数的 1/4。
如果服务器是多核 CPU,并且主要调优目标是降低 GC 停顿导致的系统延迟,那么使用 CMS 是个很明智的选择。进行老年代的并发回收时,可能会伴随着多次年轻代的 minor GC。
- 阶段 1: Initial Mark(初始标记)
- 阶段 2: Concurrent Mark(并发标记)
- 阶段 3: Concurrent Preclean(并发预清理)
- 阶段 4: Final Remark(最终标记)
- 阶段 5: Concurrent Sweep(并发清除)
- 阶段 6: Concurrent Reset(并发重置)
CMS 垃圾收集器在减少停顿时间上做了很多复杂而有用的工作,用于垃圾回收的并发线程 执行的同时,并不需要暂停应用线程。 当然, CMS 也有一些缺点,其中最大的问题就是老 年代内存碎片问题(因为不压缩),在某些情 况下 GC 会造成不可预测的暂停时间,特别是 堆内存较大的情况下。
CMS收集过程:www.codercto.com/a/45937.htm…
英文好的看这里:docs.oracle.com/en/java/jav…
G1(Garbage-First)
G1 的全称是 Garbage-First,意为垃圾优先,哪 一块的垃圾最多就优先清理它。
G1的很多概念都建立在CMS之上,需要对CMS有一定了解。
G1 GC 最主要的设计目标是:将 STW 停顿的时 间和分布,变成可预期且可配置的。事实上,G1 GC 是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。为了达成可预期停 顿时间的指标,G1 GC 有一些独特的实现。
内存划分:
首先,堆不再分成年轻代和老年代,而是划分为多 个(通常是 2048 个)可以存放对象的 小块堆区 域(smaller heap regions)。每个小块,可能一会 被定义成 Eden 区,一会被指定为 Survivor区或者 Old 区。
在逻辑上,所有的 Eden 区和 Survivor 区合起来就是年轻代,所有的 Old区拼在一起那就是老年代,这样划分之后,使得 G1 不必每次都去收集整 个堆空间,而是以增量的方式来进行处理: 每次只处理一部分内存块,称为此次 GC 的回收集(collection set)。
每次 GC 暂停都会收集所有年轻代的内存块,但一般只包含部分老年代 的内存块。
G1 的另一项创新是,在并发阶段估算每个小 堆块存活对象的总数。构建回收集的原则是: 垃圾最多的小块会被优先收集。这也是 G1 名 称的由来。
G1 GC收集一般有三个大的步骤:
-
年轻代模式转移暂停(Evacuation Pause)Evacuation
-
并发标记(Concurrent Marking)
- 初始标记 (Initial Mark)
- root区扫描(Root Region Scan)
- 并发标记(Concurrent Mark)
- 再次标记( Remark)
- 清理(Cleanup)
-
转移暂停-混合模式(Evacuation Pause(mixed))
G1调优官方:www.oracle.com/technical-r…
GC比较
| 收集器 | 类型 | 作用区 | 算法 | 目标 | 使用场景 |
|---|---|---|---|---|---|
| Serial | 串行 | 新生代 | 标-复 | 响应速度优先 | 单CPU在client模式 |
| Serial Old | 串行 | 老年代 | 标-复-整理 | 响应速度优先 | 单CPU在client模式, CMS预备方案 |
| ParNew | 并行 | 新生代 | 标-复 | 响应速度优先 | 多CPU在server模式 |
| Parallel Scavenge | 并行 | 新生代 | 标-复 | 吞吐量优先 | 后台运算,交互较少 |
| Parallel Old | 并行 | 老年代 | 标-复-整理 | 吞吐量优先 | 后台运算,交互较少 |
| CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 互联网B/S |
| G1 | 并发 | 新/老 | 标-复-整-复 | 响应速度优先 | 服务端,替换CMS |
| ZGC | 并发 | 10ms/linux/64 | |||
| ShennandoahGC | 并发 |
选择正确的 GC 算法,唯一可行的方式就是去尝试,一般性的指导原则:
- 如果系统考虑吞吐优先,CPU 资源都用来最大程度处理业务,用 Parallel GC;
- 如果系统考虑低延迟有限,每次 GC 时间尽量短,用 CMS GC;
- 如果系统内存堆较大,同时希望整体来看平均 GC 时间可控,使用 G1 GC。 对于内存大小的考量:
- 一般 4G 以上,算是比较大,用 G1 的性价比较高。
- 一般超过 8G,比如 16G-64G 内存,非常推荐使用 G1
导致GC的原因
Full GC
- 年老代(Tenured)被写满
- System.gc()被显示调用
持久代Pemanet Generation空间不足
- 不同对象的活动周期不同;年轻代更快地回收,老年代回收频率相对少。分代回收 = YoungGC + OldGC
- YoungGC: GC 复制算法。 比较频繁;
- OldGC: GC 标记-清除算法。 频度低,回收慢。
常用工具
jstat
利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控
# 加载类的情况
jstat -class pid
# gc 日志常用
jstat -gc pid
# gc 日志常用
jstat -gcutil pid
# 各个堆区堆内存情况
# 不同版本的JDK内容不同,PGC有无可证明JDK8是否取消永久带
jstat -gccapacity pid
参考
jstack
jstack -fl pid
-l长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表,会使得JVM停顿得长久得多(可能会差很多倍,比如普通的jstack可能几毫秒和一次GC没区别,加了-l 就是近一秒的时间),-l 建议不要用。一般情况不需要使用
-l long listing. Prints additional information about locks
-e extended listing. Prints additional information about threads ,打印跟多信息
hello-world一共一个线程?死锁?
死锁移步:www.jianshu.com/p/8d5782bc5…
jinfo
jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括Java System属性和JVM命令行参数
# 环境信息
jinfo pid
# JVM参数
jinfo -flags pid
jinfo与jmap在jdk8中bug:bugs.java.com/bugdatabase…
jmap
命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列,随着jdk版本的更新,jmap中的有些命令被去除了,目前最常用导出dump日志,查看对象占用内存情况
# 查看前20对象
jmap -histo 10730| head -20
# 查看对象大小,并排序
jmap -histo <pid>|less
# 查看对象数最多的对象,并按降序排序输出:
jmap -histo <pid>|grep a|sort -k 2 -g -r|less
# 查看占用内存最多的最象,并按降序排序输出:
jmap -histo <pid>|grep a|sort -k 3 -g -r|less
jcmd
JDK1.7以后,整合性命令行工具,可以导出堆、查看Java进程、导出线程信息、执行GC、还可以进行采样分析。
# 进程查看
jcmd = jcmd -l = jps
# 新能分析信息
jcmd pid PerfCounter.print
# dump 日志
jcmd pid GC.heap_dump ./dump-test
# jvm信息、
jcmd pid VM.system_properties
# 执行gc
jcmd pid GC.run
官方文档:docs.oracle.com/javase/8/do…
jconsole
图形化工具,可以很方便的监控jvm情况
其他工具简介(罗列)
- jmc:下载:www.oracle.com/java/techno…
- jvisualvm:演示
- MIT:dump log
- 阿里云云效
- rancher
- gceasy.io/
- ...
常用参数
- 以-开头的是标准VM选项,VM规范的选项;
- 以-X开头的都是非标准的(这些参数并不能保证在所有的JVM上都被实现),而且如果在新版本有什么改动也不会发布通知。
- 以-XX开头的都是不稳定的并且不推荐在生产环境中使用。这些参数的改动也不会发布通知。
- Boolean型参数选项:-XX:+ 打开, -XX:- 关闭。(比如-XX:+PrintGCDetails)
- 数字型参数选项通过-XX:=设定。数字可以是 m/M(兆字节),k/K(千字节),g/G(G字节)。比如:32K表示32768字节。(比如-XX:MaxPermSize=64m)
- String型参数选项通过-XX:=设定,通常用来指定一个文件,路径,或者一个命令列表。(比如-XX:HeapDumpPath=./java_pid.hprof)
堆内存相关
# 初始堆大小,默认为物理内存的1/64,最小为1M
-Xms4g
# 最大堆大小,默认为物理内存的1/4或者1G,最小为2M
-Xmx4g
# 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小(JDK8后移动到原数据区)
-Xmn2g
# 设置年轻代初始大小,在 -client 时默认为 8,
# 在 -server 时默认为 2,因此年轻代与年老代的比率将为 1::8,
# 年轻代将是堆的 1/9 -客户端和 1::2 或堆的 1/3 与 -server。
-XX:NewSize=n ·
# 年轻代和年老代的比值:如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:NewRatio=3
# 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。
# 如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:SurvivorRatio=8
# 设置持久代大小(JDK8后移动到原数据区)
-XX:MaxPermSize=n
# 设置单个线程栈的大小,一般默认为512k
-Xss512k
# 持久代/元数据区
# 1.8 之前
-XX:MaxPermSize=256m
# 1.8 之后
-XX:MaxMetaspaceSize=256m
GC选择
# 设置串行收集器
-XX:+UseSerialGC
# 设置并行收集器(JDK8默认)
-XX:+UseParallelGC
# 设置并行年老代收集器
-XX:+UseParalledlOldGC
# 设置并发收集器(CMS:并发标记交换)
-XX:+UseConcMarkSweepGC
# 并行收集器的线程数
-XX:+UseParallelGC
# 禁止RMI调用System.gc
-XX:+ DisableExplicitGC :
监控相关
# GC简要信息
-XX:+PrintGC
# GC详细信息
-XX:+PrintGCDetails
# GC详细信息时间戳
-XX:+PrintGCTimeStamps
-Xloggc:filename
其他参数
# 关闭自适应
-XX:-UseAdaptiveSizePolicy
# 设置垃圾最大年龄(一个对象进入老年带带标准)
-XX:MaxTenuringThreshold=0:
# 禁止调用System.gc();但jvm的gc仍然有效
-XX:-DisableExplicitGC
# 指定导出堆信息时的路径或文件名
-XX:HeapDumpPath=./java_pid<pid>.hprof
# 大对象定义
-XX:PretenureSizeThreshold=2097152
一般性的指导配置
Xmx:一般认为设置为物理内存的70%-80%,假设计算机物理内存8g,系统会用掉一些,还剩7.5g,那么配置为7.5X80%=6g,如果有明确使用到堆外内存的(Netty),还需进一步调整降低。
Xms:若非混和部署(一个机器部署多个应用,且没有容器化隔离)配置和Xmx一致
元数据区不建议设置,默认为无限大(受系统本身内存瓶颈制约)
官方参考
参数:docs.oracle.com/en/java/jav…
www.reins.altervista.org/java/gc1.4.…
总结几种常见的内存溢出问题
OutOfMemoryError: Metaspace
一般是元数据区内存不够了,元数据区一般内存变化不大,一般启动会报这个错,调大些就好了
## 初始大小
-XX:MetaspaceSize=20000K
## 最大值
-XX:MaxMetaspaceSize=20000K
OutOfMemoryError:Java heap space
- 请求创建一个超大对象,通常是一个大数组。
- 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
- 过度使用终结器(Finalizer),该对象没有立即被 GC。
- 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收
针对大部分情况,通常只需要通过 -Xmx参数调高 JVM 堆内存空间即可。如果仍然没有解决,可以参考以下情况做进一步处理:
- 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制。
- 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。
- 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接
## 最大堆
-Xmx
OutOfMemoryError:GC overhead limit exceeded
当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次。
一般情况下:如果系统运行过久后出现,大部分情况下是代码问题。如果系统运行初期抛出,调大堆即可。
OutOfMemoryError:Unableto createnewnativethread
原因
- 线程数超过操作系统最大线程数 ulimit 限制;
- 线程数超过 kernel.pid_max(只能重启);
- native 内存不足
方案
- 升级配置,为机器提供更多的内存;
- 降低 Java Heap Space 大小;
- 修复应用程序的线程泄漏问题;
- 限制线程池大小;
- 使用 -Xss 参数减少线程栈的大小;
- 调高 OS 层面的线程最大数:执行 ulimia-a查看最大线程数限制,使用 ulimit-u xxx调整最大线程数限制
总结
结合实际业务需求和环境,选择合适的时机,提高系统的性能是每个工程师追求的。作为一名JAVA工程师,了解JVM相关优化的知识,掌握对其分析、使用、优化的能力是必不可少的。这里简单介绍了关于系统优化的话题,对JVM内存做了简单介绍,并在字节码执行的层面对内存情况分析;并探讨了几种常见的GC,并分析其优劣势,使用场景、如何选择等话题;使用方面实践并介绍了几种常见的分析工具,并使用这些工具简单的做了一次调优的实验,并结合此次试验分析了一次调优案例。