三、JVM 内存分配机制


一、JVM 内存分配与回收


1. 对象优先在 Eden 区分配

  • 大多数情况下,对象在新生代中 Eden区 分配。
    Eden区 没有足够空间进行分配时,虚拟机将发起一次 MinorGC

  • MinorGC、YoungGC
  1. 发生在 新生代 的 垃圾收集动作。
  2. MinorGC 非常频繁,回收速度一般也比较快。
  • MajorGC、FullGC
  1. 一般回收 年轻代、老年代、方法区 的垃圾。
  2. MajorGC 的速度一般会比 MinorGC 的慢 10倍 以上。

# 打印堆大小。
-XX:+PrintGCDetails
/**
 * @author wy
 * describe 1、对象优先在 Eden 区分配
 * <p>
 * `-XX:PreternureSizeThreshold=1000000`直接晋升老年代的对象大小(设置了这个参数后,大于这个参数的对象直接在老年代进行分配)。
 * `-XX:+UseSerialGC`
 * `-XX:-HandlePromotionFailure`老年代空间分配担保机制。
 */
public class GCTest {


    /**
     * 1、添加`JVM参数`。
     * `-XX:+PrintGCDetails`打印堆大小。
     * <p>
     * jps
     * jmap -heap `pid`
     * <p>
     * File > Settings > Maven > Runner > VM Options
     * -XX:+PrintGCDetails -Dmaven.javadoc.skip=true -Dfile.encoding=UTF-8
     */
    @SuppressWarnings("all")
    public static void main(String[] args) {
        // 60000KB
        byte[] allocation1 = new byte[1024 * 60000];

//        byte[] allocation2 = new byte[1024 * 8000];
//        byte[] allocation3 = new byte[1024 * 1000];
//        byte[] allocation4 = new byte[1024 * 1000];
//        byte[] allocation5 = new byte[1024 * 1000];
//        byte[] allocation6 = new byte[1024 * 1000];
    }
}
  • 只给 allocation1 分配内存,几乎已经被分配完 Eden区 内存(即使程序什么也不做,新生代也会使用至少 几M 内存)。
  • 假如再给 allocation2 分配内存的时候,Eden区 内存几乎已经被分配完了。
  1. 当 Eden区 没有足够空间进行分配时,虚拟机将发起一次 MinorGC。
    GC 期间虚拟机又发现 allocation1 无法存入 Survior 空间,所以只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 FullGC。
  2. 执行 MinorGC 后,后面分配的对象如果能够存在 Eden区 的话,还是会在 Eden区 分配内存。

2. 大对象直接进入老年代

  • 大对象 就是 需要大量连续内存空间 的对象(比如:字符串、数组)。
    JVM 参数(-XX:PretenureSizeThreshold=0)可以设置大对象的大小。
  1. 默认值是 0,意味着 任何对象 都会先在 新生代 分配内存。
  2. 如果对象超过设置的大小,会直接进入 老年代,不会进入 年轻代。
  3. 这个参数只在 SerialParNew 两个收集器下有效。

  • 避免为 大对象分配内存 时的 复制操作,降低效率
    配置参数 大对象临界值
# 设置`JVM`参数,大对象(超过`1000000`B)直接进入老年代。
-XX:PretenureSizeThreshold=1000000
-XX:+UseSerialGC

3. 长期存活的对象将进入老年代

  • 既然 虚拟机 采用了 分代收集 的思想来管理内存。
    那么 内存回收 时就必须能识别,哪些对象应放在新生代,哪些对象应放在老年代中。
  • 为了做到这一点,虚拟机 给每个对象一个 对象年龄(Age)计数器
  1. 如果对象在 Eden区 出生,并经过第一次 MinorGC 后仍然能够存活。
    并且能被 Survivor区 容纳的话,将被移动到 Survivor区 空间中,并将 对象年龄 设为 1 岁
  2. 对象在 Survivor区 中每熬过一次 MinorGC,对象年龄 就增加 1 岁
    当它的年龄增加到一定程度(默认 15 岁),就会被晋升到 老年代 中。

  • 对象晋升到 老年代 的 年龄阈值
    配置参数(-XX:MaxTenuringThreshold=15)。
# 对象晋升到老年代的年龄阈值。
-XX:MaxTenuringThreshold=10

