内存清除算法
1.标记-清除:
标记 GC roots可达的对象,清理掉没有被标记的对象。
做法:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项是清除。
1.标记:遍历所有的GC Roots,然后将GC Roots可达的对象标记为存活的对象。
2.清除:清除的过程将遍历对中所有的对象,将没有标记的对象全部清除掉。
当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。
标记的时候为什么有停止程序运行呢?
假设我们刚标记完图中最右边的那个对象,暂且记为A,结果此时在程序当中又new了一个新对象B,且A对象可以到达B对象。但是由于此时A对象已经标记结束,B对象此时的标记位依然是0,因为它错过了标记阶段。因此当接下来轮到清除阶段的时候,新对象B将会被苦逼的清除掉。如此一来,不难想象结果,GC线程将会导致程序无法正常工作。上面的结果当然令人无法接受,我们刚new了一个对象,结果经过一次GC,忽然变成null了,这还怎么玩?
缺点:
1.效率较低(递归,遍历整个堆的对象)而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。
2.清理出来的内存空间不是连续的(死亡对象都是随机出现在内存的各个角落的)。再分配数组对象的时候,寻找连续的内存空间不太好找。
2.复制算法:
将内存分为两块
做法:当内存空间耗尽时,暂停程序运行,开启复制算法GC线程。将活动区间(内存)的存活对象复制到空闲的那一块内存区域,并且严格的按照内存地址依次排列,与此同时GC线程将更新存活对象的内存引用地址指向新的内存地址。 将标记为死亡对象一次清除掉。之后(活动)的那一块内存区域变为空闲,空闲的变为忙。
缺点:
1.浪费了一半内存。
2.如果对象的存活率很高,假设为100%存活,那么就需要将所有对象都复制一遍,并且将所有引用地址复制一遍。复制这一工作所话费的时间,在对象存活率达到一定程度时,(复制所用时间)将会变的不可忽视。
总结:要想使用复制算法,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%的内存浪费。
3.标记-整理算法:
做法:
1.和标记-清除算法一样,标记出存活的对象。
2.按照内存地址依次排列,而未被标记的内存会被清理掉。
不难看出,标记-整理算法不仅可以弥补 标记-清除 算法中内存区要分散的缺点,也消除了复制算法中内存减半的高额代价。(但是从效率上讲,标记-整理算法要低于复制算法)
算法总结:
1.都是基于根搜索算法(GC Roots)的来判断一个对象是否应该被回收
2.在GC线程开启时,或者说GC过程开始时,它们都要暂停服务。
3. 效率:
复制算法 > 标记/整理算法 > 标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法 = 标记/整理算法 > 标记/清除算法。
内存利用率:标记/整理算法 = 标记/清除算法 > 复制算法。
结束语
到此我们已经将三个算法了解清楚了,可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程。
难道就没有一种最优算法吗?
当然是没有的,这个世界是公平的,任何东西都有两面性,试想一下,你怎么可能找到一个又漂亮又勤快又有钱又通情达理,性格又合适,家境也合适,身高长相等等等等都合适的女人?就算你找到了,至少有一点这个女人也肯定不满足,那就是多半不会恰巧又爱上了与LZ相似的各位苦逼猿友们。你是不是想说你比LZ强太多了,那LZ只想对你说,高富帅是不会爬在电脑前看技术文章的,0.0。
但是古人就是给力,古人说了,找媳妇不一定要找最好的,而是要找最合适的,听完这句话,瞬间感觉世界美好了许多。
算法也是一样的,没有最好的算法,只有最合适的算法。
既然这三种算法都各有缺陷,高人们自然不会容许这种情况发生。因此,高人们提出可以根据对象的不同特性,使用不同的算法处理,类似于萝卜白菜各有所爱的原理。于是奇迹发生了,高人们终于找到了GC算法中的神级算法-----分代搜集算法。
4.!分代搜集算法:
本质 属于前三种算法的实际应用 新生代,老年代,永久代
新生代:朝生夕灭,存活时间短。eg:某一个方法的局部变量,循环内的临时变量等等。
老年代:生存时间长,但总会死亡。eg:缓存对象,数据库连接对象,单例对象等等。
永久代:几乎一直不灭。eg:String池中的对象,加载过的类信息。
java堆:新生代,老年代 方法区(永久代):
使用这样的方式,我们只浪费了10%的内存,这个是可以接受的,因为我们换来了内存的整齐排列与GC速度。第二点是,这个策略的前提是,每次存活的对象占用的内存不能超过这10%的大小,一旦超过,多出的对象将无法复制。
为了解决上面的意外情况,也就是存活对象占用的内存太大时的情况,高手们将JAVA堆分成两部分来处理,上述三个区域则是第一部分,称为新生代或者年轻代。而余下的一部分,专门存放老不死对象的则称为年老代。
JVM在进行GC时,并非每次都对上面三个区域一起回收,大部分回收的是新生代。因此GC按照回收的区域又分为两种:普通GC,全局GC
普通GC:只针对新生代区域的GC
全局GC:针对老年代的GC,偶尔伴随新生代的GC以及对永久带的GC
由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC。