Android进阶之路Android开发Android开发经验谈

一夜搞懂 | JVM GC&内存分配

2020-03-30  本文已影响0人  许朋友爱玩

前言

本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:

我的GIthub博客

学习导图

学习导图

一.为什么要学习GC&内存分配?

时代发展到现在,如今的内存动态分配与内存回收技术已经相当成熟,一切看似进入了“自动化”时代,不免发出疑问:"为啥我们还要了解垃圾收集和内存分配?"

答案很简单,当需要排查各种内存溢出/泄漏问题的时候,当垃圾收集成为系统达到更高并发量的瓶颈的时候,我们必须对"自动化"技术进行必要的监控和调节。

所以,我们要了解下GC&内存分配,为工作中或者是面试中实际的需要打好基础。

二.核心知识点归纳

2.1 对象存活判定算法

在了解对象存活的判定之前,我们先来了解下四种引用类型

  • 具有强引用的对象不会被GC
  • 即便内存空间不足,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具有强引用的对象
  • 只具有软引用的对象,会在内存空间不足的时候被GC,如果回收之后内存仍不足,才会抛出OOM异常
  • 软引用常用于描述有用但并非必需的对象,比如实现内存敏感的高速缓存
  • 只被弱引用关联的对象,无论当前内存是否足够都会被GC
  • 强度比软引用更弱,常用于描述非必需对象
  • 仅持有虚引用的对象,在任何时候都可能被GC(和弱引用一样)

  • 主要作用是为了垃圾收集器回收时收到一个系统通知(PhantomRefernece类实现虚引用)

  • 与弱引用的区别:不同之处在于弱引用的get方法,虚引用的get方法始终返回null,弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用

  • 必须和引用队列ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列

    (想要了解虚引用详细用法的读者,可以看下这篇文章:强软弱虚引用,只有体会过了,才能记住

2.1.1 引用计数算法

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

然而在主流的Java虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题,所以出现了另一种对象存活判定算法

//相互循环引用的DEMO
public class ReferenceCountingGC {
    public Object instance = null;
    
    private static final int _1MB = 1024 * 1024;
    
    /**
     *  这个成员属性的意义是占点内存,以便在GC日志中看清楚是否有回收过
     */ 
    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.1.2 可达性分析法

定义:通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的

可作为GC Roots的对象:

  • 虚拟机栈中引用的对象,主要是指栈帧中的本地变量表
  • 本地方法栈中Native方法引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • JVM内部的引用(基本数据类型对应的Class对象)
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映JVM内部情况的JMXBeanJVMTI中的注册的回调、本地代码缓存等
可达性分析法

Q:可达性分析算法中被判定不可达的对象真的被判『死刑』了吗?

A:在可达性分析算法中被判定不可达的对象还未真的判『死刑』,一共至少要经历两次标记过程:

判断对象是否有必要执行finalize()方法;若被判定为有必要执行finalize()方法,之后还会对对象再进行一次筛选,如果对象能在finalize()中重新与引用链上的任何一个对象建立关联,将被移除出“即将回收”的集合。

是否要回收的判断

引申:有关方法区GC,可分成两部分

  • 废弃常量与回收Java堆中的对象的GC很类似,即在任何地方都未被引用的常量会被GC

  • 无用的类

需满足以下三个条件才会被GC

A.该类所有的实例都已被回收,即Java堆中不存在该类的任何实例;

B.加载该类的ClassLoader已经被回收;

C.该类对应的java.lang.Class对象没在任何地方被引用,即无法在任何地方通过反射访问该类的方法。

2.2 垃圾收集算法

前文讲了JVM会回收哪些对象,下文笔者将探究JVM如何回收这些对象

2.2.1 分代收集理论

Q1:三个假说是什么?

在新生代上建立一个全局的数据结构(记忆集),将老年代划分成若干小块,标识出老年代哪一块内存存在跨代引用,Minor GC时,在跨代引用的内存里的对象才会加入到GC Roots进行扫描

Q2:垃圾收集器一致的设计原则

Q3:如何根据各个年代的特点选择算法呢?

这三种算法,笔者将在下文为您详细解析

2.2.2 复制算法

复制算法

分为一块较大的Eden空间和两块较小的Survivor空间,在HotSpot虚拟机中默认比例为8:1:1。每次使用Eden和一块Survivor,回收时将这两块中存活着的对象一次性地复制到另外一块Survivor上,再做清理。可见只有10%的内存会被“浪费”,倘若Survivor空间不足还需要依赖其他内存(老年代)进行分配担保

2.2.3 标记-清除算法

  • 『标记』和『清除』过程的效率不高
  • 空间碎片太多。『标记』『清除』之后会产生大量不连续的内存碎片,可能会导致后续需要分配较大对象时,因无法找到足够的连续内存而提前触发另一次GC,影响系统性能
标记-清除算法

2.2.4 标记-整理算法

标记-整理算法

2.2.5 和稀泥式

解决方法:大部分时间使用标记-清除算法,当内存空间的碎片程度影响到内存分配,再使用标记-整理算法进行收集

2.3 HotSpot算法实现&垃圾回收器

接下来介绍如何在HotSpot虚拟机上实现对象存活判定算法和垃圾收集算法,并保证虚拟机高效运行

2.3.1 枚举根节点

主流JVM使用的都是准确式GC,在执行系统停顿之后无需检查所有执行上下文和全局的引用位置,而是通过一些办法直接获取到存放对象引用的地方,在HotSpot中是通过一组称为OopMap的数据结构来实现的,完成类加载后会计算出对象某偏移量上某类型数据、JIT编译时会在特定的位置记录栈和寄存器中是引用的位置。这样GC在扫描时就可直接得知这些信息,并快速准确地完成GC Roots的枚举

2.3.2 安全点

上述“特定的位置”被称为安全点,即程序执行时并非在所有地方都停顿执行GC,只在到达安全点时才暂停,降低GC的空间成本

  • 抢先式中断:无需代码主动配合,在GC发生时把所有线程全部中断,若线程中断处不在安全点上就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件
  • 主动式中断:在GC要中断线程时不直接对线程操作,而是设置一个中断标志,让各个线程在执行时主动轮询它,当中断标志为真时就自己中断挂起

2.3.3 安全区域

安全点机制只能保证程序执行时,在不太长的时间内遇到可进入GC的安全点,但在程序不执行时(如线程处于SleepBlocked状态)线程无法响应JVM的中断请求,此时就需要安全区域来解决

2.3.4 JVM中七种回收器

垃圾回收器
序号 收集器 收集范围 算法 执行类型
1 Serial 新生代 复制 单线程
2 ParNew 新生代 复制 多线程并行
3 Parallel 新生代 复制 多线程并行
4 Serial Old 老年代 标记整理 单线程
5 CMS 老年代 标记清除 多线程并发
6 Parallel Old 老年代 标记整理 多线程
7 G1 全部 复制算法,标记-整理 多线程

注意并发和并行的概念:

GC中:

  • 并行:多条垃圾收集线程并行工作,而用户线程仍处于等待状态
  • 并发垃圾收集线程与用户线程一段时间内同时工作(交替执行)

在普通情景中:

  • 并行:多个程序在多个CPU上同时运行,任意一个时刻可以有很多个程序同时运行,互不干扰
  • 并发:多个程序在一个CPU上运行,CPU在多个程序之间快速切换,微观上不是同时运行,任意一个时刻只有一个程序在运行,但宏观上看起来就像多个程序同时运行一样,因为CPU切换速度非常快,时间片是64ms(每64ms切换一次,不同的操作系统有不同的时间),人类的反应速度是100ms,你还没反应过来,CPU已经切换了好几个程序了

2.4 内存分配和回收策略

对象的内存分配广义上是指在堆上分配,主要是在新生代Eden区上,如果启动了TLAB,将按线程优先在TLAB上分配,少数情况下也可能会分配在老年代中。分配细节还是取决于所使用的GC收集器组合以及虚拟机中与内存相关的参数的设置。以下介绍几条普遍的内存分配规则

  • 新生代GC:发生在新生代的垃圾收集动作。较频繁、回收速度也较快

解释:当大量对象在MinorGC后仍然存活的情况时,需要借助老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代,但前提是老年代本身还有容纳这些对象的剩余空间,由于在完成内存回收之前无法预知实际存活对象,只好取之前每次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,从而决定是否进行Full GC来让老年代腾出更多空间

三.课堂小测试

恭喜你!已经看完了前面的文章,相信你对JVM GC&内存分配已经有一定深度的了解,下面,进行一下课堂小测试,验证一下自己的学习成果吧!

Q1:垃圾回收算法你了解几种?请你简要分析一下,并说明其优缺点?

Q2:Java的引用机制有几种?请简要分析下,并说明其在Android中的应用场景有哪些?

Q3:安全点你了解过吗?安全区呢?请你介绍下安全区相对安全点的优势在哪里?

Q4:怎么判断对象是否存活呢?有几种方法?

上面问题的答案,在前文都提到过,如果还不能回答出来的话,建议回顾下前文


如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

本文参考链接:

上一篇 下一篇

猜你喜欢

热点阅读