GC机制,你真的了解吗?

前言

GC(Garbage Collection)相信是每一个程序猿(媛)都熟知的了。作为一个Android开发者,无疑我们是幸福的,因为我们不用像C语言那样还需要手动进行垃圾回收,但同时我们又是不幸的,由于android市场的碎片化,各个型号的手机迥然不同,适配起来相当麻烦,其中最需要避免的就是Out Of Memory了。那么深入的了解GC机制就是每一个android开发者的必修课。

 

JVM运行时的内存分配机制

内存分配图

JVM的内存分配可以通过上图来说明,程序运行时会将所申请的内存分为五大块,而不仅仅是我们通常所说的堆内存和栈内存。它们分别是一个程序计数器、一个方法区、一个堆内存和两个栈内存(虚拟机栈和本地方法栈)。

程序计数器:可能比较陌生,但它的概念和用途却是很好理解的,程序计数器是线程私有的,生命周期随线程的创建而创建,随线程消亡而消亡。主要用于记录当前线程执行的位置。通常我们所用的线程切换、循环、跳转等都需要依赖这个计数器来完成。

方法区:主要用于存储已经被JVM加载的常量、静态变量等,它是能够被线程共享的。

本地方法栈:主要针对的是native方法,因此一般在JNI开发、NDK开发涉及较多。它是线程私有的。

虚拟机栈:也是线程私有的,JVM是基于栈的解释器执行的。每个方法被执行的时候,JVM都会在虚拟机中创建一个栈帧(Stack Frame)。而每一个栈帧内又包含了局部变量表、操作数栈、动态链接、返回地址等。

堆(Heap):又称为GC堆,是JVM所分配的最大的一块内存区块,是线程共享的,它也是垃圾回收机制回收的主要区块,因为它内存存放的是对象的实例,几乎所有的对象的实例都存放在堆内存中。下面我们所介绍的GC相关内容基本是堆内存中发生的。

 

什么是GC机制?

要理解什么是垃圾回收机制,首先我们得知道什么是“垃圾”。在java虚拟机中认为内存中已经没有用的对象即是“垃圾”,需要被及时回收。

那么虚拟机又是如何去判断出内存中的对象是不是“垃圾”的呢,虚拟机是通过一种叫做“可达性分析算法”来定位内存“垃圾”的。可达性分析算法是从离散数学中引入的,它认为在可以将一组“GC Root”对象作为起始点,然后从这些起始点向下搜索形成一条引用链,所有存在于引用链中的对象即为有用的对象,反之,没有被引用链所涵盖的对象则视为“垃圾”对象,需要被系统回收。

可达性分析算法图解

上图是我画的简略的可达性分析算法的图解,图中绿色表示可达对象,灰色表示“垃圾”对象。红色表示引用链。在上图所示中存在两条完整的引用链,分别是GC Root -> A -> D -> E 和 GC Root -> C。在内存不足或手动触发GC回收时,B、F、G则会视为“垃圾”被系统回收掉。

在JAVA中GC Root对象包括下面几种:

(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

(2). 方法区中的类静态属性引用的对象。

(3). 方法区中常量引用的对象。

(4). 本地方法栈中JNI(Native方法)引用的对象。

那么未被引用链所连接的对象在GC时就一定会被杀死吗?其实不然,如果要对一个对象进行死亡宣判,至少需要经历两次标记过程。如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。

接下来我们思考一个问题,垃圾回收是否会影响程序的性能?答案是肯定的。GC本质上来讲还是运行了一段代码来清除无用的对象,只要是运行代码就一定有耗时,一旦频繁的进行耗时操作,则会影响程序的性能,更严重的可能会直观的体现在卡顿上。GC是在堆内存不足导致内存分配失败或者开发者手动调用System.gc()时被触发。而一般情况下,开发者们是不会手动去触发GC的,所以合理的堆内存分配至关重要。

 

分代回收策略

分代回收策略是java虚拟机根据对象存活的周期不同,讲堆内存分为新生代、老年代,有些虚拟机中还存在永久代。然后在触发GC时进行分代回收,优先回收新生代中的“垃圾”对象。

在新生代中,又按照8:1:1比例被细分为Eden、Survivor0、Survivor1。

分代回收策略示意图

一个对象被创建时,它首先会进入新生代的Eden分区中,当第一次GC时,若该对象不可达,则被回收,否则则被移入到Survivor0中,之后每次GC,都会检测可达性,若可达,就会在Survivor0和Survivor1之间来回存放。一般情况下当在新生代中存活超过15次GC,该对象则会被移入到老年代。

老年代的内存大小一般比新生代大,能存放更多的对象,有时候当对象所需内存过大,而新生代内存又不足时,该对象会被直接分配到老年代中。在老年代中还维护了一个512byte的card table。用来记录所有老年代对象引用新生代的对象的信息。一旦新生代发生GC时,只需要检查老年代中的card table就能判断出老年代对象所引用的可达性,大大提高了性能。

 

实现垃圾回收机制的4种算法

标记清除算法(Mark and Sweep GC)

标记清除算法很好理解,首先对每一个对象维护一个标记,然后通过可达性分析算法生成引用链,不在引用链上的对象则被标记,最后就直接清除这些被标记的对象即可。这种算法不需要移动对象,简单方便,但却可能会产生内存碎片,提高GC的频率。

复制算法(Copying)

复制算法则是将内存空间分为两个可用区块,每次只使用其中一块,当需要GC时,首先判断可达性并标记,然后将所有可达对象都复制到另一块内存中,最后直接删除当前这块内存中的所有对象即可。这种方式实现简单、运行高效,也不用考虑内存碎片。但是由于需要平分内存为两块,大大减小了内存的利用率。

标记压缩算法(Mark-Compact)

标记压缩算法算是第一种算法的优化,它是在标记清除算法的基础上最后将所有的存活对象压缩到内存的某一端。这种方式既避免了内存碎片,又不用减小内存利用率,性价比很高,但这种压缩方式还是需要移动对象,因此在一定程度上还是降低了效率。

分代回收算法

分代回收算法就是利用上面我们所说的分代回收策略和其他回收算法所结合而产生的,比如新生代中的Survivor区块就是采用的复制算法,而在老年代中则是采用的标记压缩算法。

 

四种引用方式

有时候我们不希望某一些对象被系统回收,这时我们需要通过引用方式来进行控制。Java中存在四种引用方式,由强到若分别是强引用、软引用、弱引用和虚引用。

强引用(StrongReference)

1.只要某个对象有强引用与之关联,JVM必定不会回收这个对象。

2.即使内存不足,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。

软引用(SoftReference)

1.用来描述一些有用但并不是必须的对象。

2.对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。

弱引用(WeakReference)

1.弱引用是用来描述非必须的对象。

2.当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

虚引用(PhantomReference)

1.不影响对象的生命周期。

2.如果一个对象与虚引用关联,则跟没有引用与之关联一样。

3.在任何时候都可能被垃圾回收器回收。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章