JVM学习(三):聊聊内存泄漏(memory leak)

news2024/10/6 1:35:04

一、什么是内存泄漏(memory leak)

可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。

用一句话来概括:对象还在被使用,但不需要。

二、如何理解内存泄漏

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。

例如:对象X引用对象Y,X的生命周期比Y的生命周期长;那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;

如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。

这种情况就可以叫做宽泛意义上的“内存泄漏”。

这里就要说一下内存泄漏与内存溢出的关系:

1.内存泄漏( memory leak )

申请了内存用完了不释放,比如一共有1024M的内存,分配了512M的内存一直不回收,那么可以用的内存只有512M 了,仿佛泄露掉了一部分;

通俗一点讲的话,内存泄漏就是【占着茅坑不拉shi】

2.内存溢出(out of memory)

申请内存时,没有足够的内存可以使用;

通俗一点儿讲,一个厕所就三个坑,有两个站着茅坑不走的(内存泄漏),剩下最后一个坑,厕所表示接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存泄漏变成内存溢出了。

可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。

三、Java中内存泄漏的八种情况

3.1 静态集合类

静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。

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

实例代码:

public class MemoryLeak {
    //静态变量,生命周期与JVM相同
    static List list = new ArrayList();

    public void oomTests() {
        //局部变量
        Object obj = new Object();
        list.add(obj);
    }
}

长生命周期的对象(list)持有短生命周期对象(obj)的引用,尽管obj不再使用,但是list持有它,导致obj不能被回收。这就造成了内存泄漏。

3.2 单例模式

单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

3.3 内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。

这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

3.4 各种连接,如数据库连接、网络连接和IO连接等

各种连接,如数据库连接、网络连接和IO连接等。

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。

否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

示例代码:

public static void main(String[] args) {
        try {
            Connection conn = null;
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("url", "", "");
        } catch (Exception throwable) {
            throwable.printStackTrace();
        } finally {
            //1.关闭结果集statement
            //2.关闭声明的对象resultSet
            //3.关闭连接connection
        }
    }

如果finally中的关闭操作不做,就会导致内存泄漏。

3.5 变量不合理的作用域

变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

public class UsingRandom {
    private String msg;
    public void receiveMsg(){
        //从网络中接受数据保存到msg中
        readFromNet();
        //把msg保存到数据库中
        saveDB();
    }
}

如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。

实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。

public class UsingRandom {
    public void receiveMsg(){
        //声明成局部变量
        private String msg;
        //从网络中接受数据保存到msg中
        readFromNet();
        //把msg保存到数据库中
        saveDB();
    }
}

还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。

public class UsingRandom {
    private String msg;
    public void receiveMsg(){
        //从网络中接受数据保存到msg中
        readFromNet();
        //把msg保存到数据库中
        saveDB();
        //回收msg的内存空间
        msg = null;
    }
}

3.6 改变哈希值

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。

否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。

比如下面这个程序就造成了内存泄漏:

public class ChangeHashCode {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");

        set.add(p1);
        set.add(p2);

        p1.name = "CC";//导致了内存的泄漏
        set.remove(p1); //删除失败

        System.out.println(set);

        set.add(new Person(1001, "CC"));
        System.out.println(set);

        set.add(new Person(1001, "AA"));
        System.out.println(set);

    }
}

class Person {
    int id;
    String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;

        Person person = (Person) o;

        if (id != person.id) return false;
        return Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

这也是String 为什么被设置成了不可变类型,我们可以放心地把String存入 HashSet,或者把String 当做HashMap的 key值;

当我们想把自己定义的类保存到散列表的时候,需要保证对象的hashCode不可变。

3.7 缓存泄露

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

比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。

对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。

3.8 监听器和回调

内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。

需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。

四、内存泄漏案例分析

4.1 案例代码

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    //入栈
    public void push(Object e) { 
        ensureCapacity();
        elements[size++] = e;
    }