4. 对象动态年龄判断

  • 当前放对象的 Survivor区(放对象的那块 Survivor区)。
    一批对象的总大小 大于 这块 Survivor区域 内存大小 的 50%
  • 那么此时 大于等于 这批对象年龄最大值 的 其他对象,就可以 直接进入老年代 了。

  • 例如:Survivor区 里现在有一批对象(年龄1、年龄2、年龄n)的多个年龄对象。
  1. 当对象大小总和 超过了 Survivor区域 的 50%
  2. 此时就会把 年龄n 以上的对象都放入 老年代
  3. 这个规则是 希望那些可能 长期存活的对象,尽早进入 老年代。
  • 对象 动态年龄 判断机制,一般是在 MinorGC 之后触发的

5. MinorGC 后存活的对象 Survivor 区放不下

  • 这种情况会把 存活的对象,部分挪到 老年代,部分可能还会放在 Survivor区

6. 老年代空间分配担保机制

  • 年轻代 每次 MinorGC 之前,JVM 都会计算下 老年代剩余可用空间
  1. 如果这个 可用空间 小于 年轻代里现有的所有对象大小之和(包括垃圾对象)。
  2. 就会查看(-XX:-HandlePromotionFailure)参数是否设置了(JDK-8 默认就设置了)。
  3. 如果有这个参数,就会看看 老年代的可用内存大小,是否大于之前每一次 MinorGC 后进入老年代的对象的 平均大小
  4. 如果上一步结果是 小于 或者 之前说的参数没有设置。
    那么就会触发一次 FullGC,对 老年代 和 年轻代 一起回收一次垃圾。
  5. 如果回收完,还是没有 足够空间 存放新的对象,就会发生 "OOM"
  • 当然,如果 MinorGC 之后。
  1. 剩余存活的 需要挪动到 老年代 的对象大小,还是大于 老年代可用空间。
  2. 那么也会触发 FullGC
  3. FullGC 完之后,如果还是没用空间放 MinorGC 之后的存活对象,则也会发生 "OOM"
    在这里插入图片描述

7. Eden区 与 Survivor区 默认 8:1:1

  • 大量的对象 被分配在 Eden区,Eden区 满了后会触发 MinorGC
  1. 可能会有 99% 以上的对象,成为垃圾被回收掉。
  2. 剩余存活的对象,会被挪到为空的 那块 Survivor区。
  • 下一次 Eden区 满了后,又会触发 MinorGC。
  1. 把 Eden区 和 Survivor区 垃圾对象回收。
  2. 把剩余存活的对象 一次性挪动到 另外一块为空的 Survivor区。
  • 因为 新生代的对象 都是 朝生夕死的,存活时间很短。
  1. 所以 JVM 默认的 8:1:1 的比例是很合适的。
  2. 让 Eden区 尽量的大,Survivor区 够用即可

  • JVM 默认有这个参数(-XX:+UseAdaptiveSizePolicy),会导致这个比例自动变化。
  • 如果不想这个比例有变化,可以设置参数(-XX:-UseAdaptiveSizePolicy)。

二、如何判断对象可以被回收

  • 堆中几乎放着所有的对象实例。
  • 对堆垃圾回收前的第一步,就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

1. 引用计数法

  • 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1
  • 当引用失效,计数器就减 1
  • 任何时候计数器为 0 的对象,就是不可能再被使用的。

  • 这个方法实现简单,效率高
  1. 但是目前主流的虚拟机中,并没有选择这个算法来管理内存
  2. 其最主要的原因是,它很难解决对象之间 相互循环引用的问题。

  • 所谓对象之间的相互引用问题,如下面代码所示。
  1. 除了对象 testAtestN 相互引用着对方之外,这两个对象之间再无任何引用。
  2. 但是他们因为互相引用对方,导致它们的引用计数器都不为 0
  3. 于是引用计数算法,无法通知 GC 回收器回收他们。
/**
 * @author wy
 * describe 引用计数法
 */
public class ReferenceCountingGCTest {

    private ReferenceCountingGCTest instance = null;

    public static void main(String[] args) {
        ReferenceCountingGCTest testA = new ReferenceCountingGCTest();
        ReferenceCountingGCTest testB = new ReferenceCountingGCTest();
        testA.instance = testB;
        testB.instance = testA;
        testA = null;
        testB = null;
        /*
        testA 无法销毁
        testB 无法销毁
         */
    }
}

