jvm-调优-案例分析

1:内存优化示例

1 监控分析
配置参数,获得GC⽇志

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps

拿到如下FullGC信息

45.705:
[Full GC (Ergonomics) --⾃适应调整引发的full gc , Allocation Failure 、
System.gc() 
[PSYoungGen: 37887K->0K(359424K)] --新⽣代垃圾收集:垃圾收集前->垃圾收集后新⽣代使⽤量
(新⽣代总⼤⼩)
[ParOldGen: 98362K->95250K(184832K)] --⽼⽣代垃圾收集:垃圾收集前->垃圾收集后⽼年代使⽤
量(⽼年代总⼤⼩)
136250K->95250K(544256K), --垃圾收集前->垃圾收集后堆内存的使⽤量(堆总空间⼤⼩)
[Metaspace: 3135K->3135K(1056768K)],元空间区域垃圾收集前->后元空间的使⽤量(元空间⼤⼩)
0.0773607 secs] --GC事件持续的时间
[Times: user=1.25 sys=0.02, real=0.07 secs] --GC线程消耗的cpu时间,GC过程中操作系统
调⽤和系统等待事件所消耗的时间,应⽤程序暂停的时间

以上gc⽇志中,在发⽣fullGC之时,整个应⽤的堆占⽤以及GC时间。为了更加精确需多次收集,计算平均值。或者是采⽤耗时最⻓的⼀次FullGC来进⾏估算。上图中,⽼年代空间占⽤在95250kb(约93MB),以此定为⽼年代空间的活跃数据。
2 判断
因FullGC的时间1.25秒⼤于1秒,故需要进⾏调优。
3 确定⽬标
则其他堆空间的分配,基于以下规则来进⾏。
⽼年代的空间⼤⼩为 93MB

  • java heap:参数-Xms和-Xmx,建议扩⼤⾄3-4倍FullGC后的⽼年代空间占⽤。
    93 * (3-4) = (279-372)MB ,设置heap⼤⼩为372MB;
  • 元空间:参数-XX:MetaspaceSize=N和-XX:MaxMetaspaceSize=N,建议扩⼤⾄1.2-1.5倍FullGc后的元空间空间占⽤。
    3135K*(1.2-1.5)=(3762-4702)K,设置元空间⼤⼩为5MB;
  • 新⽣代:参数-Xmn,建议扩⼤⾄1-1.5倍FullGC之后的⽼年代空间占⽤。
    93*(1-1.5)=(93-139.5)M,设置新⽣代⼤⼩为140MB;

4 调整参数

java -Xms373m -Xmx373m -Xmn140m -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5m

5 对⽐差异
收集FullGC⽇志,发现FullGC的时间0.28秒,已经⼩于1秒,并且频率不⾼。已经达到调优⽬标,应⽤到所有服务器配置。

2:延迟优化示例

确定年轻代的⼤⼩是通过评估垃圾回收的统计信息以及观察MinorGC的消耗时间和频率,下⾯举例说明如何通过垃圾回收的统计信息来确定年轻代的⼤⼩。

尽管MinorGC消耗的时间和年轻代⾥⾯的存活的对象数量有直接关系,但是⼀般情况下,更⼩年轻代空间,更短的MinorGC时间。如果不考虑MinorGC的时间消耗,减少年轻代的⼤⼩会导致MinorGC变得更加频繁,由于更⼩的空间,⽤完空间会⽤更少的时间。同理,提⾼年轻代的⼤⼩会降低MinorGC的频率

当测试垃圾回收数据的时候,发现MinorGC的时间太⻓了,正确的做法就是减少年轻代的空间⼤⼩。如果MinorGC太频繁了就增加年轻代的空间⼤⼩。

1 监控分析
这个例⼦是运⾏在如下的HotSpot VM命令参数下的。

-Xms6144m -Xmx6144m -Xmn2048m -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=96m -XX:+UserParallelOldGC
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -
Xloggc:/Users/hadoop/Desktop/gc.log

