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

 2019-12-22 10:47  阅读(876)
文章分类:JVM

标签(空格分隔): 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类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表中一个引用。
    判断对象存活仅与引用有关,太过”狭隘“
    “食之无味,弃之可惜”——缓存
Type Purpose GC
Strong Objectobj=newObject() 只要强引用存在,就永远不会回收被引用的对象
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.  空间分配担保
点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 《深入理解Java虚拟机》——内存自动管理:内存分配与回收

相关推荐