《深入理解Java虚拟机》——内存自动管理:内存分配与回收

标签(空格分隔): JVM

先讲讲自动内存回收——GC

GC的三大问题——What & When & How

  1. 哪些内存需要回收?
  2. 何时回收?
  3. 如何回收?

什么是内存溢出,内存泄露?

GC的三大问题(一)——哪些内存需要回收?

回收区域:Java堆和方法区(对象的创建是动态的,不确定性)(线程私有区域不考虑)
回收对象:“死亡的”对象实例

如何定义“死亡”(Java堆)

1. 引用计数法

引用计数器
简单,效率高,不可靠
无法解决“对象之间循环引用”
/*示例代码*/
public class ReferenceCountingGC {
    public ReferenceCountingGC instance = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();

        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        System.GC();
    }
}

2. 可达性分析

GC Roots + 引用链
被Java采用
在Java中,可作为GC Roots的对象有:
    1.
    2.
    3.
    4.

3. 引用类型:强软弱虚

Java中引用的定义:
    如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表中一个引用。
判断对象存活仅与引用有关,太过”狭隘“
“食之无味,弃之可惜”——缓存
TypePurposeGC
StrongObject obj = new Object()只要强引用存在,就永远不会回收被引用的对象
Soft有用但非必需发生内存溢出异常之前,将此类对象列进回收范围进行第二次回收
Weak非必需对象下一次GC被回收
Phanton无实际用途唯一的作用是在该对象被回收时,给出一个系统通知

如何定义“死亡”(方法区)

废弃常量

1. 常量池中的常量不被引用

无用的类

1. 该类的所有实例都已经被回收——Java堆中不存在该类的实例 
2. 加载该类的ClassLoader已经被回收
3. 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

GC的三大问题(二)——何时回收?

两个阶段:
    1.第一次标记
    2.第二次标记

第一次标记:

1. 可达性分析:若某一对象“不可达”,则标记

2. 筛选: 若对象被选中,则加入F-Queue队列
    当对象未覆盖finalize()方法或者此方法已经被虚拟机调用过,则不被选中

第二次标记:

3. GC对F-Queue队列中的对象执行第二次小规模标记
    对象在finalize()中实现“自我拯救”,退出F-Queue队列
4. 虚拟机自动建立”低优先级“的Finalizer线程执行回收动作——调用对象的finalize()方法

“自我拯救”????

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isLive() {
        System.out.println("I am still alive!");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        SAVE_HOOK = this;
    }

    public static void main(String[] args) {
        SAVE_HOOK = new FinalizeEscapeGC();

        /* 实现自我拯救 */
        SAVE_HOOK = null;
        System.GC();

        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            isLive();
        } else {
            System.out.println("I'm dead!");
        }


        /* finalize()只能执行一次 。不会被回收???? */
        SAVE_HOOK = null;
        System.GC();

        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            isLive();
        } else {
            System.out.println("I'm dead!");
        }
    }
}

finalize()

Forget it!

GC的三大问题(三)——如何回收?

GC算法

1. Mark-Sweep:效率低 + 内存碎片多
    适合对象存活率较高的情况,适合回收老年代

2. Copying:把活着的对象复制到另一块内存空间,再清理已使用的那块;
    适合对象存活率较低的情况,被主流商业虚拟机采用回收新生代。(“朝生夕死”)
    Eden:Survivor:Survivor = 8:1:1,每次新生代可用内存空间占整个新生代容量的90%
    老年代分配担保(Handle Promotion)

3. Mark-Compact:让存活的对象向内存的一端移动,直接清理掉端边界以外的内存;
    适合对象存活率较高的情况,适合回收老年代

4. Generational Collection
    “因地制宜”   

Hotpot的GC算法实现

我们始终关注算法的执行效率!

枚举根节点

安全点

安全区域

垃圾收集器(了解)

GC日志(了解)

内存分配策略

1. 大多数情况下,对象在新生代的Eden区上分配。当Eden区上没有足够空间时,虚拟机发起一次 Minor GC(Copying)

2. 大对象直接进入老年代
    需要大量连续内存空间的Java对象,例如很长的字符串和byte[4 * _1MB]
    "朝生夕死"的“短命大对象”是JVM最不想看到的,写程序也应尽量避免
    经常出现大对象导致内存即使仍有不少空间,也需要提前触发GC以获得足够多的连续空间

3. 长期存活的对象将进入老年代
    对象年龄计数器。
    如果对象在Eden区出生,经过第一次Minor GC后仍然存活,并且能被Survivor区容纳,将被移动到Survivor空间中,对象年龄变为1.
    接下来,对象在Survivor区每“熬过”一次Minor GC,年龄加一,到达阈值(15),进入老年代

4. 动态对象年龄判定
    如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年区,无需达到“阈值年龄”!

5.  空间分配担保
    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/Reese1995/article/details/77945725
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