得到以下GC⽇志

2020-12-17T14:40:29.564-0800: 1.280: --GC事件开始的时间,相对于jvm开始启动的间隔秒数
[GC (Allocation Failure) --区分GC类型,触发gc原因 
[PSYoungGen: 2045989K->249795K(2097152K)] --新⽣代:垃圾收集前新⽣代使⽤量->后新⽣代使
⽤量(新⽣代总⼤⼩)
3634533K->1838430K(6291456K),垃圾收集前->后堆内存的使⽤量(堆总空间⼤⼩)
0.0543798 secs] GC事件持续的时间
[Times: user=0.38 sys=0.01, real=0.05 secs] GC线程消耗的cpu时间,GC过程中操作系统调
⽤和系统等待事件所消耗的时间 应⽤程序暂停的时间
2020-12-17T14:40:31.949-0800: 3.665:
[GC (Allocation Failure)
[PSYoungGen: 2047896K->247788K(2097152K)]
3655319K->1859216K(6291456K),
0.0539614 secs] 
[Times: user=0.35 sys=0.01, real=0.05 secs]
2020-12-17T14:40:34.346-0800: 6.062:
[GC (Allocation Failure)
[PSYoungGen: 2045889K->248993K(2097152K)]
3677202K->1881099K(6291456K),
0.0532377 secs] 
[Times: user=0.39 sys=0.01, real=0.05 secs]
2019-12-21T14:40:36.815-0800: 8.531:
[GC (Allocation Failure)
[PSYoungGen: 2047094K->247765K(2097152K)]
3696985K->1900882K(6291456K),
0.054332 secs] 
[Times: user=0.37 sys=0.01, real=0.05 secs]

显示了MinorGC平均的消耗时间是0.05秒,平均的频率是2.417秒1次。当计算MinorGC的消耗时间和频率的时候,越多的数据参与计算,准确性会越⾼。并且应⽤要处于稳定运⾏状态下来收集MinorGC信息也是⾮常重要的。

下⼀步是⽐较MinorGC的平均时间和系统对延迟的要求,如果MinorGC的延迟时间⼤于系统的要求,减少年轻代的空间⼤⼩,然后继续测试,再收集数据以及重新评估。 如果MinorGC的频率⼤于系统的要求,就增加年轻代的空间⼤⼩,然后继续测试,再收集以及重新评估。也许需要数次重复才能够让系统达到延迟要求。当你改变年轻代的空间⼤⼩的时候,尽量保持⽼年代的空间⼤⼩不要改变。

2 判断
从上垃圾回收信息来看,如果应⽤的延迟要求是40毫秒的话,观察到的MinorGC的延迟是50毫秒,⽐系统的要求⾼出了不少,应该减少年轻代⼤⼩。

3 调整参数
意味着年⽼代的空间⼤⼩是4096M(6144M-2048M),减⼩年轻代的空间⼤⼩的10%⽽且要保持old代的空间⼤⼩不变,可以使⽤如下选项。

-Xms5940m -Xmx5940m -Xmn1844m -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=96m -
XX:+UserParallelOldGC

注意的是young代的空间⼤⼩从2048M减少到1844M,整个Java堆的⼤⼩从6144M减少到5940M, 两者都是减少了204m。

4 重复调整

⽆论是young的空间调⼤还是调⼩,都需要重新收集垃圾回收信息和重新计算MinorGC的平均时间和频率,以达到应⽤的延迟要求,可能需要⼏个轮回来达到这个要求。

为了说明了增加年轻代的⼤⼩以降低MinorGC的频率,我们下⾯举⼀个例⼦。如果系统要求的频率是5秒⼀次,这个上⾯的例⼦中是2.417秒⼀次,也就是说它⽤了2.417秒,填充满了2048M空间,如果需要5秒⼀次的频率,那么就需要5/2.417倍的空间,即2048*5/2.417等于4237M。因此young代的空间需要调整到4237M。下⾯是⼀个示例来说明配置这个:

