内存泄露排查流程

目录

视频教程链接:

JVM内存图

一、内存泄露场景

举例内存泄漏

静态集合类

饿汉式单例模式

非静态内部类

实例变量作用域不合理

隐式内存泄漏

不关闭资源引发内存泄漏

改变对象哈希值引发内存泄漏

缓存引发内存泄漏

使用 ThreadLocal 造成内存泄露

二、创建内存泄露案例

三、排查流程


视频教程链接:

一、系统内存泄露该怎么处理-闲聊_哔哩哔哩_bilibili

JVM内存图

一、内存泄露场景

什么是内存泄漏

VM在运行时会存在大量的对象,一部分对象是长久使用的,一部分对象只会短暂使用

JVM会通过可达性分析算法和一些条件判断对象是否再使用,当对象不再使用时,通过GC将这些对象进行回收,避免资源被用尽

内存泄漏:当不再需要使用的对象,因为不正确使用时,可能导致GC无法回收这些对象

举例内存泄漏

对象生命周期变长引发内存泄漏

静态集合类

public class StaticClass {
    private static final List<Object> list = new ArrayList<>();

    /**
     * 尽管这个局部变量Object生命周期非常短
     * 但是它被生命周期非常长的静态列表引用
     * 所以不会被GC回收 发生内存溢出
     */
    public void addObject(){
        Object o = new Object();
        list.add(o);
    }
}

生命周期基本与JVM一样长

静态集合引用局部对象,使得局部对象生命周期变长,发生内存泄漏

饿汉式单例模式

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){
        if (INSTANCE!=null){
            throw new RuntimeException("not create instance");
        }
    }

    public static Singleton getInstance(){
        return INSTANCE;
    }
}

饿汉式的单例模式也是被静态变量引用,即时不需要使用这个单例对象,GC也不会回收

非静态内部类

非静态内部类会有一个指针指向外部类

public class InnerClassTest {

    class InnerClass {

    }

    public InnerClass getInnerInstance() {
        return this.new InnerClass();
    }

    public static void main(String[] args) {
        InnerClass innerInstance = null;

        {
            InnerClassTest innerClassTest = new InnerClassTest();
            innerInstance = innerClassTest.getInnerInstance();
            System.out.println("===================外部实例对象内存布局==========================");
            System.out.println(ClassLayout.parseInstance(innerClassTest).toPrintable());

            System.out.println("===================内部实例对象内存布局===========================");
            System.out.println(ClassLayout.parseInstance(innerInstance).toPrintable());
        }

        //省略很多代码.....
    }
}

当调用外部类实例方法通过外部实例对象返回一个内部实例对象时(调用代码中的getInnerInstance方法)

外部实例对象不需要使用了,但内部实例对象被长期使用,会导致这个外部实例对象生命周期变长

因为内部实例对象隐藏了一个指针指向(引用)创建它的外部实例对象

实例变量作用域不合理

如果只需要一个变量作为局部变量,在方法结束就不使用它了,但是把他设置为实例变量,此时如果该类的实例对象生命周期很长也会导致该变量无法回收发生内存泄漏(因为实例对象引用了它)变量作用域设置的不合理会导致内存泄漏

隐式内存泄漏

动态数组ArrayList中remove操作会改变size的同时将删除位置置空,从而不再引用元素,避免内存泄漏

移除指定位置元素,该位置后面的元素全部往前移动一位。最后一个元素设置为null(让gc清除掉)

不关闭资源引发内存泄漏

各种连接: 数据库连接、网络连接、IO连接在使用后忘记关闭,GC无法回收它们,会发生内存泄漏

所以使用连接时要使用 try-with-resource 自动关闭连接

改变对象哈希值引发内存泄漏

一般认为对象逻辑相等,只要对象关键域相等即可

一个对象加入到散列表是通过计算该对象的哈希值,通过哈希算法得到放入到散列表哪个索引中

如果将对象存入散列表后,修改了该对象的关键域,就会改变对象哈希值,导致后续要在散列表中删除该对象,会找错索引从而找不到该对象导致删除失败(极小概率找得到)

public class HashCodeTest {
    /**
     * 假设该对象实例变量a,d是关键域
     * a,d分别相等的对象逻辑相等
     */
    private int a;
    private double d;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        HashCodeTest that = (HashCodeTest) o;
        return a == that.a &&
                Double.compare(that.d, d) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(a, d);
    }

    public HashCodeTest(int a, double d) {
        this.a = a;
        this.d = d;
    }

    public HashCodeTest() {
    }

    @Override
    public String toString() {
        return "HashCodeTest{" +
                "a=" + a +
                ", d=" + d +
                '}';
    }

    public static void main(String[] args) {
        HashMap<HashCodeTest, Integer> map = new HashMap<>();
        HashCodeTest h1 = new HashCodeTest(1, 1.5);
        map.put(h1, 100);
        map.put(new HashCodeTest(2, 2.5), 200);

        //修改关键域 导致改变哈希值
        h1.a=100;

        System.out.println(map.remove(h1));//null

        Set<Map.Entry<HashCodeTest, Integer>> entrySet = map.entrySet();
        for (Map.Entry<HashCodeTest, Integer> entry : entrySet) {
            System.out.println(entry);
        }
        //HashCodeTest{a=100, d=1.5}=100
        //HashCodeTest{a=2, d=2.5}=200
    }
}

缓存引发内存泄漏

当缓存充当散列表的Key时,如果不再使用该缓存,就要手动在散列表中删除,否则会发生内存泄漏

