【JVM系列】Java中存放对象数据的大仓库——什么是堆?

【JVM系列】Java中存放对象数据的大仓库——什么是堆?

欢迎关注,分享更多原创技术内容~

微信公众号:ByteRaccoon、知乎:一只大狸花啊、稀土掘金:浣熊say

微信公众号海量Java、数字孪生、工业互联网电子书免费送~

什么是堆?

官方一点来说:Java堆内存(Java Heap)是Java虚拟机(JVM)中最大的一块内存区域之一,用于存储对象实例和数组,Java中的所有对象都在堆上分配内存。Java堆是线程共享的,所有线程都可以访问和操作堆上的对象。每个线程的局部变量表中存储的是对象引用,而实际的对象数据都存储在堆上。

假设我们是一个操作工人,每天都在操作台上用各种工具和原料来生产各种设备。那么,为了提升效率,一般我们的工作间和操作台的大小都是有限的,只会放少量的我们需要用的工具或者材料。但是,如果我们的工作是造一艘航母呢,需要大量的零件和工具,这些零件和工具不可能都放在我们的工作台上吧?所以,我们需要一个地方来存放大量的零件和工具 —— 仓库。那么我们该如何去仓库获取我们需要的零件和设备呢?最简单的方法是我们有一张表,上面记录了零件和设备在仓库的物理地址,当我们需要用到仓库中的零件和设备的时候,只需要根据这个地址去取就可以了。

在计算机中,操作台就像是CPU的内部寄存器,你作为那个操作工人就是ALU,而我们的仓库就是堆内存空间,可以帮助我们的程序存储大量的对象和数据。而仓库地址在计算机中就是引用或者说是指针,通过引用的方式来快速的获取对象和相关数据。所以说,堆实际上就是程序运行时存放对象或者数据的一个大仓库,为了解决Java虚拟机栈的上实时存储的数据量有限的问题,加快计算机的运行效率。

堆内存OOM(Out Of Memory)

什么是Out Of Memory?

就像现实世界的仓库大小是有限的一样,内存的堆大小也是有限的,当无法向仓库中存储新的零件和设备的时候,仓库肯定会告诉我们:“我已经满了,放不下新的东西了”。堆内存也一样,当堆内存满了之后,操作系统会抛出一个“Out Of Memory Error”来告诉程序员堆内存已经满了,你需要打扫仓库来腾出新的空间了。

无论在现实世界中“仓库满”了和在计算世界当中“堆满了”都是一件很严重的事情,在现实世界中如果仓库满了导致零件和设备放不进去,那么我们的工作人员也就没办法从仓库中拿到所需要的设备来进行工作,这样就会造成工期延误。而这个事情在计算机中会更加严重一点儿,如果真的出现OOM,程序在堆中无法分配新的对象就会造成程序的崩溃,是绝对需要避免的问题。

因此,在Java中,堆内存OOM(Out of Memory)指的是程序在运行过程中,无法再为新的对象分配足够的堆内存空间。这一般是因为程序运行时产生的对象太多,而垃圾回收无法及时释放不再使用的对象,最终导致堆内存溢出。下面是一些可能导致堆内存OOM的常见原因和解决方法:

问题 解决方法
内存泄漏 仔细检查代码,确保不再需要的对象及时释放引用。
对象生命周期过长 确保对象的生命周期与其实际需要的时间一致,避免长时间保持对不再需要的对象的引用。
创建过多的对象 优化代码,减少对象创建,及时释放不再使用的对象。
堆内存设置不足 调整 -Xmx 参数来提高堆内存的最大限制。
持久化对象过多 确保及时释放不再使用的资源,如数据库连接、文件句柄等。
使用内存分析工具 使用工具如VisualVM、MAT等进行内存分析,识别内存泄漏和对象过多的地方,并进行优化。

上面这些问题就是常见的Java堆的Out Of Memory的原因,我们需要尽量避免这些问题来保证Java堆内存不会被耗尽。

Java代码模拟Out Of Memory