-Xms8333m -Xmx8333m -Xmn4237m -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=96m
-XX:+UsePrallelOldGC

注意是-Xms和-Xmx也同步调整了
另外⼀些调整young代的空间需要注意的事项:

  • ⽼年代的空间⼀定不能⼩于活动对象的⼤⼩的1.5倍。
  • 年轻代的空间⾄少要有Java堆⼤⼩的10%,太⼩的Java空间会导致过于频繁的MinorGC。
  • 当提⾼Java堆⼤⼩的时候,不要超过JVM可以使⽤的物理内存⼤⼩。如果使⽤过多的物理内存,会
    导致使⽤交换区,这个会严重影响性能。

如果在仅仅是MinorGC导致了延迟的情况下,你⽆法通过调整young代的空间来满⾜系统的需求,那么你需要重新修改应⽤程序、修改JVM部署模型把应⽤部署到多个JVM上⾯(通常得要多机器了)或者重新评估系统的需求。

3:检测死锁

所谓死锁,是指多个进程(线程)在运⾏过程中因争夺资源⽽造成的⼀种僵局,当进程(线程)处于这种僵持状态时,若⽆外⼒作⽤,它们都将⽆法再向前推进。 因此我们举个例⼦来描述,如果此时有⼀个线程A,按照先锁a再获得锁b的的顺序获得锁,⽽在此同时⼜有另外⼀个线程B,按照先锁b再锁a的顺序获得锁。

3.1 死锁产⽣条件

  • 1.互斥条件:进程(线程)要求对所分配的资源进⾏排它性控制,即在⼀段时间内某资源仅为⼀进程(线程)所占⽤。
  • 2.请求和保持条件:当进程(线程)因请求资源⽽阻塞时,对⽅获得的资源保持不放。
  • 3.不剥夺条件:进程(线程)已获得的资源在未使⽤完之前,不能剥夺,只能在使⽤完时由⾃⼰释放。
  • 4.环路等待条件:在发⽣死锁时,必然存在⼀个进程(线程)–资源的环形链。
    在这里插入图片描述
package com.example.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeathLock {
	 private static Lock lock1 = new ReentrantLock();
	 private static Lock lock2 = new ReentrantLock();
	 public static void deathLock() {
		 Thread t1 = new Thread() {
			 @Override
			 public void run() {
				 try {
					lock1.lock();
					TimeUnit.SECONDS.sleep(1);
					lock2.lock();
				 } catch (InterruptedException e) {
					e.printStackTrace();
				 }
			 }
		 };
		 Thread t2 = new Thread() {
			 @Override
			 public void run() {
				 try {
					lock2.lock();
					TimeUnit.SECONDS.sleep(1);
					lock1.lock();
				 } catch (InterruptedException e) {
					e.printStackTrace();
				 }
			 }
		 };
		 t1.setName("mythread1");
		 t2.setName("mythread2");
		 t1.start();
		 t2.start();
	 }
	 public static void main(String[] args) {
		deathLock();
	 }
}

3.3 查看进程号

在这里插入图片描述

3.4 查看进程号

jstack是java虚拟机⾃带的⼀种堆栈跟踪⼯具。Jstack⼯具可以⽤于⽣成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每⼀条线程正在执⾏的⽅法堆栈的集合,⽣成线程快照的主要⽬的是定位线程出现⻓时间停顿的原因,如 线程间死锁 、 死循环 、 请求外部资源导致的⻓时间等待 等。 线程出现停顿的时候通过jstack来查看各个线程的调⽤堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
在这里插入图片描述
在这里插入图片描述

3.5 jconsole 检测