    //出栈
    public Object pop() { 
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }
    
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

4.2 案例分析

上述程序并没有明显的逻辑错误,但是这段程序有一个内存泄漏,随着GC活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。

代码的主要问题在pop函数,假设这个栈一直增长,当进行大量的pop操作时,由于引用未进行置空,gc是不会释放的。

如果栈先增长,在收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些对象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用,这个内存泄露很隐蔽。

4.3 解决方法:

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

一旦引用过期,清空这些引用,将引用置空。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/169929.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最新光速配置VScode运行C/C++教程(W11适用)

文章目录1.下载MingW编译器:2.解压7z压缩文件方法3.VScode配置4.下面看看成果:1.下载MingW编译器: 下载地址 往下面拖找到上面这个东西-下载最新版本 2.解压7z压缩文件方法 7z解压程序官网 下它!相信这个时候在装的一般都是6…

【ubuntu | cuda】ubuntu22.04 安装cuda11.3方法

every blog every motto: You can do more than you think. 0. 前言 ubuntu22.04 安装cuda11.3方法记录 1. 正文 1.1 方法一: https://developer.nvidia.com/cuda-11.3.0-download-archive?target_osLinux&target_archx86_64&DistributionUbuntu&…

服务网格领域的百花齐放,是否存在一个更优解?

作者lingsamuel,API7.ai 云原生技术专家,Apache APISIX Committer。 作者林志煌,API7.ai 技术工程师,Apache APISIX contributor。 服务网格是一种技术架构,它用于管理微服务系统中各个服务之间的通信,旨在…

关于volatile和gcc 优化的的思考

volatile是一个特征修饰符(type specifier) volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。这是百度百科的介绍,那编译器是具体是怎么优化的呢。我们知道gcc 是有O0 O1 O…

torch.nn.MSELoss扒开看看它

目录官网介绍Toy默认参数定制参数预测问题-线性回归官网介绍 Toy 设X,Y∈Rnd\mathbf{X} , \mathbf{Y} \in \mathbf{R}^{n\times d}X,Y∈Rnd,假设其中X\mathbf{X}X是模型的输入,Y\mathbf{Y}Y是真实标签 默认参数 torch.nn.MSELoss(size_averageNone, r…

【Java IO流】字节流详解

文章目录1. IO 流概述2. IO 流分类3. 字节输出流4. 字节输入流5. 文件拷贝6. IO 流中的异常处理7. 总结Java编程基础教程系列1. IO 流概述 什么是 IO 流? IO 流是存取数据的解决方案,在计算机中数据存放在硬盘的文件中,如果程序需要使用这些…

Gitlab 项目上传到Maven仓库

Gitlab 项目上传到Maven仓库Gitlab 项目上传到Maven仓库1. 生成Deploy tokens2.项目工程AS的build.gradle配置Maven3. 拉取Maven库Gitlab 项目上传到Maven仓库 1. 生成Deploy tokens 项目地址-》Settings-》Repository-》Deploy tokens-》Expand-》输入Name-》Create deploy …

回收租赁商城系统功能拆解10讲-会员等级

回收租赁系统适用于物品回收、物品租赁、二手买卖交易等三大场景。 可以快速帮助企业搭建类似闲鱼回收/爱回收/爱租机/人人租等回收租赁商城。 回收租赁系统支持智能评估回收价格,后台调整最终回收价,用户同意回收后系统即刻放款,用户微信零…

数据分析面试题--SQL面试题

目录标题1,UNION和JOIN的区别2,连续登录问题3,窗口函数和普通聚合函数的区别4,窗口函数的基本用法5,序号函数:row_number(),rank(),dense_rank()的区别6,窗口函数涉及的一些其他函数7,次日留存率…

CAD软件中如何标注曲线长度?

在CAD设计过程中,如果想要用CAD标注图纸中某一曲线的长度该如何操作呢?今天小编就来给大家分享一个CAD标注曲线长度的小工具,有需要的小伙伴可以一起来看看哦! 此插件可以用多种方式标注多段线、样条曲线的长度,并可以…

Go语言笔记:UDP基础使用与广播

文章目录目的基础说明作为服务器使用作为客户端使用广播总结目的 UDP是比较基础常用的网络通讯方式,这篇文章将介绍Go语言中UDP基础使用的一些内容。 本文中使用 Packet Sender 工具进行测试,其官网地址如下: https://packetsender.com/ 基…

知识站点上关于Notes Domino话题几个问答

大家好,才是真的好。 今天周一我们继续不讲技术,介绍一下知识网站上关于Notes/Domino几个有趣问题的讨论。 国内的知识网站头把交椅是知乎,在中文界中是扛把子.不过在国外,最流行的知识网站叫做Quora,上面关于Notes/…

【Java】【系列篇】【Spring源码解析】【三】【体系】【PostProcessors体系】

PostProcessor英文翻译为后置处理器,在Spring体系里面主要针对的对象为Bean和BeanFactory.有着收尾或完善的作用。一、BeanPostProcessor分支 1.1、作用 在Bean对象在实例化和依赖注入完毕后,在显示调用初始化方法的前后添加我们自己的逻辑。注意是Bean…

【学习笔记】【Pytorch】十六、模型训练套路

【学习笔记】【Pytorch】十六、模型训练套路一、内容概述二、模型训练套路1.代码实现:CPU版本2.代码实现:优先GPU版本a3.代码实现:优先GPU版本b4.计算测试集上的正确率三、使用免费GPU训练模型一、内容概述 本内容主要是介绍一个完整的模型训…

信用评分分卡简介

背景 随着金融科技初创企业的兴起,过去 5 年中出现了许多新的消费信贷机构,与传统银行展开竞争。他们通常瞄准银行认为规模太小或因金融危机期间发生的后期损失而不得不削减贷款的细分市场。通俗的讲就是消费金融公司瞄准了银行的次贷市场。 这些新的消…

【C语言】文件操作修改通讯录(升级版本)可以存储数据

文件操作的内容,我们在上文已经学习了,那么如果有不明白的小伙伴请看这篇文章 【C语言】小王带您实现文件操作(简单图示讲解)_小王学代码的博客-CSDN博客 通讯录我们在之前也学习实现了静态、动态通讯录 【C语言】使用C语言实现静…

分享80个PHP源码,总有一款适合您

PHP源码 分享80个PHP源码,总有一款适合您 下面是文件的名字,我放了一些图片,文章里不是所有的图主要是放不下..., 80个PHP源码下载链接:https://pan.baidu.com/s/1yJ1aR6vt2kDjiVyqj0gPuw?pwdlfl9 提取码&#xff…

深信服EDR任意用户登录与命令执行漏洞

深信服EDR任意用户登录与命令执行漏洞1.深信服EDR简介2.深信服EDR漏洞2.1.后台任意用户登录漏洞2.1.1.漏洞描述2.1.2.影响版本2.1.3.漏洞复现2.2.任意命令执行漏洞2.2.1.漏洞描述2.2.2.影响版本2.2.3.漏洞复现2.2.3.1.构建URL2.2.3.2.效果1.深信服EDR简介 终端检测响应平台&…

C生万物 | 使用宏将一个整数的二进制位的奇数位和偶数位交换

👑作者主页:Fire_Cloud_1 🏠学习社区:烈火神盾 🔗专栏链接:万物之源——C 淋漓尽致——位运算✒题目分析 && 实现思路[位运算]1、获取这个整数的奇数位和偶数位2、使用移位运算使【奇变偶】【偶变奇…

如何通过限流算法防止系统过载

限流算法,顾名思义,就是指对流量进行控制的算法,因此也常被称为流控算法。 我们在日常生活中,就有很多限流的例子,比如地铁站在早高峰的时候,会利用围栏让乘客们有序排队,限制队伍行进的速度&am…