下面是一个简单的Java代码来模拟Out Of Memory的现场,代码会不断地向list中添加1MB大小的字节数组,直到堆内存溢出,触发OutOfMemoryError

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

public class OutOfMemoryExample {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();

        try {
            while (true) {
                list.add(new byte[1024 * 1024]); // 每次添加1兆的字节数组
            }
        } catch (OutOfMemoryError e) {
            System.out.println("Caught OutOfMemoryError: " + e.getMessage());
        }
    }
}

编译:

javac OutOfMemoryExample.java

运行:

java -Xmx128m OutOfMemoryExample

为了Out Of Memory错误更加容易复现,这里将整堆空间大小设置为了128MB,因此,最多循环128次Java堆内存就会发生溢出。

如何解决Out Of Memory

那么如何解决Out Of Memory呢?对于仓库来说,其实办法无非两个:1. 及时清理不再需要的东西 2. 扩大仓库的空间。

首先,我们得及时的清理掉仓库里面已经不被需要的设备,这也就是Java中的垃圾回收的概念。Java HotSpot设计了各种各样的垃圾回收器来帮助我们在堆空间分配满之前清理掉不需要的对象,这个我们在后面的内容中再详细去说

其次,我们也可以通过设置更大的Java堆内存空间来避免OOM,当然这只是一种治标不治本的办法。

除此之外,我们还需要通过一些工具来观察和发现已经出现了OOM的程序。总的来说,以下是一些常见的OOM的解决方法:

解决方法 操作步骤或建议
内存分析工具 使用VisualVM、MAT、YourKit等工具,检查内存使用情况,识别内存泄漏和大对象。
调整堆内存参数 使用 -Xmx 和 -Xms 参数调整Java虚拟机的最大堆和初始堆大小。
检查代码中的内存泄漏 审查代码,确保资源使用完毕后及时释放,避免对象长时间保留在内存中。
垃圾回收调优 根据性能和响应时间需求选择合适的垃圾回收器,并调整垃圾回收参数。
使用对象池 对于频繁创建和销毁的对象,考虑使用对象池减轻垃圾收集压力。
升级Java版本 升级到最新的Java版本,可能修复一些内存管理方面的bug。
分析内存Dump 在OOM时生成堆转储,使用工具分析转储文件以找出内存占用较大的对象。
增加物理内存 考虑在服务器上增加物理内存,以提供更多可用内存。

上面这些方法可以帮助诊断和解决Java应用程序中的内存问题,根据具体情况,可能需要综合使用这些方法来获得最佳效果。

Java堆内存泄漏

在仓库的类比当中,我们的操作员需要通过一个表来查询设备和实际仓库的地址,我们把这个地址叫做指针或者引用。实际上,当我们的仓库管理员希望清理仓库的时候,也需要根据这个表来判断哪些设备还有用需要保留下来,哪些设备已经没用了,需要当成垃圾移出仓库。

而内存泄漏说的就是这么样一种情况,实际上这个对象已经没有用了,但是它的位置信息没有从表中删除,也就导致管理员在清扫垃圾的时候不会去清扫这一部分对象。长此以往,整个堆内存就会被这种实际已经没用,但是还没从表中删除的对象所占满,导致OOM。

官方一点:堆内存泄漏通常发生在程序中保持对对象引用,但这些对象不再被程序所需要时未被正确释放的情况,下面是一些可能导致堆内存泄漏的情况,我们通过 Java 代码进行说明:

  1. 集合对象未释放:
import java.util.ArrayList;
import java.util.List;

public class HeapMemoryLeakExample {

    private static List<MyObject> objectList = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            createAndRetainObject();
        }

        // 在程序的其他部分可能未正确释放 objectList 中的对象
        // 这可能导致堆内存泄漏
    }

    private static void createAndRetainObject() {
        MyObject myObject = new MyObject();
        objectList.add(myObject);
    }

    private static class MyObject {
        // 一些数据和逻辑
    }
}