如果使用的是WeakHashMap,它内部的Entry是弱引用,当它的Key不再使用时,下次垃圾回收就会回收掉,不会发生内存泄漏

public class CacheTest {
    private static Map<String, String> weakHashMap = new WeakHashMap<>();
    private static  Map<String, String> map = new HashMap<>();
    public static void main(String[] args) {
        //模拟要缓存的对象
        String s1 = new String("O1");
        String s2 = new String("O2");
        weakHashMap.put(s1,"S1");
        map.put(s2,"S2");

        //模拟不再使用缓存
        s1=null;
        s2=null;

        //垃圾回收WeakHashMap中存的弱引用
        System.gc();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //遍历各个散列表
        System.out.println("============HashMap===========");
        traverseMaps(map);
        System.out.println();
        System.out.println("============WeakHashMap===========");
        traverseMaps(weakHashMap);
    }

    private static void traverseMaps(Map<String, String> map){
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println(entry);
        }
    }
}

使用 ThreadLocal 造成内存泄露

ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定,从而实现线程的安全,但是使用不当,就会引起内存泄露。

一旦线程不在存在,ThreadLocal 就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾回收器回收。所以使用到 ThreadLocal 来保留线程池中线程的变量副本时,ThreadLocal 没有显示的删除时,就会一直保留在内存中,不会被垃圾回收。

解决办法是不在使用 ThreadLocal 时,调用 remove() 方法,该方法删除了此变量的当前线程值。不要使用 ThreadLocal.set(null),它只是查找与当前线程关联的 Map 并将键值对设置为当前线程为 null。

try {
    threadLocal.set(System.nanoTime());
}
finally {
    threadLocal.remove();
}

finalize方法

之所以会造成内存泄漏,是因为在垃圾回收的时候,如果重写了finalize方法而且该对象的finalize方法没有被执行过,但是进入队列之后一直没被调用就会一直占用内存空间

二、创建内存泄露案例

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

@RestController
@RequestMapping(value = "/demo")
@Slf4j
public class DemoController {
    private static List<Object> list = new LinkedList<>();
    @GetMapping("/leak")
    public String leak(){
        for (int index =0;index<1000; index++){
            UserInfo userInfo = new UserInfo();
            list.add(userInfo);
        }
        return "success";
    }
    
    @Data
    class UserInfo{
        private String username;
    }
}

后台启动命令

nohup java -Dserver.port=8080 -jar demo.jar   >> log.txt 

三、排查流程

动态监控内存

# 每隔3秒执行一次
vmstat 3 

排查发现出现内容泄露

排查jvm内存分配情况(内存分配合理)

jmap -heap 进程id

排查是否存在异常未清除类

jmap -histo:live  <pid>|sort -k 2 -g -r|less

对象实例的数量。

对象的占用空间大小(以字节为单位)。

对象类的全名。

排查回收对象情况

jmap -finalizerinfo 进程id

下载堆内存

jmap -dump:live,format=b,file=myjmapfile.hprof 进程id

堆分析工具

虚拟机堆转储快照分析工具

使用jdk的 jvisualvm工具(建议采用):

  1. 打开命令行或终端窗口。
  2. 输入 

jvisualvm 并按 Enter 键启动 

  1. 在 jvisualvm 中,选择 "File" -> "Load...",然后选择要打开的 HPROF 文件。
  2. 点击 "Open" 或 "OK" 来加载并分析 HPROF 文件。

使用eclipse memory analyzer分析内存类依赖关系

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue Devtools 是 Vue.js 官方提供的一个调试工具,用于在开发过程中对 Vue.js 应用进行调试和监测。当遇到内存泄露问题时,可以通过以下步骤使用 Vue Devtools 进行排查: 1. 确保 Vue Devtools 安装和启动:在浏览器安装 Vue Devtools 插件,然后在项目中确保已正确引入 Vue Devtools 的开发版。 2. 打开 Vue Devtools:在浏览器开发者工具中,切换到 Vue 选项卡,确保 Vue Devtools 已打开。 3. 观察组件状态和生命周期:在 Vue Devtools 中,可以查看当前所有渲染的组件实例。观察组件的状态和生命周期,查看是否存在异常。 4. 检查内存占用情况:在 Vue Devtools 的性能面板中,可以查看当前应用的内存占用。如果内存占用不断增长,可能存在内存泄露问题。 5. 分析垃圾回收:在 Vue Devtools 的性能面板中,可以查看垃圾回收的情况。如果发现垃圾回收频率较低,可能意味着存在内存泄露。 6. 使用快照:在 Vue Devtools 的快照面板中,可以记录当前应用的状态,并随时保存和比对快照。通过比对快照,可以找出内存泄露的具体变化。 7. 分析组件树:在 Vue Devtools 的组件树面板中,可以查看整个组件树的结构。通过观察组件树的变化,找出可能导致内存泄露的组件。 8. 排查事件监听:使用 Vue Devtools 的事件监听面板,检查是否存在未正确注销的事件监听器。未移除的事件监听器可能导致组件无法被销毁,从而引发内存泄露。 9. 使用 Heap Snapshot:如果通过以上方法仍无法确定内存泄露的原因,可以使用 Heap Snapshot 功能。Heap Snapshot 可以在内存占用高峰时,记录当前内存中的对象实例,从而帮助进一步分析内存泄露的源头。 通过以上步骤,可以使用 Vue Devtools 进行内存泄露问题的排查和分析,找到导致内存泄露的原因,并进行相应的优化处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值