我对JVM中GC机制的理解

前言

做为一名Java编程者,想要往高级进阶,内存管理每每是避不开的环节,而垃圾回收 如下简称GC(Garbage Collection)机制做为内存管理最重要的一个部分,是咱们必需要掌握的。今天就分享下我对 垃圾回收机制分代回收策略 的理解.java

目录


  • 1. 背景算法

  • 2. 两种回收机制编程

    • 2.1. 引用计数
    • 2.2. 可达性分析
  • 3. 回收算法缓存

    • 3.1. 标记清除算法
    • 3.2. 复制算法
    • 3.3. 标记压缩算法
  • 4. 分代回收策略bash

    • 4.1. 新生代
    • 4.2. 老生代
  • 5. 四大引用ide


1. 背景

通常来说,在咱们编程的过程当中是会不断的往内存中写入数据的,而这些数据用完了要及时从内存中清理,不然会引起OutOfMemory(内存溢出) ,因此每一个编程者都必须听从这一原则。据说(我也不懂C语言~)在C语言阶段,垃圾是须要编程者本身手动回收的,而咱们Javaer相对来讲就要幸福多了,由于JVM存在GC机制,也就是说JVM会帮咱们自动清理垃圾,但幸福也是有代价的,由于老是会有些垃圾对象阴差阳错的避开GC算法,这一现象也称之为内存泄漏,因此只有掌握了GC机制才能避免写出内存泄漏的程序。spa

2. 两种回收机制

2.1 引用计数

什么是引用计数呢?打个比方A a = new A(),代码中 A 对象被引用 a 所持有,此时引用计数就会 +1 ,若是 a 将引用置为 nulla = null此时对象 A 的引用计数就会变为 0 ,GC算法检测到 A 对象引用计数为 0 就会将其回收。很简单,但引用计数存在必定弊端线程

场景以下:3d

A a = new A();
B b = new B();
a.next = b;
b.next = a;
a = null;
b = null;
复制代码

执行完上述代码后 AB 对象会被回收吗?看似引用都已经置为 null ,但实际上 abnext 分别持有对方引用,造成了一种相互持有引用的局面,致使AB 即便成了垃圾对象且不能被回收。有些同窗可能会说,内存泄漏太容易看出来了, ab 置空前将各自的 next 置为空不就完了。嗯,这样说没错,可是在实际业务中面对庞大的业务逻辑内存泄漏是很难一眼看出的。因此JVM在后来摒弃了引用计数,采用了可达性分析。调试

2.2 可达性分析

可达性分析实际上是数学中的一个概念,在JVM中,会将一些特殊的引用做为 GcRoot ,若是经过 GcRoot 能够访达的对象不会被看成垃圾对象。换种方式说就是,一个对象被 GcRoot 直接 或 间接持有,那么该对象就不会被看成垃圾对象。用一张图表示大概就是这个样子:

图中A、B、C、D可以被 GcRoot 访达,因此不会被回收。E、F不能被 GcRoot 访达,因此会被标记为垃圾对象。最典型的是G、H,虽然说相互引用,但不能被 GcRoot 访达,因此也会被标记为垃圾对象。综上所述: 可达性分析 能够解决 引用计数 中 对象相互引用 不能被回收的问题。

什么类型的引用可做为 GcRoot 呢。 大概有以下四种:

  • 栈中局部变量
  • 方法区中静态变量
  • 方法区中常量
  • 本地方法栈JNI的引用对象

注意点

千万不要把引用和对象两个概念混淆,对象是实实在在存在于内存中的,而引用只是一个变量/常量并持有对象在内存中的地址指。

下面我来经过一些代码来验证几种 GcRoot

局部变量

笔者是用Android代码进行调试,不懂Android的同窗把onCreate视为main方法便可。

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        method();
    }

    private void method(){
        Log.i("test","method start");
        A a = new A();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.i("test","method end");
    }

    class A{
        @Override
        protected void finalize() throws Throwable {
            Log.i("test","finalize A");
        }
    }
}

复制代码

提示

  • 在java中一个对象被回收会调用其finalize方法
  • JVM中垃圾回收是在一个单独线程进行。为了更好的验证效果,在此加2000毫秒延时

打印结果以下:

17:58:57.526   method start
17:58:59.526   method end
17:58:59.591   finalize A
复制代码

method 方法执行时间是2000毫秒,对象Amethod 方法结束当即被回收。因此能够认定栈中局部变量可做为 GcRoot

本地方法区静态变量

public class MyApp extends Application {

    private static A a;
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("test","onCreate");
        a = new A();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a = null;
        Log.i("test","a = null");
    }
}
复制代码

打印结果以下:

18:12:35.988   a = new A()
18:12:38.028   a = null
18:12:38.096   finalize A
复制代码

建立一个 A 对象赋值给静态变量 a , 2000毫秒后将静态变量 a 置为空。经过日志能够看出对象 A 在静态变量 a 置空后被当即回收。因此能够认定静态变量可做为 GcRoot