Jconsole是JDK⾃带的监控⼯具,在JDK/bin⽬录下可以找到。它⽤于连接正在运⾏的本地或者远程的JVM,对运⾏在Java应⽤程序的资源消耗和性能进⾏监控,并画出⼤量的图表,提供强⼤的可视化界⾯。⽽且本身占⽤的服务器内存很⼩,甚⾄可以说⼏乎不消耗。
在这里插入图片描述

.3.6 Arthas检测

[arthas@46384]$ thread -b
"mythread1" Id=10 WAITING on
java.util.concurrent.locks.ReentrantLock$NonfairSync@7a724bfd owned by
"mythread2" Id=11
 at sun.misc.Unsafe.park(Native Method)
 - waiting on
java.util.concurrent.locks.ReentrantLock$NonfairSync@7a724bfd
 at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
 at
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(Ab
stractQueuedSynchronizer.java:836)
 at
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQu
euedSynchronizer.java:870)
 at
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSy
nchronizer.java:1199)
 at
java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:2
09)
 at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
 at com.kkb.example.DeathLock$1.run(DeathLock.java:20)
 Number of locked synchronizers = 1
 - java.util.concurrent.locks.ReentrantLock$NonfairSync@6dff7c09 <---- but
blocks 1 other threads!
Affect(row-cnt:0) cost in 8 ms. [arthas@46384]$

3.6 死锁预防

1、以确定的顺序获得锁
如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。按照上⾯的例⼦,两个线程获得锁的时序图如下:
在这里插入图片描述
2、超时放弃
当使⽤synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然⽽Lock
接⼝提供了 boolean tryLock(long time, TimeUnit unit) throws InterruptedException ⽅法,该⽅法可以按照固定时⻓等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种⽅式,也可以很有效地避免死锁。 还是按照之前的例⼦,时序图如下:
在这里插入图片描述

4 OutOfMemoryError:PermGen space

OutOfMemoryError:PermGen space 表示⽅法区和运⾏时常量池溢出。
原因:

  • Perm 区主要⽤于存放 Class 和 Meta 信息的,Class 在被 Loader 时就会被放到 PermGen space,这个区域称为永久代。GC 在主程序运⾏期间不会对永久代进⾏清理,默认是 64M ⼤⼩。
  • 当程序程序中使⽤了⼤量的 jar 或 class,使 java 虚拟机装载类的空间不够,超过 64M 就会报这部分内存溢出了,需要加⼤内存分配,⼀般 128m ⾜够。\

解决⽅案:

  • 扩⼤永久代空间
    • JDK7 以前使⽤ -XX:PermSize 和 -XX:MaxPermSize 来控制永久代⼤⼩。
    • JDK8 以后把原本放在永久代的字符串常量池移出, 放在 Java 堆中(元空间 Metaspace)中,元数据并不在虚拟机中,使⽤的是本地的内存。使⽤ -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 控制元空间⼤⼩。
  • 清理应⽤程序中 WEB-INF/lib 下的 jar,⽤不上的 jar 删除掉,多个应⽤公共的 jar 移动到
    Tomcat 的 lib ⽬录,减少重复加载

5 OutOfMemoryError:Java heap space

OutOfMemoryError:Java heap space 表示堆空间溢出。
原因
JVM 分配给堆内存的空间已经⽤满了。
问题定位
使⽤ jmap 或 -XX:+HeapDumpOnOutOfMemoryError 获取堆快照。 (2)使⽤内存分析⼯具(visualvm、mat、jProfile 等)对堆快照⽂件进⾏分析。 (3)根据分析图,重点是确认内存中的对象是否是必要的,分清究竟是是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

内存泄露

内存泄漏是指由于疏忽或错误造成程序未能释放已经不再使⽤的内存的情况

内存泄漏并⾮指内存在物理上的消失,⽽是应⽤程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。

内存泄漏随着被执⾏的次数越多-最终会导致内存溢出。

⽽因程序死循环导致的不断创建对象-只要被执⾏到就会产⽣内存溢出。