在这个例子中,createAndRetainObject 方法将 MyObject 实例添加到 objectList 中,但程序的其他部分可能没有及时移除或释放 objectList 中的对象,而objectList集合对象的生命周期又是和整个main线程的生命周期相关联的,因此会造成内存泄漏。

  1. 静态变量持有对象引用:
public class HeapMemoryLeakExample {

    private static MyObject staticObject;

    public static void main(String[] args) {
        staticObject = new MyObject();

        // 在程序的其他部分可能未正确释放 staticObject
        // 这可能导致堆内存泄漏
    }

    private static class MyObject {
        // 一些数据和逻辑
    }
}

在这个例子中,staticObject 是一个静态变量,它持有对 MyObject 实例的引用,如果在程序的其他部分没有正确释放 staticObject,则可能导致堆内存泄漏。

  1. 监听器和回调未正确管理:
import java.util.ArrayList;
import java.util.List;

public class HeapMemoryLeakExample {

    private List<MyListener> listeners = new ArrayList<>();

    public static void main(String[] args) {
        HeapMemoryLeakExample example = new HeapMemoryLeakExample();
        example.setupListeners();

        // 在程序的其他部分可能未正确移除监听器
        // 这可能导致堆内存泄漏
    }

    private void setupListeners() {
        for (int i = 0; i < 1000; i++) {
            MyListener listener = new MyListener();
            listeners.add(listener);
        }
    }

    private static class MyListener {
        // 一些数据和逻辑
    }
}

在这个例子中,HeapMemoryLeakExample 类维护了一个监听器列表,但如果在程序的其他部分没有正确移除监听器,这可能导致堆内存泄漏。

  1. 未关闭资源:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class HeapMemoryLeakExample {

    private static String content;

    public static void main(String[] args) {
        readAndRetainFileContent();

        // 在程序的其他部分可能未正确关闭文件资源
        // 这可能导致堆内存泄漏
    }

    private static void readAndRetainFileContent() {
        try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
            StringBuilder builder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line);
            }
            content = builder.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,文件资源通过 BufferedReader 读取,但如果在程序的其他部分没有正确关闭文件资源,可能导致堆内存泄漏。

  1. 缓存对象未及时清理:
import java.util.HashMap;
import java.util.Map;

public class HeapMemoryLeakExample {

    private static Map&lt;String, MyObject&gt; objectCache = new HashMap&lt;&gt;();

    public static void main(String[] args) {
        // 缓存对象
        cacheObjects();

        // 在程序的其他部分可能未正确清理缓存
        // 这可能导致堆内存泄漏
    }

    private static void cacheObjects() {
        for (int i = 0; i &lt; 1000; i++) {
            MyObject myObject = new MyObject();
            objectCache.put("key" + i, myObject);
        }
    }

    private static class MyObject {
        // 一些数据和逻辑
    }
}

在这个例子中,对象被缓存在 objectCache 中,但如果在程序的其他部分没有正确清理缓存,可能导致堆内存泄漏。

总结

Java中的堆内存我们可以看作一个大的仓库,用来存放程序运行当中所需要的大量的数据和对象,这些数据和对象通过一个表格来记录它们在仓库中的实际地址,这个地址就是“引用”或者“指针”。

和仓库一样,堆内存也可能被占满,导致新分配的对象无法在堆中分配内存,从而导致程序错误,即Out Of Memory。垃圾回收器则是在仓库中不断的去发现和清扫哪些不再被需要的堆空间,以方便程序后续继续分配新的对象到仓库中。

有那么一些对象,其地址还被记录在引用表格当中,但是其实际上已经没有被程序所使用了,这就造成了所谓的“内存泄漏”,在Java代码中需要尽量避免可能造成堆内存泄漏的情况。

总之,这里我们简单介绍了下什么是堆、堆内存溢出和内存泄漏的相关内容,更多的关于堆内存的分代模型和垃圾回收器,可以参考后续的内容。


原文始发于微信公众号(ByteRaccoon):【JVM系列】Java中存放对象数据的大仓库——什么是堆?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/186426.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!