JVM 性能调优有很多设置, 这个参考 JVM 参数即可.
主要调优的目的:
控制 GC 的行为. GC 是一个后台处理, 但是它也是会消耗系统性能的, 因此经常会根据系统运行的程序的特性来更改 GC 行为
控制 JVM 堆栈大小. 一般来说, JVM 在内存分配 https://www.baidu.com/s?wd=内存分配&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1Y3mWNbPyFhnjTvuHTvuW6Y0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-bIi4WUvYETgN-TLwGUv3EPHRzPj0sP104n16dPWn1PWDz 上不需要你修改,(举例)但是当你的程序新生代对象在某个时间段产生的比较多的时候, 就需要控制新生代的堆大小. 同时, 还要需要控制总的 JVM 大小避免内存溢出
控制 JVM 线程的内存分配 https://www.baidu.com/s?wd=内存分配&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1Y3mWNbPyFhnjTvuHTvuW6Y0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-bIi4WUvYETgN-TLwGUv3EPHRzPj0sP104n16dPWn1PWDz . 如果是多线程程序, 产生线程和线程运行所消耗的内存也是可以控制的, 需要通过一定时间的观测后, 配置最优结果
最近因项目存在内存泄漏, 故进行大规模的 JVM 性能调优 , 现把经验做一记录.
一, JVM 内存模型及垃圾收集算法
1. 根据 Java 虚拟机规范, JVM 将内存划分为:
- New(年轻代)
- Tenured(年老代)
永久代(Perm)
其中 New 和 Tenured 属于堆内存, 堆内存会从 JVM 启动参数 (-Xmx:3G) 指定的内存中分配, Perm 不属于堆内存, 有虚拟机直接分配, 但可以通过 - XX:PermSize -XX:MaxPermSize 等参数调整其大小.
年轻代(New): 年轻代用来存放 JVM 刚分配的 Java 对象
年老代(Tenured): 年轻代中经过垃圾回收没有回收掉的对象将被 Copy 到年老代
永久代(Perm): 永久代存放 Class,Method 元信息, 其大小跟项目的规模, 类, 方法的量有关, 一般设置为 128M 就足够, 设置原则是预留 30% 的空间.
New 又分为几个部分:
Eden:Eden 用来存放 JVM 刚分配的对象
Survivor1
Survivro2: 两个 Survivor 空间一样大, 当 Eden 中的对象经过垃圾回收没有被回收掉时, 会在两个 Survivor 之间来回 Copy, 当满足某个条件, 比如 Copy 次数, 就会被 Copy 到 Tenured. 显然, Survivor 只是增加了对象在年轻代中的逗留时间, 增加了被垃圾回收的可能性.
2. 垃圾回收算法
垃圾回收算法可以分为三类, 都基于标记 - 清除 (复制) 算法:
Serial 算法(单线程)
并行算法
并发算法
JVM 会根据机器的硬件配置对每个内存代选择适合的回收算法, 比如, 如果机器多于 1 个核, 会对年轻代选择并行算法, 关于选择细节请参考 JVM 调优文档.
稍微解释下的是, 并行算法是用多线程进行垃圾回收, 回收期间会暂停程序的执行, 而并发算法, 也是多线程回收, 但期间不停止应用执行. 所以, 并发算法适用于交互性高的一些程序. 经过观察, 并发算法会减少年轻代的大小, 其实就是使用了一个大的年老代, 这反过来跟并行算法相比吞吐量相对较低.
还有一个问题是, 垃圾回收动作何时执行?
当年轻代内存满时, 会引发一次普通 GC, 该 GC 仅回收年轻代. 需要强调的时, 年轻代满是指 Eden 代满, Survivor 满不会引发 GC
当年老代满时会引发 Full GC,Full GC 将会同时回收年轻代, 年老代
当永久代满时也会引发 Full GC, 会导致 Class,Method 元信息的卸载
另一个问题是, 何时会抛出 OutOfMemoryException, 并不是内存被耗空的时候才抛出
JVM98% 的时间都花费在内存回收
每次回收的内存小于 2%
满足这两个条件将触发 OutOfMemoryException, 这将会留给系统一个微小的间隙以做一些 Down 之前的操作, 比如手动打印 Heap Dump.
二, 内存泄漏及解决方法
1. 系统崩溃前的一些现象:
每次垃圾回收的时间越来越长, 由之前的 10ms 延长到 50ms 左右, FullGC 的时间也有之前的 0.5s 延长到 4,5s
FullGC 的次数越来越多, 最频繁时隔不到 1 分钟就进行一次 FullGC
年老代的内存越来越大并且每次 FullGC 后年老代没有内存被释放
之后系统会无法响应新的请求, 逐渐到达 OutOfMemoryError 的临界值.
2. 生成堆的 dump 文件
通过 JMX 的 MBean 生成当前的 Heap 信息, 大小为一个 3G(整个堆的大小)的 hprof 文件, 如果没有启动 JMX 可以通过 Java 的 jmap 命令来生成该文件.
3. 分析 dump 文件
下面要考虑的是如何打开这个 3G 的堆信息文件, 显然一般的 Window 系统没有这么大的内存, 必须借助高配置的 Linux. 当然我们可以借助 X-Window 把 Linux 上的图形导入到 Window. 我们考虑用下面几种工具打开该文件:
- Visual VM
- IBM HeapAnalyzer
JDK 自带的 Hprof 工具
使用这些工具时为了确保加载速度, 建议设置最大内存为 6G. 使用后发现, 这些工具都无法直观地观察到内存泄漏, Visual VM 虽能观察到对象大小, 但看不到调用堆栈; HeapAnalyzer 虽然能看到调用堆栈, 却无法正确打开一个 3G 的文件. 因此, 我们又选用了 Eclipse 专门的静态内存分析工具: Mat.
4. 分析内存泄漏
通过 Mat 我们能清楚地看到, 哪些对象被怀疑为内存泄漏, 哪些对象占的空间最大及对象的调用关系. 针对本案, 在 ThreadLocal 中有很多的 JbpmContext 实例, 经过调查是 JBPM 的 Context 没有关闭所致.
另, 通过 Mat 或 JMX 我们还可以分析线程状态, 可以观察到线程被阻塞在哪个对象上, 从而判断系统的瓶颈.
5. 回归问题
Q: 为什么崩溃前垃圾回收的时间越来越长?
A: 根据内存模型和垃圾回收算法, 垃圾回收分两部分: 内存标记, 清除(复制), 标记部分只要内存大小固定时间是不变的, 变的是复制部分, 因为每次垃圾回收都有一些回收不掉的内存, 所以增加了复制量, 导致时间延长. 所以, 垃圾回收的时间也可以作为判断内存泄漏的依据
Q: 为什么 Full GC 的次数越来越多?
A: 因此内存的积累, 逐渐耗尽了年老代的内存, 导致新对象分配没有更多的空间, 从而导致频繁的垃圾回收
Q: 为什么年老代占用的内存越来越大?
A: 因为年轻代的内存无法被回收, 越来越多地被 Copy 到年老代
三, 性能调优
除了上述内存泄漏外, 我们还发现 CPU 长期不足 3%, 系统吞吐量不够, 针对 8core×16G,64bit 的 Linux 服务器来说, 是严重的资源浪费.
在 CPU 负载不足的同时, 偶尔会有用户反映请求的时间过长, 我们意识到必须对程序及 JVM 进行调优. 从以下几个方面进行:
线程池: 解决用户响应时间长的问题
连接池
JVM 启动参数: 调整各代的内存比例和垃圾回收算法, 提高吞吐量
程序算法: 改进程序逻辑算法提高性能
1.Java 线程池(java.util.concurrent.ThreadPoolExecutor)
大多数 JVM6 上的应用采用的线程池都是 JDK 自带的线程池, 之所以把成熟的 Java 线程池进行罗嗦说明, 是因为该线程池的行为与我们想象的有点出入. Java 线程池有几个重要的配置参数:
corePoolSize: 核心线程数(最新线程数)
maximumPoolSize: 最大线程数, 超过这个数量的任务会被拒绝, 用户可以通过 RejectedExecutionHandler 接口自定义处理方式
keepAliveTime: 线程保持活动的时间
workQueue: 工作队列, 存放执行的任务
Java 线程池需要传入一个 Queue 参数 (workQueue) 用来存放执行的任务, 而对 Queue 的不同选择, 线程池有完全不同的行为:
SynchronousQueue:
一个无容量的等待队列, 一个线程的 insert 操作必须等待另一线程的 remove 操作, 采用这个 Queue 线程池将会为每个任务分配一个新线程
LinkedBlockingQueue :
无界队列, 采用该 Queue, 线程池将忽略
maximumPoolSize 参数, 仅用 corePoolSize 的线程处理所有的任务, 未处理的任务便在
LinkedBlockingQueue 中排队
ArrayBlockingQueue: 有界队列, 在有界队列和
maximumPoolSize 的作用下, 程序将很难被调优: 更大的 Queue 和小的 maximumPoolSize 将导致 CPU 的低负载; 小的 Queue 和大的池, Queue 就没起动应有的作用.
其实我们的要求很简单, 希望线程池能跟连接池一样, 能设置最小线程数, 最大线程数, 当最小数 <任务<最大数时, 应该分配新的线程处理; 当任务> 最大数时, 应该等待有空闲线程再处理该任务.
但线程池的设计思路是, 任务应该放到 Queue 中, 当 Queue 放不下时再考虑用新线程处理, 如果 Queue 满且无法派生新线程, 就拒绝该任务. 设计导致 "先放等执行","放不下再执行","拒绝不等待". 所以, 根据不同的 Queue 参数, 要提高吞吐量不能一味地增大 maximumPoolSize.
当然, 要达到我们的目标, 必须对线程池进行一定的封装, 幸运的是 ThreadPoolExecutor 中留了足够的自定义接口以帮助我们达到目标. 我们封装的方式是:
以 SynchronousQueue 作为参数, 使 maximumPoolSize 发挥作用, 以防止线程被无限制的分配, 同时可以通过提高 maximumPoolSize 来提高系统吞吐量
自定义一个 RejectedExecutionHandler, 当线程数超过 maximumPoolSize 时进行处理, 处理方式为隔一段时间检查线程池是否可以执行新 Task, 如果可以把拒绝的 Task 重新放入到线程池, 检查的时间依赖 keepAliveTime 的大小.
2. 连接池(org.apache.commons.dbcp.BasicDataSource)
在使用 org.apache.commons.dbcp.BasicDataSource 的时候, 因为之前采用了默认配置, 所以当访问量大时, 通过 JMX 观察到很多 Tomcat 线程都阻塞在 BasicDataSource 使用的 Apache ObjectPool 的锁上, 直接原因当时是因为 BasicDataSource 连接池的最大连接数设置的太小, 默认的 BasicDataSource 配置, 仅使用 8 个最大连接.
我还观察到一个问题, 当较长的时间不访问系统, 比如 2 天, DB 上的 Mysql 会断掉所以的连接, 导致连接池中缓存的连接不能用. 为了解决这些问题, 我们充分研究了 BasicDataSource, 发现了一些优化的点:
Mysql 默认支持 100 个链接, 所以每个连接池的配置要根据集群中的机器数进行, 如有 2 台服务器, 可每个设置为 60
initialSize: 参数是一直打开的连接数
minEvictableIdleTimeMillis: 该参数设置每个连接的空闲时间, 超过这个时间连接将被关闭
timeBetweenEvictionRunsMillis: 后台线程的运行周期, 用来检测过期连接
maxActive: 最大能分配的连接数
maxIdle: 最大空闲数, 当连接使用完毕后发现连接数大于 maxIdle, 连接将被直接关闭. 只有 initialSize < x < maxIdle 的连接将被定期检测是否超期. 这个参数主要用来在峰值访问时提高吞吐量.
initialSize 是如何保持的? 经过研究代码发现, BasicDataSource 会关闭所有超期的连接, 然后再打开 initialSize 数量的连接, 这个特性与 minEvictableIdleTimeMillis,timeBetweenEvictionRunsMillis 一起保证了所有超期的 initialSize 连接都会被重新连接, 从而避免了 Mysql 长时间无动作会断掉连接的问题.
3.JVM 参数
在 JVM 启动参数中, 可以设置跟内存, 垃圾回收相关的一些参数设置, 默认情况不做任何设置 JVM 会工作的很好, 但对一些配置很好的 Server 和具体的应用必须仔细调优才能获得最佳性能. 通过设置我们希望达到一些目标:
GC 的时间足够的小
GC 的次数足够的少
发生 Full GC 的周期足够的长
前两个目前是相悖的, 要想 GC 时间小必须要一个更小的堆, 要保证 GC 次数足够少, 必须保证一个更大的堆, 我们只能取其平衡.
(1)针对 JVM 堆的设置一般, 可以通过 - Xms -Xmx 限定其最小, 最大值, 为了防止垃圾收集器在最小, 最大之间收缩堆而产生额外的时间, 我们通常把最大, 最小设置为相同的值
(2)年轻代和年老代将根据默认的比例 (1:2) 分配堆内存, 可以通过调整二者之间的比率 NewRadio 来调整二者之间的大小, 也可以针对回收代, 比如年轻代, 通过 -XX:newSize -XX:MaxNewSize 来设置其绝对大小. 同样, 为了防止年轻代的堆收缩, 我们通常会把 - XX:newSize -XX:MaxNewSize 设置为同样大小
(3)年轻代和年老代设置多大才算合理? 这个我问题毫无疑问是没有答案的, 否则也就不会有调优. 我们观察一下二者大小变化有哪些影响
更大的年轻代必然导致更小的年老代, 大的年轻代会延长普通 GC 的周期, 但会增加每次 GC 的时间; 小的年老代会导致更频繁的 Full GC
更小的年轻代必然导致更大年老代, 小的年轻代会导致普通 GC 很频繁, 但每次的 GC 时间会更短; 大的年老代会减少 Full GC 的频率
如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象, 应该选择更大的年轻代; 如果存在相对较多的持久对象, 年老代应该适当增大. 但很多应用都没有这样明显的特性, 在抉择时应该根据以下两点:(A)本着 Full GC 尽量少的原则, 让年老代尽量缓存常用对象, JVM 的默认比例 1:2 也是这个道理 (B)通过观察应用一段时间, 看其他在峰值时年老代会占多少内存, 在不影响 Full GC 的前提下, 根据实际情况加大年轻代, 比如可以把比例控制在 1:1. 但应该给年老代至少预留 1/3 的增长空间
(4)在配置较好的机器上(比如多核, 大内存), 可以为年老代选择并行收集算法: -XX:+UseParallelOldGC , 默认为 Serial 收集
(5)线程堆栈的设置: 每个线程默认会开启 1M 的堆栈, 用于存放栈帧, 调用参数, 局部变量等, 对大多数应用而言这个默认值太了, 一般 256K 就足用. 理论上, 在内存不变的情况下, 减少每个线程的堆栈, 可以产生更多的线程, 但这实际上还受限于操作系统.
(4)可以通过下面的参数打 Heap Dump 信息
- -XX:HeapDumpPath
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:/usr/aaa/dump/heap_trace.txt
通过下面参数可以控制 OutOfMemoryError 时打印堆的信息
-XX:+HeapDumpOnOutOfMemoryError
请看一下一个时间的 Java 参数配置:(服务器: Linux 64Bit,8Core×16G)
JAVA_OPTS="$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G"
经过观察该配置非常稳定, 每次普通 GC 的时间在 10ms 左右, Full GC 基本不发生, 或隔很长很长的时间才发生一次
通过分析 dump 文件可以发现, 每个 1 小时都会发生一次 Full GC, 经过多方求证, 只要在 JVM 中开启了 JMX 服务, JMX 将会 1 小时执行一次 Full GC 以清除引用, 关于这点请参考附件文档.
4. 程序算法调优: 本次不作为重点
参考链接: https://blog.csdn.net/chen77716/article/details/5695893
来源: http://www.bubuko.com/infodetail-2581269.html