写点什么

性能优化 - 内存泄漏、内存溢出、cpu 占用高、死锁、栈溢出、FullGC 频繁检测手段 - 总结与分享

作者:C++后台开发
  • 2022-12-17
    湖南
  • 本文字数:8539 字

    阅读完需:约 28 分钟

性能优化-内存泄漏、内存溢出、cpu占用高、死锁、栈溢出、FullGC频繁检测手段-总结与分享

介绍

什么是内存泄漏

含义:内层泄露是程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。(换言之,GC 回收不了这些不再被使用的对象,这些对象的生命周期太长) 危害:当应用程序长时间连续运行时,会导致严重的性能下降;OOM;偶尔会耗尽连接对象;可能导致频繁 GC。(大量 Full GC 发生也可推测系统可能发生内存溢出)

什么是内存溢出

含义:内层溢出通俗理解就是内存不够,程序要求的内存超出了系统所能分配的范围。 危害:内存溢出错误会导致处理数据的任务失败,甚至会引发平台崩溃等严重后果。

什么是 CPU 飙升

应用程序 CPU 使用率高,甚至超过 100%

什么是死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

什么是栈溢出

Java 里的 StackOverflowError。抛出这个错误表明应用程序因为深递归导致栈被耗尽了。每当 java 程序启动一个新的线程时,java 虚拟机会为他分配一个栈,java 栈以帧为单位保持线程运行状态;当线程调用一个方法是,jvm 压入一个新的栈帧到这个线程的栈中,只要这个方法还没返回,这个栈帧就存在。 如果方法的嵌套调用层次太多(如递归调用),随着 java 栈中的帧的增多,最终导致这个线程的栈中的所有栈帧的大小的总和大于-Xss 设置的值,而产生 StackOverflowError 溢出异常。

内存泄漏、内存溢出、CPU 飙升三者之间的关系

内存泄露可能会导致内存溢出。 内存溢出会抛出异常,内存泄露不会抛出异常,大多数时候程序看起来是正常运行的。 内存泄露的程序,JVM 频繁进行 FullGC 尝试释放内存空间,进而会导致 CPU 飙升 内存泄露过多,造成可回收内存不足,程序申请内存失败,结果就是内存溢出。

基本命令

首先了解各个基本命令、工具的使用,用它们去分析 JVM 参数,后文案例均是基于以下命令/工具解决。

top free df jps

# 先掌控全局,分别获取执行中的程序进程情况、显示内存的使用情况、查看磁盘剩余空间top free df 
# 获取java进程的PIDjps 或者ps -ef|grep java
复制代码

jinfo

可以打印一些当前 jvm 的各种参数,比如 jvm 的一些启动参数,jvm 中的一些属性 k-v 等。


jinfo [option] pid
复制代码

jmap(内存溢出解决方案)

这个命令可以查看 JVM 内存的一些相关数据


  1. 堆历史:可以看到当前 JVM 中所有已加载内的类创建对象的数量,占用内存等,可以导入文件中查看;


jmap -histo[:live] <pid> [ > ./xx.log]
复制代码


  1. 堆信息:可以查看 java 程序新生代和老年代的占比即使用情况。


jmap -heap <pid>
复制代码


  1. 堆转储:可以 dump 堆日志(保存堆现场),再使用 visualVM 查看 jmap 生成的堆转储快照。


jmap -dump:live,format=b,file=heap.hprof <pid>
复制代码


3.1 HeapDump 文件 HeapDump 文件是一个二进制文件,它保存了某一时刻 JVM 堆中对象使用情况(指定时刻 Java 堆栈的快照),是一种镜像文件。jhat 可分析 heapdump 文件,但是 jhat 命令在 JDK9、JDK10 中已经被删除,官方建议用 VisualVM 代替。 自动导出 dump 文件:通过 JVM 参数 HeapDumpOnOutOfMemoryError,可以让 JVM 在出现内存溢出时候 Dump 出当前的内存转储快照。


# 在IDE中VM option中添加了以下环境变量,程序OOM后生成文件,后缀名为hprof-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
复制代码


3.2 VisualVM 工具


VisualVM 能够监控线程状态、内存使用情况、CPU 使用情况

jstack(cpu 占用高解决方案)