内存泄漏常⻅⼏个情况:

  • 静态集合类
    • 声明为静态(static)的 HashMap、Vector 等集合
    • 通俗来讲 A 中有 B,当前只把 B 设置为空,A 没有设置为空,回收时 B ⽆法回收-因被 A 引
      ⽤。
  • 监听器
    • 监听器被注册后释放对象时没有删除监听器
  • 物理连接
    • DataSource.getConnection()建⽴链接,必须通过 close()关闭链接
  • 内部类和外部模块等的引⽤
    • 发现它的⽅式同内存溢出,可再加个实时观察
    • jstat -gcutil 7362 2500 70

重点关注:

  • FGC — 从应⽤程序启动到采样时发⽣ Full GC 的次数。
  • FGCT — 从应⽤程序启动到采样时 Full GC 所⽤的时间(单位秒)。
  • FGC 次数越多,FGCT 所需时间越多-可⾮常有可能存在内存泄漏。

解决⽅案

  • 检查程序,看是否有死循环或不必要地重复创建⼤量对象。有则改之。
  • 扩⼤堆内存空间

使⽤ -Xms 和 -Xmx 来控制堆内存空间⼤⼩。

6 OutOfMemoryError: GC overhead limitexceeded

原因
JDK6 新增错误类型,当 GC 为释放很⼩空间占⽤⼤量时间时抛出;⼀般是因为堆太⼩,导致异常的原因,没有⾜够的内存

解决⽅案:
查看系统是否有使⽤⼤内存的代码或死循环; 通过添加 JVM 配置,来限制使⽤内存:

<jvm-arg>-XX:-UseGCOverheadLimit</jvm-arg>
#### OutOfMemoryError:unable to create new native thread

7 -XX:InitialCodeCacheSize and -XX:ReservedCodeCacheSize

VM⼀个有趣的,但往往被忽视的内存区域是“代码缓存”,它是⽤来存储已编译⽅法⽣成的本地代码。代码缓存确实很少引起性能问题,但是⼀旦发⽣其影响可能是毁灭性的。如果代码缓存被占满,JVM会打印出⼀条警告消息,并切换到interpreted-only 模式:JIT编译器被停⽤,字节码将不再会被编译成机器码。因此,应⽤程序将继续运⾏,但运⾏速度会降低⼀个数量级,直到有⼈注意到这个问题。就像其他内存区域⼀样,我们可以⾃定义代码缓存的⼤⼩。相关的参数是-XX:InitialCodeCacheSize 和-XX:ReservedCodeCacheSize,它们的参数和上⾯介绍的参数⼀样,都是字节值。

8 -XX:+UseCodeCacheFlushing

如果代码缓存不断增⻓,例如,因为热部署引起的内存泄漏,那么提⾼代码的缓存⼤⼩只会延缓其发⽣溢出。为了避免这种情况的发⽣,我们可以尝试⼀个有趣的新参数:当代码缓存被填满时让JVM放弃⼀些编译代码。通过使⽤-XX:+UseCodeCacheFlushing 这个参数,我们⾄少可以避免当代码缓存被填满的时候JVM切换到interpreted-only 模式。不过,我仍建议尽快解决代码缓存问题发⽣的根本原因,如找出内存泄漏并修复它。

9 -XX:OnOutOfMemoryError

当内存溢发⽣时,我们甚⾄可以可以执⾏⼀些指令,⽐如发个E-mail通知管理员或者执⾏⼀些清理⼯作。通过-XX:OnOutOfMemoryError 这个参数我们可以做到这⼀点,这个参数可以接受⼀串指令和它们的参数。在这⾥,我们将不会深⼊它的细节,但我们提供了它的⼀个例⼦。在下⾯的例⼦中,当内存溢出错误发⽣的时候,我们会将堆内存快照写到/tmp/heapdump.hprof ⽂件并且在JVM的运⾏⽬录执⾏脚本cleanup.sh

java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -
XX:OnOutOfMemoryError =“sh ~/cleanup.sh” MyApp

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值