导语:大多数程序员在平常开发中经常会碰到GC的问题:OOM异常、GC停顿等,这些异常直接致使糟糕的用户体验,若是不能获得及时处理,还会严重影响应用程序的性能。本系列从GC的基础入手,逐步帮助读者熟悉GC各类技术及问题根源。(编辑:中间件小Q妹)c++
想当初,盘古开天辟地......git
好吧,扯远了,这也不是仙侠小说...程序员
GC究竟是怎么来的呢?这个问题要从C语言聊起, 你们都知道, C/C++语言在编写程序的时候, 须要码神们本身管理内存, 直观的说就是使用内存的时候要malloc,以后这段内存会一直保留给程序进程,直到程序显式的调用free才会得以释放。github
//第 0 步: char* aMem; //第 1 步: aMem = (char*) malloc(sizeof(char) * 1024); //第 2 步: strcpy(aMem, "I am a bunch of memory"); //第 3 步: free(aMem);
看到没有,就3步, 和把大象放进冰箱里同样:算法
是否是很简单?须要的时候malloc申请内存,用完以后free释放内存。但实际上就这么简单的3行代码,可能会引起很多问题, 让咱们step by step的看一下:编程
问题1:若是上面第0步也变成 aMem = (char *) malloc(sizeof(char)), 这里直接执行line 1, 有什么问题?微信
答: 内存泄漏,全部malloc申请的内存,必需要free释放以后才能再次被分配使用, 若是不free,那么程序会一直占用这段内存,直到整个进程结束。虽然程序逻辑执行没有问题, 可是若是内存泄漏过多,极可能在后面的程序中出现内存不足的问题,产生各类未知错误。可是要注意的是,若是第0步用malloc分配了空间给aMem,(假设地址是aMem=0x1234),第1步这里的malloc一样分配了空间给aMem,(假设此次malloc返回地址是aMem=0x5678), 也就是说, 0x1234指向的那段空间一直被占用,而后你的程序里却没法经过有效手段得到这个地址,也就没有办法再free它了。(由于aMem被修改为0x5678了)因此除非程序退出,否则咱们再也没有机会释放这个0x1234指向的空间了。数据结构
问题2:这里实际上申请了1024个byte的空间, 若是系统没有这么多空闲空间,有什么问题?运维
答:直接报错, 这个时候要调查一下是否是存在内存泄漏。开源项目介绍
问题3:若是copy的字符串不是“I am a bunch of memory”, 而是“1,2,3,4 ... 1025" 会怎么样?
答:因为strcpy不进行越界检查,这里第一步malloc出来的1024个字符, 却装载了1026个字符(包括'0'), 也就是说内存被污染了, 这种状况轻的会致使内存溢出,若是被别有用心的人利用了, 可能就把你的程序全部信息dump出来...好比你的小秘密...
问题 4:若是以前内存没有申请成功,第3步free会有什么问题?
答:出错,若是malloc以前失败了,其实就是第二步出错了。假设没有第二步, malloc失败以后,调用free程序会直接crash。
问题 5:若是这里调用两次free会怎么样?
答: 一样会出错, 两次free会致使未知错误、或程序crash。
问题 6:若是这里free以后, aMem里面存的是什么值?
答:free不会修改aMem的值,若是malloc以前返回0x1234给aMem,那么这里free以后,aMEM仍是0x1234。试想一下,若是后面还用aMem访问0x1234会有什么问题?
有人可能会说:上面6个问题彻底能够避免, 只要我能保证malloc和free用的对就行啦。若是现实真的这么美好,那就万事大吉了。惋惜现实状况是更为复杂的程序, 好比1000行的代码里存在if...else...、for /while循环就会容易出现上面的问题。并且内存泄漏一般埋伏在你不知道的地方,慢慢积累,直到有一天产品的业务量达到必定程度后,服务进程就会忽然崩溃。更可怕的是咱们每每缺乏有效的分析手段(或者高级的在线调试手段)来定位内存到底在哪里泄露了。
因此除了严格执行编程规范,还有别的办法能够减小Memory leak吗?一些大牛们想到了一个办法:程序员只负责分配和使用内存,由计算机负责识别须要free释放的内存,而且自动把这些不用的内存free掉。这样程序员只要malloc/new,不须要free/delete。 若是计算机能识别而且回收不用的内存(垃圾),那么一方面减小了代码量,另外一方面也会避免内存泄漏的可能性,岂不美哉?这就是Automatic Memory Management概念的由来,也就是GC的由来。
如今你们应该明白GC的意义了吧,主要包括下面两方面:
下面咱们来介绍GC里面各类牛闪闪的算法:Reference Counting,Mark Sweep,Concurrent Mark Sweep,Generational Concurrent Mark Sweep等,这些算法其实可粗略的分为两大类:
由此而来,目前GC算法主要分为两类:Reference Counting(引用计数) 与 Object Tracing (对象追踪)。今天咱们主要谈谈Reference Counting。
引用计数(Reference Counting)就是一种发现垃圾对象,并回收的算法。广义上讲,垃圾对象是指再也不被程序访问的Object,具体细分的话,“再也不被程序访问的对象”实际上还要分红两类。来来来,让咱们对Object进行一次灵魂拷问:你是什么样的垃圾?
再也不被程序访问的Object,具体能够细分为两大类:
1. 对象被还能被访问到, 可是程序之后再也不用到它了。
举个例子:
public class A { private void method() { System.out.println("I am a method"); } public static void main (String args[]) { A a1 = new A(); A a2 = new A(); a1.method(); // The following code has noting to do with a2 .... .... // a2.method(); } }
这个例子里面,a2还能被访问到,可是程序后面也不会用到它了。从程序逻辑角度,这个a2指向的对象就是垃圾,可是从计算机的角度,这个垃圾“不够垃圾”。由于若是程序后面忽然后悔了,想用a2这个对象了 (好比code里面最后一行注释), 程序仍是能够正常访问到这个对象的。 因此从计算机的角度,a2所指向的对象不是垃圾。
看到这里,你们可能会疑问:编码时已经注释了a2.method(),那么程序确定不会运行这段代码, 这样的话,a2引用的对象仍是垃圾,为何从计算机的角度来说a2对象却不是垃圾?
实际上,咱们有不少语言是支持动态代码修改的,好比Java的Bytecode Instrument,彻底能够在运行时插入a2.method()的字节码,因此仍是能够访问的。另外,这段代码的逻辑就是a2在函数栈上,a2引用的对象在堆里,因此只要a2一直引用这个对象,这个对象对程序来讲可见的,计算机不会认为它是垃圾,因此这种垃圾是不可回收物。
计算机: 我不要你以为,我要我以为!
2. 对象已经不能被访问了, 程序想用也没有办法找到它。
仍是举个例子:
public class A { private void method() { System.out.println("I am a method"); } public static void main (String args[]) { A a1 = new A(); A a2 = new A(); a1.method(); // The following code has noting to do with a2 .... .... a2 = a1; } }
和前面例子几乎一致,只是最后咱们把a1赋值给a2。这里a2的值就变了,也就是说a2指向的对象变成了a1指向的对象,a2原来的对象就没有别的东西引用它了,程序在此以后没有任何办法能够访问到它。因此它就变成了真正的垃圾。请看下图:
因此咱们一般所讲的垃圾回收技术,也主要用来处理这种对象。那么问题来了, 如何找到这种对象呢? 按照刚才的思路,没有再被任何东西引用的对象,就是可回收垃圾,由此得出一个简单直观的回收算法:引用计数。
引用计数的概念, Wikipedia的解释:
In computer science, reference counting is a programming technique of storing the number of references, pointers, or handles to a resource, such as an object, a block of memory, disk space, and others.
简单说来就是如下几点:
看不懂?不要紧,上代码:
public class A { private void method() { System.out.println("I am a method"); } public static void main (String args[]) { // 假设每一个对象有一个引用计数变量rc A a1 = new A(); // 在堆上建立对象A, A.rc++; A a2 = new A(); // 在堆上建立对象A1,A1.rc++; a2 = a1; // A1.rc--,if ( A1.rc == 0 ) { 回收A1 }, A.rc++; } // 函数退出: // a1销毁, A.rc--; // a2销毁, A.rc--; // if ( A.rc == 0 ) { 回收A } }
还没看懂?上图:
读到这里,你应该就明白Reference Counting的核心原理了。看起来很简单,只须要一个计数器和一些加减法就能够进行内存回收了。可是,Reference Counting存在一个比较大的问题,也是我我的认为目前Reference Counting算法研究的核心问题:循环引用 。
请看下面的伪代码:
class Parent { Child child; } class Child { Parent parent; } public class Main { public static void main (String[] args) { Parent p = new Parent(); Child c = new Child(); p.child = c; c.parent = p; } }
图就是这样的:
这个互相引用产生了环状引用, 引用计数器一致保持在1, Object没法被回收,形成了内存泄漏。可能你会问:不就是一个环,两个Object吗?这一点泄漏不是大问题,谁写代码不泄漏点内存。可是遇到下面这种状况呢?
单单一个环,带了一个长长的小尾巴,致使整个链上的全部对象没法回收,Heap内存逐渐失控,最终出现OOM异常,系统崩,代码卒。那么如何处理这个循环引用的问题呢?
就如前面所说, Reference Counting目前主要的研究课题都在破坏环形引用上。在我看来,目前主要是如下两种模式:
1. 左边跟我一块儿画条龙: 把问题抛给程序员
就是在程序设计语言层面提供一些办法,能够是API、注解、新的关键字等等,而后把破环的能力交给程序员。
好比Swift 提供的weak/unbound关键字,包括C++的weak_ptr,相对于strong或者默认的引用,weak在进行引用时不作引用计数的增减,而是判断所引用的对象是否已经被回收,这样全部构成环的引用都用weak来作引用,这样在计数器中,构成环的部分就不计数了。这样作的优缺点是:
优势:计算机不须要考虑环状问题,只要按照计数器进行对象回收就能够了。
缺点:程序员的意识直接决定了内存会不会溢出。若是程序员不使用weak关键字,那么有可能形成上述的内存泄漏。
2. 右边再划一道彩虹:把问题抛给计算机
这种办法就是让计算机本身找到方法去检测循环引用,一种常见的方法是配合Tracing GC,找到没有被环之外的对象引用的环,把它们回收掉。关于Tracing GC 我们放到后续讨论。你们这里只要理解,为了帮助引用计数处理环形引用,计算机必须在适当的时候触发一个单独算法来找到环,而后再作处理。这样作的优缺点:
优势:程序员彻底不须要介入,只需专一本身的业务实现。weak pointer、strong pointer分不清楚也无所谓。
缺点:须要加入新的环形引用检测机制,算法复杂度,对于程序的影响都是问题。
说了这么多,我们总结一下Reference Counting的优缺点:
优势: Reference Counting算法设计简单,只须要在引用发生变化时进行计数就能够决定Object是否变成垃圾。而且能够随着对象的引用计数归零作到实时回收对象。因此Reference Counting是没有单独的GC阶段的,程序不会出现所谓的GC stop the world 阶段。
缺点: 程序在运行过程当中会不断的生成对象,给对象成员变量赋值,改变对象变量等等。这全部的操做都须要引入一次++和--,程序性能必然受影响。(目前一种优化方法就是利用编译器优化技术,减小Reference Counting引入的计数问题,但也没法彻底避免)。
处理环形引用问题,不管是交给程序员处理,仍是交给计算机处理,都增长了程序的复杂度,还有可能会引入GC stop the world phase,这些都会在必定程度上影响程序的性能和吞吐量。
好啦!今天就聊到这里吧,预知后事如何,且听下回分解!下次给你们分享另外一类GC算法:Tracing GC,这也是目前应用比较普遍的一类算法。不管是Javascript的V八、Android的ART、Java的Hotspot、OpenJ9,仍是Golang的GC,都采用了Tracing GC算法。
臧琳,腾讯云中间件JVM工程师,主要负责腾讯云中间件JDK定制化开发及优化工做。专一于JVM中内存管理、Runtime运行时以及执行引擎在云业务中的性能分析及优化。
腾讯开源JDK项目 Tencent Kona-8,咱们致力于从JDK的层面解决云上的痛点,提升Java的业务能力。开源连接:
https://github.com/Tencent/Te...
欢迎扫码关注咱们的微信公众号,期待与你相遇~