这个命令可以查看线程的堆栈信息,定位到简单的死锁,常用的是通过 jstack 定位 CPU 高的问题,具体步骤是:


  1. 查看当前占用 cpu 最高的进程 pid(COMMAND 列);


top
复制代码


  1. 获取当前进程中所有线程占 CPU 的情况(也可 top -p 再按 H);


top -Hp <pid>
复制代码


  1. 将占用最高的 tid 转换为 16 进制


printf "%x\n" <tid>;
复制代码


  1. 查看占用最高的线程的堆栈状态。通过这个流程可以直接定位到哪个线程正在执行占用了大量的 cpu。其中 A10 就是过滤到关键词之后(A:after)10 行信息。


jstack <pid> | grep -A10 <16进制tid>
复制代码


  1. 前面的步骤已经获取了堆栈信息,我们也可以保存线程栈现场到指定文件里分析。


jstack <pid> > jstack.log 
复制代码

jstat(FullGC 频繁解决方案)

这个命令可以查看堆的各个部分的详细的使用情况,可以通过 jstat --help 查看帮助;


jstat -gc <pid> [1000 10]
复制代码


查看 gc 情况,每 1 秒打印一次总共打印 10 次(可选),可以查看各个带的使用总大小和使用大小对于 jvm 的优化就是要去优化它的 FullGC 次数,FullGC 越少越好,最好控制在 FullGC 几个小时甚至几天一次,具体看业务的情况。


jstat 参数说明:


S0C:第一个幸存区的大小(From Survivor区),以下几个容量的单位都是KB S1C:第二个幸存区的大小 (To Survivor区)S0U:第一个幸存区的使用大小S1U:第二个幸存区的使用大小 EC:伊甸园区的大小 (Eden区)EU:伊甸园区的使用大小OC:老年代大小 OU:老年代使用大小 MC:方法区大小(元空间)MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间,单位s FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间,单位s GCT:垃圾回收消耗总时间,单位s
复制代码


根据 jstat 查看出来的 gc 情况,我们可能需要以下几个主要指标:


各内存区域大小是否合理;观察Eden区的对象增长,如每秒有多少对象创建;每次YoungGC后有多少对象存活下来、有多少对象进入了老年代;YoungGC的耗时;FullGC触发频率及耗时;
复制代码

GC 分析


  • Minor GC/Young GC: 指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。

  • Major GC/Full GC: 一般会回收老年代 ,年轻代,方法区的垃圾,Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。


大量的对象在 Eden 区分配,YoungGC 之后存活的对象经过 S0、S1,大对象与长期存活的对象可能会到 Old 区。Eden 与 Survivor 区默认 8:1:1。


  • 触发 YoungGC 时机: Eden 区域满了

  • 触发 FullGC 时机:


1.System.gc() 显式触发 Full GC


2.老年代空间不足,晋升到老年代的对象大小大于老年代的可用内存。进入到老年代有多种情况:


1)Survivor 区的对象满足晋升到老年代的条件,即对象年龄达到了 MaxTenuringThreshold,这是一般情况;


2)根据对象动态年龄判断机制:在 YoungGC 后判断,Survivor 区中年龄 1 到 N 的对象大小是否超过 Survivor 的 50% ,这会让大于等于年龄 N 的对象放入老年代(-XX:TargetSurvivorRatio),如果此时老年代没有足够的空间来放置这些对象也会引起 Full GC;


3)堆中产生大对象超过阈值(-XX:PretenureSizeThreshold):很长的字符串或者数组在被创建后会直接进入老年代


3.元空间空间不足(-XX:MetaspaceSize)


4.老年代空间分配担保失败:在 YoungGC 前判断,YoungGC 后晋升到 Old 区的历史平均大小是否大于本次 Old 区剩余空间大小(XX:-HandlePromotionFailure)


更多 C++后台开发技术点知识内容包括 C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒体,音视频开发,Linux 内核,TCP/IP,协程,DPDK 多个高级知识点。

C/C++Linux服务器开发高级架构师/C++后台开发架构师免费学习地址

【文章福利】另外还整理一些C++后台开发架构师 相关学习资料,面试题,教学视频,以及学习路线图,免费分享有需要的可以点击领取



Arthas

官方文档:https://arthas.aliyun.com


