GC是什么?为什么我们要去使用它

GC(Garbage Collection)是各大语言的宠儿,也是计算机科学领域里很热门的一个话题。最早在JVM中有看过这个算法,后来发现即使是js这种脚本语言也是有GC的。单纯就JVM来说的话,GC算法也在不断地改进,成熟。从最早的串行到高顿吞吐量的并行,为了解决高延迟又演化出了CMS(Concurrent Mark Sweep),为了解决碎片的问题,又开发了G1.

为什么我们需要进行GC呢


在早期的编程语言中,程序运行中没有栈帧(Stack Frame)去维护,所以采用的是静态分配策略,这比动态分配要快不少,但是它有一个很不人性化的缺点就是需要在编译期的时候确定程序所需要的数据结构大小。

在1958年,Algol-58 语言首次提出了块结构(block-structured),块结构语言通过在内存中申请栈帧来实现按需分配的动态策略。在过程被调用时,帧(frame)会被压到栈的最上面,调用结束时弹出。栈分配策略赋予程序员极大的自由度,局部变量在不同的调用过程中具有不同的值,这为递归提供了基础。但是后进先出(Last-In-First-Out, LIFO)的栈限制了栈帧的生命周期不能超过其调用者,而且由于每个栈帧是固定大小,所以一个过程的返回值也必须在编译期确定。所以诞生了新的内存管理策略——堆(heap)管理。

现在计算机VS图灵机


现代计算机相对于图灵机来说,本质上的区别就在于资源有限性,所以在使用完各种资源以后,需要将其释放(release)。

那释放资源全都用GC可以嘛


GC本来就是给马大哈的程序员准备的,人为的手动释放资源很容易出问题,那我能不能在每次需要释放资源的时候都调用GC算法呢?这样不是一劳永逸嘛。

并不能,GC同样有它适用的和不适用的场景,在(socket/file handle)的使用中没有使用GC,很大一部分原因在于它的不确定性。你即使调用了GC你也不知道它啥时会回收,它到底会不会回收,这样的GC还是不可靠(雾,GC都是大猪蹄子

但是如果我们显示对资源进行回收就不一样了,调用了close/destroy后,资源百分百就被释放掉了

为啥在内存里面就可以用GC呢,有两个原因,第一是它具有独占性(当然你也可以叫唯一性)OS给每个运行的程序分配的内存都是唯一的,也是相互独立的,咋俩互不干扰,我晚一点回收对你也没有什么影响。第二点是内存够用,日常使用程序中也没见内存占用太高,我晚一点回收也不会OutOfMenmory,那我就放心大胆地用GC的回收机制。

由这两点可见,对于资源比较紧张的一些嵌入式的设备,还是手动释放资源来的较好。(不包括树莓派)

GC是什么


GC的本质是内存的自动管理,用来回收堆(Heap)中不再需要(使用)的对象。

我们知道内存其实也会被划分为各个区域的,常见的stack和heap。stack里面装的是局部变量,函数的调用结束以后也就回收了,这个是个固定的流程,所以不需要GC。Heap中的空间,用来在多个函数之间去共享数据,由程序自己来动态申请,这个时候我们就需要利用到GC了

GC由两大核心组成

  • 对象识别,其实也就是常说的判断对象是否存活,live object和garbage
  • 回收算法,什么时候回收,如何回收

那咋办嘛


先从对象识别开始说下吧,判断对象是live object还是garbage

在堆(Heap)里存放着Java中基本上所有对象的实例,当一个对象没有引用且不会再引用的时候就可以认为他已经死去,视为garbage

  1. 引用计数(Python和PHP都采用这种方式)

给对象一个引用计数器(在对象头加上一个counter),当引用的时候计数器就加一,引用失效就减一,引用次数为0的时候,该对象就失效了。

这个很像OS里面的PV原语,实现是很简单的,效率也比较高。但缺点也是显而易见的,就是很难去解决对象之间循环使用的问题。

举个栗子 如果现在有两个对象 OA OB ,OA.instance = OB;OB.instance=OA;它们两个相互引用,导致counter都不为0,于是都不会被回收

  1. 可达性分析(Tracing)

这个是现代语言使用最广泛的一种方式,从root对象开始,不断的去追踪(tracing),找到所有可以被引用的对象,那这些对象就可以被称作是可达的(reachable),剩下的对象自然就是不可达对象,视作garbage将其回收。

那么,你说的这个root对象,它是什么样子的呢?
  • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

被堆中对象所引用的对象,它们就不算Roots了,这样就不会引起循环引用的问题。

引用


判定对象是否存活与“引用”有关。在 JDK 1.2 以前,Java 中的引用定义很传统,一个对象只有被引用或者没有被引用两种状态,我们希望能描述这一类对象:当内存空间还足够时,则保留在内存中;如果内存空间在进行垃圾手收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为了以下四种。不同的引用类型,主要体现的是对象不同的可达性状态reachable和垃圾收集的影响。

强引用(Strong Reference)

类似 "Object obj = new Object()" 这类的引用,就是强引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象。但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏。

软引用(Soft Reference)

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用(Weak Reference)

弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

虚引用(Phantom Reference)

虚引用也称幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。它仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制。

事实上,软引用,弱引用和虚引用都可以被叫做弱引用

以弱引用的方式指向一个对象,弱引用不会保护该对象被 GC 回收。如果该对象被回收了,那么这个弱引用会被赋予一个安全值(一般为NULL)。

可以看一下这个wiki,讲了一下弱引用如何解决循环引用Dealing with reference cycles

参考


posted @ 2019-08-05 15:33  天天不是小可爱  阅读(2422)  评论(2编辑  收藏  举报