方法区常量与静态变量验证过程彻底一致,关于native 验证过程比较复杂,感兴趣的同窗可自行验证。

验证成员变量是否可做为 GcRoot

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        A a = new A();
        B b = new B();
        a.b = b;
        a = null;
    }

    class A{
        B b;
        @Override
        protected void finalize() throws Throwable {
            Log.i("test","finalize A");
        }
    }

    class B{
        @Override
        protected void finalize() throws Throwable {
            Log.i("test","finalize B");
        }
    }
}
复制代码

打印结果以下:

13:14:58.999   finalize A
13:14:58.999   finalize B
复制代码

经过日志能够看出,A、B 两个对象都被回收。虽然 B 对象被 A 对象中的 b 引用所持有,但成员变量不能被做为 GcRoot, 因此B 对象不可达,进而会被看成垃圾。

3. 回收算法

上一小结描述了 GC 机制,但具体实现仍是要靠算法,下面我简单描述一下几种常见的 GC算法

3.1. 标记清除算法

获取全部的 GcRoot 遍历内存中全部的对象,若是能够被 GcRoot 就加个标记,剩下全部的对象都将视为垃圾被清除。

  • 优势:实现简单,执行效率高
  • 缺点:容易产生 内存碎片(可用内存分布比较分散),若是须要申请大块连续内存可能会频繁触发 GC

3.2. 复制算法

将内存分为两块,每次只是用其中一块。首先遍历全部对象,将可用对象复制到另外一块内存中,此时上一块内存可视为全是垃圾,清理后将新内存块置为当前可用。如此反复进行

  • 优势:解决了内存碎片的问题
  • 缺点:须要按顺序分配内存,可用内存变为原来的一半。

3.3. 标记压缩算法

获取全部的 GcRootGcRoot 开始从遍历内存中全部的对象,将可用对象压缩到另外一端,再将垃圾对象清除。实则是牺牲时间复杂度来下降空间复杂度

  • 优势:解决了标记清除的 内存碎片 ,也不须要复制算法中的 内存分块
  • 缺点:仍须要将对象进行移动,执行效率略低。

4. 分代回收策略

在JVM中 垃圾回收器 是很繁忙的,若是一个对象存活时间较长,避免重复 建立/回收垃圾回收器 进一步形成负担,能不能牺牲点内存把它缓存起来? 答案是确定的。JVM制定了 分代回收策略 为每一个对象设置生命周期 ,堆内存会划分不一样的区域,来存储各生命周期的对象。通常状况下对象的生命周期有 新生代、老年代、永久代(java 8已废弃)

4.1. 新生代

首先来看新生代内存结构示意图:

按照8:1:1将新生代内存分为 Eden、SurvivorA、SurvivorB

新生代内存工做流程:

  • 当一个对象刚被建立时会放到 Eden 区域,当 Eden 区域即将存满时作一次垃圾回收,将当前存活的对象复制到 SurvivorA ,随后将 Eden 清空
  • Eden 下一次存满时,再作一次垃圾回收,先将存活对象复制到 SurvivorB ,再把 EdenSurvivorA 全部对象进行回收,
  • Eden 再一次存满时,再作一次垃圾回收,将存活对象复制到 SurvivorA,再把 EdenSurvivorB对象进行回收。如此反复进行大概 15 次,将最终依旧存活的对象放入到老年代区域。

新生代工做流程与 复制算法 应用场景较为吻合,都是以复制为核心,因此会采用复制算法

4.2. 老年代

根据对 上一小节 咱们能够得知 当一个对象存活时间较久会被存入到 老年代 区域。 老年代 区即将被存满时会作一次垃圾回收,

因此 老年代 区域特色是存活对象多、垃圾对象少,采用标记压缩 算法时移动少、也不会产生内存碎片。因此老年代 区域能够选用 标记压缩 算法进一步提高效率。

5. 四大引用

在咱们开发程序的过程当中,避免不了会建立一些比较大的对象,好比Android中用于承载像素信息的Bitmap,使用稍有不当就会形成内存泄漏,若是存在大量相似对象对内存影响仍是蛮大的。

为了尽量避免上述状况的出现,JVM为咱们提供了四种对象引用方式:强引用、软引用、弱引用、虚引用 供咱们选择,下面我用一张表格来作一下类比

  • 假设如下所述对象可被 GcRoot 访达
引用类型 回收时机
强引用 毫不会被回收(默认)
软引用 内存不足时回收
弱引用 第一次触发GC时就会被回收
虚引用 随时都会被回收,不存在实际意义





参考文献:《Android 工程师进阶 34 讲》 第二讲

结语

文章从五个方面描述了 GC 机制。

  • GC 机制的诞生是为了提高开发者的效率
  • 可达性分析 解决的 引用计数 相互引用的问题
  • 不一样场景 运用不一样 GC 算法能够提高效率
  • 分代回收策略 进一步提高 GC 效率
  • 巧妙运用 四大引用 能够必定程度解决 内存泄漏
相关文章
相关标签/搜索