首页 电脑数码

性能优化-内存泄漏、内存溢出、cpu占用高、死锁、栈溢出详解

时间:2024-03-05 18:51:51  作者:Hu先生Linux后台开发

介绍

什么是内存泄漏

含义:内层泄露是程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。(换言之,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进程的PID
jps 或者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分析

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

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++后台开发架构师免费学习地址:C/C++Linux鏈嶅姟鍣ㄥ紑鍙�/鍚庡彴鏋舵瀯甯堛€愰浂澹版暀鑲层€�-瀛︿範瑙嗛鏁欑▼-鑵捐璇惧爞

【文章福利】另外还整理一些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 1000
jvisualvm --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 1000
jvisualvm --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;

内存溢出案例分析

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


相关文章