堆空间-概述

堆空间

概述

什么是堆空间?

  • 定义:虚拟机中 [最大的存储空间]{.red}

  • 作用:[存放所有被创建的对象和数组的空间]{.red}

    +++danger 对象一定被分配在堆空间中吗?

    逃逸分析

    +++

  • 特点:

    • [堆空间可以处于物理上并不连续的内存空间中]{.red}

      • 物理上不连续:操作系统采用了虚拟内存技术,所以分配给虚拟机的内存显然是不连续的

      • 逻辑上连续:虚拟机的使用者并不关心实际内存是怎么存储的,从表面上看来就是连续的

      • [如果创建的是大对象通常会要求使用连续的堆空间]{.red}

        :::info

        大对象:需要占用的堆内存非常大的对象,诸如数组对象 new byte[1024 * 1024 * 1024]

        :::

        堆空间实际占用内存
    • [堆空间中的对象的销毁不取决于方法是否结束,而是取决于垃圾回收机制]{.red}

    • [堆空间既存在垃圾回收机制,也存在 OutOfMemoryError 异常]{.red}

    • [堆空间是所有线程共享的区域:线程可以互相访问彼此创建的对象和数组]{.red}

    • [堆空间主要管理数据的存储,所以存储的是对象和数组的实际数据]{.red}

空间划分

  • 核心:[分代思想]{.red}

  • 虚拟机规范划分:新生代 + 老年代

    • 新生代:

      • 伊甸园区:TLAB(Thread Local Allocation Buffer) + 共享区域
      • 幸存者区:幸存者0区 + 幸存者1区
    • 老年代

    • 方法区:[逻辑上归属堆区,物理上并不属于堆区,甚至可以称为非堆区]{.red}

      :::info

      ① 类似于中国大陆和中国台湾的关系,逻辑上台湾归属于中国,但是目前中国大陆的政策无法管理台湾

      ② 方法区和堆区实际存储的内容完全不同,所以进行区分也是正常的

      :::

    堆空间划分
  • JDK 7:[方法区采用永久代实现:使用内存是虚拟机提供的内存]{.red}

  • JDK 8:[方法区采用元空间实现:使用的内存是操作系统提供的物理内存]{.red}

    方法区落地实现

    +++danger 为什么堆空间要采用分代思想进行管理?

    ① 显然堆空间即使不分代也是可以进行垃圾回收的

    ② 但是如果所有的对象都被杂乱无章地放在一起,每次执行垃圾回收的时候就需要挨个查找

    ③ 哪些是短生命周期的,可以回收,哪些是长生命周期的,不能回收,就相当于每次都要问一问老年代需不需要回收

    ④ 这样垃圾回收的性能显然就会下降,执行了太多没有意义的操作

    ⑤ 核心:优化垃圾回收机制的性能

    +++

空间大小

  • 堆空间大小设置

    • 特点:[堆空间大小可以是固定容量也可以是动态扩展的]{.red}

    • 固定容量:虚拟机默认采用的是动态扩展,固定容量需要自行设置

      +++info 如何将堆空间设置为固定容量?

      ① 只需要将堆空间的的初始容量和最大容量设置为相同的即可

      ② 再继续想想,为什么要将两者设置为相同?或者说为什么要使用固定容量?

      +++

    • 动态扩展

      • [默认初始化容量:物理内存 / 64]{.blue}

      • [默认最大容量:物理内存 / 4]{.blue}

      • 现状:大多数虚拟机都支持堆空间的动态扩展,[但是实际开发中都是采用固定容量]{.blue}

        // Runtime: 运行时数据区
        public static void main(String[] args)
        {
        // 初始内存:245MB
        long initialMemory = Runtime.getRuntime().totalMemory();
        // 最大内存:3625MB
        long maxMemory = Runtime.getRuntime().maxMemory();
        // 输出内存提示信息
        System.out.println("堆空间初始化内存: " + initialMemory / 1024 /1024 + "MB");
        System.out.println("堆空间最大内存: " + maxMemory / 1024 / 1024 + "MB");

        // 计算物理机的实际内存:15680MB 14500MB
        System.out.println("物理机实际内存: " + initialMemory / 1024 /1024 * 64 + "MB");
        System.out.println("物理机实际最大内存: " + maxMemory / 1024 /1024 * 4 + "MB");
        }
      • 问题:

        +++danger 为什么要将堆空间的初始化大小和最大容量设置为相同的呢?

        ① 核心:避免频繁扩展堆空间的大小,同时也避免在垃圾回收之后重新计算堆空间大小,从而提高虚拟机的执行性能

        ② 此前提到大多数的虚拟机都支持堆的动态扩展,并且需要使用垃圾回收机制

        ③ 如果允许堆采用动态扩展,那么每次垃圾回收之后都会重新计算堆空间大小

        ④ 因为对象被回收了,堆需要的空间就没那么大了,所以需要重新计算,这显然是浪费性能的

        +++

        +++ 为什么计算得到的物理机内存和实际内存不一样呢?

        ① Runtime.getRuntime().totalMemory(); 获取的堆内存是只有正在使用的内存

        ② 幸存者区只会将其中一个区用作存放对象,另外一个区空着不放对象,所以获得的内存每次就少了一部分幸存者区的内存

        +++

    • 设置堆空间大小命令

      • [-Xms Size 设置堆空间的初始化大小]{.blue}

        # 设置堆空间初始化大小
        # 等价于 -XX:InitialHeapSize
        #(实际上这个参数我并没有使用过,网上也没有提到如何使用,很奇怪,就连官方文档都没有提到)
        -Xms1m
      • [-Xmx Size 设置堆空间的最大容量]{.blue}:

        # 设置堆空间最大大小
        # 等价于 -XX:MaxHeapSize
        -Xmx1m
      • [-Xmn Size 设置堆空间中年轻代的大小]{.blue}:堆剩下的空间就是老年代占有的

        # 设置堆空间中年轻代的大小
        -Xmn1m
  • 新生代和老年代大小设置

    • 默认大小比例

      • [新生代 : 老年代 = 1 : 2]{.blue}

      • [伊甸园区 : 幸存者0区 : 幸存者1区 = 8 : 1 : 1]{.blue}

      • 细节:

        • 实际开发中通常使用默认的比例设置

        • 实际上虚拟机会采用 [自适应策略]{.red},新生代和老年代的比例是 1:2,但是新生代内部的比例却不是 8:1:1

          堆空间比例
    • 设置大小比例命令

      • [-XX:+UseAdaptiveSizePolicy:关闭虚拟机自适应策略]{.blue}

        :::info

        实际上这个命令并不会生效,自适应策略是无法关闭的,想要让比例为默认的8:1:1,直接设置就好

        :::

      • [-XX:NewRatio=ration:设置新生代和老年代的比例大小]{.blue}

      • [-XX:SurvivorRation=ratio:设置伊甸园区和幸存者区的比例大小]{.blue}

    +++ 为什么两个幸存者区的比例始终为 1:1 呢?

    涉及到垃圾回收算法:复制标记算法

    +++

