文章来源:studyidea.cn/html
GC
中文直译垃圾回收,是一种回收内存空间避免内存泄漏的机制。当 JVM
内存紧张,经过执行 GC
有效回收内存,转而分配给新对象从而实现内存的再利用。 JVM
GC
机制虽然无需开发主动参与,减轻很多工做量,可是某些状况下,自动 GC
将会致使系统性能降低,响应变慢,因此这就须要咱们提早了解掌握 GC
机制。当面对这种状况时,才能从容不迫的解决问题。另外 GC
机制也是 Java
面试高频考题,了解掌握 GC 是一项必备技能。java
学习 GC
,首先咱们解决三个问题:面试
咱们先来看一段简单的代码。算法
上面代码经过将字符串对象转化成字节数组,而后写入本地文件。方法一旦开始执行,就将会在分配必定内存给新建的对象,而后将引用告诉了str
, bytes
变量。等到方法执行完毕,方法内部局部变量紧接将就会被销毁。可是这样仅仅销毁了局部变量,却没有带走内存上这些实际的对象。这类再也不起做用,没有被引用的对象,将其归类为垃圾。数组
在偌大的内存上存活着无数对象,GC
以前须要准确将这些对象标记出来,分为存活对象与垃圾对象。这个过程一旦少标记,那就只能等待下次 GC
标记,再回收,这样将会影响 GC 效率。另外决不能错标记,将正常存活对象标记为垃圾。一旦回收正常存活的对象,可能就会引发程序各类崩溃。ide
目前有两种算法能够用来标记:性能
引用计数法经过在对象头分配一个字段,用来存储该对象引用计数。一旦该对象被其余对象引用,计数加 1。若是这个引用失效,计数减 1。当引用计数值为 0 时,表明这个对象已再也不被引用,能够被回收。学习
如上图所示,当 str
引用堆中对象时,计数值增长为 1。当 str
变为 null
时,既再也不引用该对象,计数值减 1。此时该对象就能够被 GC
回收。url
引用计数法只须要判断计数值,因此实现比较简单,这个过程也比较高效。可是存在一个很严重的问题,没法解决对象循环引用问题。idea
从上图能够看到, a
,b
再也不引用堆中对象,致使计数减一。此时两个对象内部还存在互相引用,计数值不为 0,此时 GC
没办法回收该对象。
这个算法首先须要按照规则查找当前活跃的引用,将其称为 GC Roots
。接着将 GC Roots
做为根节点出发,遍历对象引用关系图,将能够遍历(可达)的对象标记为存活,其他对象当作无用对象。
注意这里是是引用,而不是对象。
从上图能够看到,绿色对象虽然存在循环引用,可是因为这些对象不能被 GC Roots
遍历到,因此将会被回收。
能够被当作GC Roots
活跃引用包括但不限于如下引用:
还记得刚开始接触 Java
时,只知道堆栈,对象实例分配在堆中,方法中局部变量位于栈中。实际上 JVM
内存区域划分更加细致,分为:
如图所示,咱们将内存划分为线程私有与线程共享的区域。方法区与堆都是线程共享的区域,这两部分占用 JVM
大部份内存,剩下三个小弟将会跟线程绑定,随着线程消亡,自动将会被 JVM
回收。
堆
堆应该是你们最熟悉的一块区域,几乎全部对象实例都将会在此出生,一般也是虚拟机上占用内存最大一块区域,简直就是 JVM
内存中的大哥大。堆内存内部也不是简简单单一块而已,目前将会根据分代算法,将堆分代,不一样对象位于不一样区域。这一点咱们下文再详细了解。
方法区
方法区将会保存已被虚拟上加载的类信息、常量,静态变量,字节码等信息,堆上的对象正式经过方法区这些信息,才能正确建立出来。
栈
虚拟机栈栈由一系列栈帧组成,每一个栈帧其实表明一个方法,栈帧中将会保存一个方法的局部变量表,方法出入口信息,操做栈等。每当调用一个方法,就将会把这个栈帧压入栈中,执行结束,出栈。
本地方法栈与虚拟机栈比较相似,最大区别在于,虚拟机栈执行的 Java
方法,而本地方法栈将会用来执行 Native
方法服务。下面方法就会在本地方法栈中执行。
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
复制代码
程序计数器
程序计算器能够说是这几块区域占用最小的一部分,可是功能却十分重要。Java 源代码经过编译变成字节码,而后被 JVM 载入运行以后,将会变成一条条指令,而程序计数器的工做就是告诉当前线程下一条须要执行指令。这样即便发生了线程切换,等待恢复的时候,当前线程依然知道接下去要执行的指令。
目前主流 GC 算法主要分为三种:
这是一个最为基础也是最容易实现的算法,主要实现步骤分为两步:标记,清除。
GC Roots
标记出可达对象。ps:这个图着实难画啊。。。。
能够看到通过这个算法回收以后,虽然堆空间被清理出来,可是也产生不少空间碎片。这就会致使一个新对象根据堆剩余容量计算,看起来是能够分配,可是实际分配过程,因为没有连续内存,致使虚拟机感知到内存不足,又不得不提早再次触发 GC
。
可能这里你就会有疑惑,为何对象须要分配一块连续的内存?
这里引用一下 R 神 @RednaxelaFX 答案。
另外这个算法还有一个不足:标记与清除效率比较低。这就竟会致使 GC
占用时间过长,影响正常程序使用。
为了解决上述效率问题,诞生复制算法。这个算法将可用内存分为两块,每次只使用其中一块,当这一块内存使用完毕,触发 GC
,将会把存活的对象依次复制到另一块上,而后再把已使用过的内存一次性清理。
这个算法每次只须要操做一半内存,GC
回收以后也不存在任何空间碎片,新对象内存分配时只须要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。可是这个算法闲置一半内存空间,空间利用效率不高。
PS:复制算法以空间换时间,二者不可兼得
另外对象存活率也会影响复制算法效率。若是对象大部分都是朝生夕死,只须要移动少许存活对象,就能腾出大部分空间。反而若是对象存活率高,这就须要进行较多的复制操做,回收以后也并无多余内存,这就可能致使频繁触发 GC
。
针对这种存活时间长的对象,就须要使用标记-整理算法。
标记-整理算法能够说是标记-清除算法的改进版,改进了清除致使的空间碎片问题。这个算法分为两步:
GC Roots
标记存活对象。虽然标记-整理算法解决了标记-清除算法空间碎片问题,也完整利用整个内存空间,可是这个算法问题效率并不高。相较于标记-清除算法,标记-整理算法多增长整理这一步,因此该算法效率还低于标记-清除算法。
从上面三种 GC
算法能够看到,并无一种空间与时间效率都是比较完美的算法,因此只能作的是综合利用各类算法特色将其做用到不用的内存区域。
目前商业虚拟机根据对象存活周期不一样划份内存区域,通常分为新生代,老年代。新对象通常状况都会优先分配在新生代,新生代对象若存活时间大于必定阈值以后,将会移到至老年代。新生代的对象都是短命鬼,老年代的对象都是长寿先生。
新生代每次 GC
以后均可以回收大批量对象,因此比较适合复制算法,只须要付出少许复制存活对象的成本。这里内存划分并无按照 1:1 划分,默认将会按照 8:1:1 划分红 Eden
与两块 Survivor
空间。每次使用 Eden
与一块Survivor
空间,这样咱们只是闲置 10% 内存空间。不过咱们每次回收并不能保证存活对象小于 10%,在这种状况下就须要依靠老年代的内存分配担保。当Survivor
空间并不能保存剩余存活对象,就将这些对象经过分配担保进制移动至老年代。
老年代中对象存活率将会特别高,且没有额外空间进行分配担保,因此并不适合复制算法,因此须要使用标记-清除或标记-整理算法。
最近又到一年一次大考的时候,不得不又拿起周志明『深刻 Java 虚拟机』从新学习。还记得第一次翻看这本书的时候,大半内容看不懂,看完也很快就忘了。而后过了一段时间,又从新拿起此书,此次比上次好,也已经能看小大半了。最近跟一些小伙伴聊天,发现他们都是看这本书学习 JVM
,不得不说这本书真是一本神书。最近『深刻 Java 虚拟机』第三版即将上架开售,有须要的小伙伴能够考虑入手了。
好了 ,GC
机制就就总结到这里,下一篇咱们来聊聊 JVM 经常使用 GC
回收器。
GC Roots Java虚拟机详解04----GC算法和种类 深刻 Java 虚拟机
欢迎关注个人公众号:程序通事,得到平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客:studyidea.cn