使用略


  • 命令合并


下文部分命令是多个命令的合并,与拆开输入等效


# 例如,当程序名为StaticTest时java -jar arthas-boot.jar  `ps -ef|grep StaticTest |grep -v grep|awk '{print $2}'`jstat -gc `jps|grep StaticTest |grep -v grep|awk '{print $1}'` 500 1000jvisualvm --openpid `ps -ef|grep StaticTest |grep -v grep|awk '{print $2}'`
复制代码

内存泄漏案例分析

介绍

JAVA 在内存管理上有着独一无二的优势,它取消了指针,引入垃圾回收机制,由垃圾收集器(GC)来自动管理内存回收;GC 隐式地负责分配和释放内存,因此能够处理大多数内存泄漏问题。虽然 GC 有效地处理了相当一部分内存,但它不能保证对内存泄漏提供万无一失的解决方案。即使在认真的开发人员的应用程序中,内存泄漏仍然可能悄悄发生。所以我们有必要了解内存泄漏的潜在原因是什么,如何在运行时识别它们,以及如何在应用程序中处理它们。

案例一、通过静态字段的内存泄漏

第一种可能导致潜在内存泄漏的情况是大量使用静态变量。静态字段的生命周期与正在运行的应用程序一致。

import java.util.ArrayList;import java.util.List;
public class StaticTest { public static List<Double> list = new ArrayList<>(); //静态集合
public void populateList() { for (int i = 0; i < 10000000; i++) { list.add(Math.random()); } System.out.println("Debug Point 2"); }
public static void main(String[] args) throws InterruptedException { Thread.sleep(10000); System.out.println("Debug Point 1"); new StaticTest().populateList(); System.out.println("Debug Point 3"); Thread.sleep(10000); System.gc(); //有修改,在此处显示触发Full GC Thread.sleep(Integer.MAX_VALUE); }}
复制代码


如果我们分析这个程序执行期间的堆内存,那么我们将看到在调试点 1 和 2 之间,堆内存如预期的那样增加了。但是当我们在调试点 3 离开 populateList()方法时,堆内存还没有被垃圾回收。


然而,如果我们在上面的程序删除了关键字 static,那么它将给内存使用带来巨大的变化。

静态集合类持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

#查看jvm默认具体参数java -XX:+PrintCommandLineFlags -version java -XX:+PrintGCDetails -version
复制代码


比较关键字 static 带来堆空间大小回收的差异,两者差距接近 300M;10000000 个 Double 对象没有被回收,再根据对齐填充 8 的倍数,反推出来一个 Double 包装对象占用 32 字节的空间。


结论:我们需要密切关注静态变量的使用。静态的集合或大型对象在整个应用程序的生命周期中都被保留在内存中,这些可在其他地方使用的重要内存空间就被浪费掉了。


建议:尽量减少静态变量的使用;单例对象懒加载,需要用对象的时候再创建,而不是初始化时就创建好了对象。


案例一变种

import java.util.ArrayList;import java.util.List;
/** * JVM参数默认 * @author luke * @date 2022/11/11 */public class StaticTest { public static List<Integer> list = new ArrayList<>(100000000); public void populateList() throws InterruptedException { for (int i = 1; i <= 100000000; i++) { list.add(i); if(i % 100000 == 0){ Thread.sleep(1000); //System.out.println(list.size()); } } System.out.println("running......"); } public static void main(String[] args) throws InterruptedException { System.out.println("before......"); new StaticTest().populateList(); System.out.println("after......"); }}// 代码最终因内存泄露,回收不了可用空间而OOM。// Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
复制代码


先使用 jstat 命令统计垃圾回收,间隔时间 500 毫秒打印一次。根据一个 Integer 对象占用 16 字节,每 1 秒钟向 list 添加 100000 个整数,16*100000 字节大约是 1.5M,与统计图 eden 区平均每秒产生对象的大小接近。 再观察 jvisualvm 中堆内存的曲线图,每分钟平均生产 100M 对象,数据相符合。


使用命令如下:

jstat -gc `ps -ef|grep StaticTest |grep -v grep|awk '{print $2}'` 500 1000jvisualvm --openpid `ps -ef|grep StaticTest |grep -v grep|awk '{print $2}'`
复制代码


案例二、连接资源未关闭

import java.io.File;import java.io.IOException;
/** * @author luke * @date 2022/10/27 */public class FileTest { public static void main(String[] args) throws IOException { File f = new File("C:\\Users\\lzyxx\\Desktop\\a.txt"); System.out.println(f.exists()); System.out.println(f.isDirectory()); }}
复制代码


各种连接,如数据库连接、网络连接和 IO 连接等,如果不显性地关闭连接资源,将会造成大量的对象无法被回收,从而引起内存泄漏。

案例三、equals()和 hashCode()方法使用不当

import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;
/** * 线程池通过submit方式提交任务,会把Runnable封装成FutureTask。 * 直接导致了Runnable重写的toString方法在afterExecute统计的时候没有起到我们想要的作用(重写toString以用于统计任务数), * 最终导致几乎每一个任务(除非hashCode相同)就按照一类任务进行统计。所以这个metricsMap会越来越大,调用metrics接口的时候,会把该map转成一个字符返回。 * 改成execute方式提交任务即可 */public class GCTest { /** * 统计各类任务已经执行的数量, 此处为了简化代码,只用map来代替metrics统计 */ private static final Map<String, AtomicInteger> metricsMap = new ConcurrentHashMap<>();
public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) { @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); metricsMap.compute(r.toString(), (s, atomicInteger) -> new AtomicInteger(atomicInteger == null ? 1 : atomicInteger.incrementAndGet())); } }; /** * 线程池执行两类任务 */ for (int i = 0; i < 500; i++) { executor.submit(new SimpleRunnable()); // 错误方式 executor.submit(new SimpleRunnable2());// executor.execute(new SimpleRunnable()); // 正确方式// executor.execute(new SimpleRunnable2()); } executor.shutdown(); executor.awaitTermination(1, TimeUnit.DAYS); System.out.println(metricsMap); } static class SimpleRunnable implements Runnable{ @Override public void run() {} @Override public String toString(){ return this.getClass().getSimpleName(); } }
static class SimpleRunnable2 implements Runnable{ @Override public void run() {} @Override public String toString(){ return this.getClass().getSimpleName(); } }}
复制代码

案例四、ThreadLocal 的错误使用

如果任何类创建了 ThreadLocal 变量,但没有显式删除它,那么即使在 web 应用程序停止后,该对象的副本也将保留在工作线程中,从而防止对象被垃圾收集。


案例:内存的最大大小为 1m,while 循环每隔 100ms 申请 30kb 大小的空间

import java.lang.reflect.Field;
/** * jvm运行参数 -Xmx1m -Xms1m -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ * * 使用以下命令观察:jstat -gc `ps -ef|grep MemoryLeakExample |grep -v grep|awk '{print $2}'` 1000 1000 */public class MemoryLeakExample{
public static int i = 0;
public static void main(String[] args) throws Exception{ while (true){ ThreadLocal<Object> threadLocal = new ThreadLocal<>(); try { objectThreadLocal.set(new byte[10 * 1024]); printEntriesSize(); Thread.sleep(100); i++; }catch (Throwable e){ System.out.println(i); throw e; }finally { //threadLocal.remove(); // 正确使用 } } }
/** * 打印ThreadLocal.entry的个数 */ public static void printEntriesSize() throws NoSuchFieldException, IllegalAccessException{ Thread thread = Thread.currentThread(); Class<? extends Thread> aClass = thread.getClass(); Field threadLocals = aClass.getDeclaredField("threadLocals"); threadLocals.setAccessible(true); Object threadLocalMap = threadLocals.get(thread); Class<?> tlmClass = threadLocalMap.getClass(); Field entriesSize = tlmClass.getDeclaredField("size"); entriesSize.setAccessible(true); System.out.println(entriesSize.get(threadLocalMap)); }}
复制代码


为什么程序没有因可用空间越来越少而 oom,ThreadLocal.entry 的个数会周期性的由少变多? 解释:ThreadLocalMap 底层使用数组来保存元素,利用线性探测法解决哈希冲突,但是调用 ThreadLocal#set,遍历 Entry 数组过程中会清理 key 为 null 的 value,尽量保证不出现内存泄漏的情况。


  • 如何预防?


  1. 当我们不再使用 ThreadLocal 时,记得清理它们。ThreadLocals 提供了 remove()方法,该方法将删除此变量的当前线程值。

  2. 不要使用 ThreadLocal#set(null)以清除该值。它实际上不会清除该值,而是会查找与当前线程关联的 Map,并将键值对分别设置为当前线程和 null。

  3. 最好将 ThreadLocal 视为需要在 finally 块中关闭的资源。


try {    threadLocal.set(System.nanoTime());    //... further processing} finally {    threadLocal.remove();}
复制代码

案例五、缓存泄漏

内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,就很容易遗忘。


  • 如何预防?


使用 WeakHashMap 缓存对象,这个 map 的特点是当除了自身有对 key 的引用外,此 key 没有其他引用那么此 map 会自动丢弃此值

案例六、 内部类持有外部类

嵌套类分为两类:非静态类和静态类。非静态嵌套类称为内部类。声明为静态的嵌套类称为静态内部类。 按照嵌嵌套类的语法限定,非静态内部类(InnerClass)可以访问其封闭类(OuterClass)的成员,即使这些成员是私有的。而静态内部类没有权限访问 OuterClass 的成员(当然静态类成员除外)。


默认情况下,每个非静态内部类(InnerClass)都有对其包含类(OuterClass)的隐式引用。如果我们在应用程序中使用这个内部类对象,那么即使在我们的包含类对象超出范围之后,它也不会被垃圾收集。


  • 如何预防?


如果内部类不需要访问包含的类成员,请考虑将其转换为静态类。


public class OuterClass {    class InnerClass {    }    static class StaticClass {    }}
复制代码

cpu 占用高案例分析

CPU 占用飙升甚至超过 100%的原因分析:


  1. 内存消耗过大,导致 Full GC 次数过多


多个线程的 CPU 都超过了 100%,通过 jstack 命令可以看到这些线程主要是垃圾回收线程(VM Thread); 通过 jstat 命令监控 GC 情况,可以看到 Full GC 次数非常多,并且次数在不断增加。


  1. 代码中有大量消耗 CPU 的操作,导致 CPU 过高,系统运行缓慢


例如某些复杂算法,甚至算法 BUG,无限循环递归等等。jstack 命令可直接定位到代码行。


  1. 由于锁使用不当,导致死锁


死锁不会直接导致 cpu 资源占用过高,synchronize 和 AQS 中锁的设计是线程获取锁失败时,会主动挂起线程,而不会自旋循环检测锁是否被释放。 如果因为死锁,阻塞线程越来越多,内存占用也越来越高且无法释放,导致不停的 gc,会造成 CPU 占用飙升。


  1. 线程由于某种原因而进入 TIMED_WAITING、WAITING 状态

使用 synchronized 会让等待锁的线程处于 Blocked 状态;


使用 AQS 相关的锁则会让等待锁的线程处于 TIMED_WAITING、 WAITING 状态,因为底层基于 LockSupport;

内存溢出案例分析

一般来说内存溢出主要分为以下几类:


  • 堆溢出(java.lang.OutOfMemoryError: Java heap space) 最常见最复杂情况

  • 栈深度不够( java.lang.StackOverflowError) 需关注配置项 -Xss 大小

  • 栈线程数不够(java.lang.OutOfMemoryError: unable to create new native thread)

  • 元空间溢出(java.lang.OutOfMemoryError: Metaspace) 需关注配置项 -XX:MaxMetaspaceSize 大小、jstat 指标参数 MC、MU 如果发现元空间大小是持续上涨的,则需要检查代码是否存在大量的反射类加载、动态代理生成的类加载等导致。可以通过-XX:+TraceClassLoading -XX:+TraceClassUnloading 记录下类的加载和卸载情况,反推具体问题代码。


原文链接:性能优化-内存泄漏、内存溢出、cpu 占用高、死锁、栈溢出、FullGC 频繁检测手段-总结与分享 - Luke! - 博客园

用户头像

C/C++后台开发技术交流qun:720209036 2022-05-06 加入

还未添加个人简介

评论

发布
暂无评论
性能优化-内存泄漏、内存溢出、cpu占用高、死锁、栈溢出、FullGC频繁检测手段-总结与分享_性能优化_C++后台开发_InfoQ写作社区