堆空间溢出

:::primary

参考博客:来来来,聊聊7种内存泄露场景和13种解决方案

警告:jvm 内存泄露与内存溢出jvm内存泄漏这些博客的例子全部都是错的,但凡写的人自己试一试他写的代码就会发现错了

:::

  • 异常:OutOfMemoryError

  • 原因:[内存泄露或者内存溢出]{.red}

    • 内存泄露(Memory Leak)

      • 定义:[应该被垃圾回收机制清除的对象无法被顺利清除,造成空间浪费,最终可能造成 OOM 异常]{.red}

        内存泄露
      • 情况:(这部分笔记暂时存在问题,先了解什么是内存泄露就行)

        +++danger ① 静态集合类变量长期引用短生命周期的对象

        测试代码

        public static void main(String[] args) throws InterruptedException
        {
        // 线程休眠: 便于观察上升曲线
        Thread.sleep(10000);
        int count = 0;
        for (int i = 0; i < 1000000; i++)
        {
        Object object = new Object();
        // 链表不被销毁就会长期持有这个对象的引用
        list.add(object);
        // 短生命周期引用不再指向对象, 链表依然持有该对象的引用
        // 注意: 这里将引用置为空的目的是代表这个对象我们不想再使用了
        object = null;
        System.out.println("创建对象数量: " + ++count);
        }
        // 线程休眠: 便于观察上升曲线
        Thread.sleep(10000000);
        }

        测试图示

        内存泄露1

        测试堆内存结果

        堆内存分析1

        测试结论:

        • 每个添加进入链表的对象我们都不想继续使用,但是由于链表是静态变量,生命周期和类加载器一致,所以垃圾回收无法回收
        • 堆内存结果会再创建对象完成之后长时间保持水平线,意味着垃圾回收一直没有生效

        :::info

        疑惑:为什么将 static 关键字去掉之后依然没有触发垃圾回收机制?

        :::

        +++

        ② [未关闭不再使用的连接资源]{.red}:创建的数据库连接、流对象等连接资源再使用之后没有合理关闭,就会造成内存泄露

        ③ [未重写相应的 equals 和 hashCode 方法]{.red}

        ④ 外部类引用内部类对象:

        ⑤ 重写 finalize 方法不得当:

    • 内存溢出(Memory OverFlow)

      • 定义:[堆空间需要使用的内存超过虚拟机分配的内存,造成 OOM 异常]{.red}

      • 测试:

        测试代码

        public static void main(String[] args)
        {
        // 虚拟机参数设置: -Xms1m -Xmx1m
        ArrayList<Object> list = new ArrayList<>();
        // 注意:尽可能将对象数量设置多点,否则不会溢出
        for (int i = 0; i < 10000000; i++)
        {
        // 循环创建对象
        list.add(new Object());
        }
        }

        测试结果

        内存溢出
Author: Fuyusakaiori
Link: http://example.com/2021/09/23/jvm/runtime/heapspace/概述/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.