2. 可达性分析算法

  • 这个算法的基本思想。
  1. 就是通过一系列的称为 "GCRoots" 的对象作为起点。
  2. 从这些节点开始向下搜索引用的对象,找到的对象都标记为 非垃圾对象
  3. 其余未标记的对象都是 垃圾对象

  • "GCRoots" 根节点。
    线程栈的本地变量、静态变量、本地方法栈的变量 等等。
    在这里插入图片描述

3. 常见引用类型

  • Java 的 引用类型 一般分为四种。
  1. 强引用
  2. 软引用
  3. 弱引用。
  4. 虚引用。

3.1 强引用
  • 强引用:普通的变量引用。
/**
 * 强引用:普通的变量引用。
 */
public static Person person = new Person();

3.2 软引用
  • 软引用:将对象用 SoftReference 软引用类型的对象 包裹。
  1. 正常情况不会被回收。
  2. 但是 GC 做完后,发现 释放不出空间 存放新的对象。
  3. 则会把这些 软引用 的对象回收掉。

  • 软引用,可用来实现 内存敏感 的高速缓存

  • 软引用,在实际中有重要的应用。
    例如:浏览器的后退按钮。
  1. 按后退时,这个后退时显示的网页内容,是重新进行请求还是从缓存中取出呢?
    这就要看具体的实现策略了。
  2. 如果一个网页在浏览结束时,就进行内容的回收。
    则按后退查看前面浏览过的页面时,需要重新构建。
  3. 如果将浏览过的网页存储到内存中,会造成内存的大量浪费,甚至会造成内存溢出。
/**
 * 软引用
 */
public static SoftReference<Person> personSoftReference = new SoftReference<>(new Person());

3.3 弱引用
  • 弱引用:将对象用 WeakReference 弱引用类型的对象 包裹。
  • 弱引用 跟没引用差不多,GC 会直接回收掉,很少用。
/**
 * 弱引用
 */
public static WeakReference<Person> personWeakReference = new WeakReference<>(new Person());

3.4 虚引用
  • 虚引用:也称为 幽灵引用 或者 幻影引用。
  • 它是 最弱 的一种引用关系,几乎不用。

4. finalize() 方法最终判定对象是否存活

  • 即使在可达性分析算法中不可达的对象,也并非是 “非死不可” 的。
  1. 这时候它们暂时处于 “缓刑阶段”。
  2. 要真正宣告一个对象死亡,至少要经历再次标记过程。
  • 标记 的前提是 对象在进行可达性分析后,发现没有与 GCRoots 相连接的引用链

  • 第一次标记并进行一次筛选。
  1. 筛选的条件是 此对象 是否有必要执行 finalize() 方法。
  2. 当对象没有覆盖 finalize() 方法,对象将直接被回收。

  • 第二次标记。
  1. 如果 这个对象 覆盖了 finalize() 方法。
    finalize() 方法是对象脱逃死亡命运的 最后一次机会
  2. 如果对象要在 finalize() 中成功拯救自己。
    只要重新 与 引用链上 的任何的一个对象,建立关联即可。
  3. 譬如:把自己赋值给某个 类变量 或 对象的成员变量。
    那在第二次标记时,它将移除出 “即将回收” 的集合。
  4. 如果对象这时候还没逃脱,那基本上它就真的被回收了。
/**
 * -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\jvm.dump
 */
@Test
public void test2() throws InterruptedException {
    ArrayList<Object> list = new ArrayList<>();

    long i = 0;
    long j = 0;
    while (true) {
        list.add(new Person(String.valueOf(i++), 18));
        new Person(String.valueOf(j--), 20);
        Thread.sleep(10);
    }
    /*
    关闭资源,Person{name='-50', age=20},即将被回收
    关闭资源,Person{name='-49', age=20},即将被回收
    关闭资源,Person{name='-48', age=20},即将被回收
    关闭资源,Person{name='-47', age=20},即将被回收
     */
}

5. 如何判断一个类是无用的类

  • 类需要同时满足下面三个条件,才能算是 “无用的类”
  1. 该类 所有的实例 都已经被回收,也就是 Java 堆中不存在该类的 任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑士梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值