⭐垃圾回收机制
垃圾回收机制简称GC,GC主要用于Java堆的管理。因为Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
System.gc()与finalize()
GC是不定时去堆内存中清理不可达对象。垃圾收集器在一个Java程序中的执行是自动的,不能强制执行清楚那个对象,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System.gc 方法来"建议"执行垃圾收集器,但是他是否执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
我们可以使用finalize方法,在每次GC前做一些其他的工作,当然我们可以重写这个方法。(finalize方法是Object的方法),finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
先判生死(判断一个对象是否已死、判断对象是否存活)
- 指针计数法:给对象添加一个引用计数器,如果有一个地方引用它,计数器加一,当引用失效时就减一。任何时刻计数器为零的对象就是不可能再被使用的。
- 可达性分析法 :通过一系列称为=="GC Roots"的根对象==作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连 ,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
可达性分析图示:
如图,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。
Java中固定可作为GC Roots的对象:
- 在虚拟机栈中引用的对象,即栈帧中的本地变量表,譬如各个线程调用的方法栈堆中使用的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用静态类型变量。
- 在方法区中常量引用的对象,譬如字符串常量池里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象。
- 所有被同步锁(synchronized关键字)持有的对象。
引用
- 强引用 :是最传统的"引用"定义,类似
Object obj = new Object()
这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器永远不会回收掉被引用的对象。 - 软引用:用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,如果系统不会发生内存溢出异常,就不需要管,但在要发生内存溢出异常之前,就要被回收。
- 弱引用:弱引用也是用来描述那些非必须对象。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 虚引用:虚引用也称为"幽灵引用"或者"幻影引用",它是最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知,完全不会对该对象的生存时间有任何影响,也无法通过虚引用来获取一个对象的实例。
垃圾收集算法
标记-清除(Mark-Sweep)
标记-清除算法很简单,就跟它的名字一样,先标记再清除。我们可以标记要回收的对象,标记完成之后统一回收,当然也可以标记存活的对象,然后统一回收没有标记的对象。也就是先判生死,再进行处决。
缺点:
- 效率不稳定,如果堆中有大量的对象,且其中大部分都需要回收,那么标记和清除就是很浪费时间的,收集效率会随着对象的增加而下降。
- 内存空间碎片化,收集之后,堆中会有大量不连续的空间,此时如果需要给一个比较大的对象分配空间,剩余的空间足够但是不连续,那么就不得不提取触发另一次垃圾收集。
标记-复制(Mark-Copy)
标记-复制算法又简称复制算法,为了解决面对大量要回收的对象时效率低的问题 ,其核心思想是半区复制 ,即将可用内存划分为两块大小相同的区域,每次只使用一块 ,当这一块用完之后,就将还存活着的对象复制 到另一块上,然后清理到整个当前块。虽然是当存活的对象较多时,效率也是比较低的,但是对于多数对象是可回收的情况,运行高效的,而且实现简单,分配内存时也不需要考虑内存碎片。但是其缺点也非常明显,可用内存只是原来的一半,空间浪费明显。
现在Java虚拟机大多都优先采用了这种收集算法去回收新生代,IBM公司曾有一项专门研究对新生代"朝生夕灭"的特点做了更量化的诠释------新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。这里出现了新生代的概念,所以暂时跳过这一部分,后续介绍。
标记-整理算法(Mark-Compact)
标记-整理算法,其中的标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。
分代收集理论
分代收集理论是建立在两个假说之上的:
- 弱分代假说:绝大多数对都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集的对象就越难以消亡。
根据这两个假说,Java的堆被收集器分为不同的区域,然后根据回收对象的年龄(熬过垃圾收集的次数)分配到不同的区域。比较常见的划分就是新生代 与老年代 ,顾名思义,新生代中每次垃圾收集时都发现大批对象死去,只有少量对象存活,而每次垃圾回收存活后的对象将会逐步晋升到老年代中。
事实上这样简单的划分是存在一点问题的,比如:对象不是孤立的,对象之间存在跨代引用。那么在进行Minor GC时,为了找到年轻代中的存活对象,不得不遍历整个老年代;反之亦然。这种方案存在极大的性能浪费。根据经验就有了第三个假说:
- 跨代引用假说:跨代引用相对于同代引用来说仅占极极少数的。
因为跨代引用是极少的,为了找出那么一点点跨代引用,却得遍历整个老年代!所以我们可与采用记忆集的方式去优化:在新生代上建立一个全局的记忆集,这个架构把老年代划分为若干个区域,记录的就是那一块内存存在跨代引用。如果进行Minor GC,就不需要遍历整个老年代,只需要新生代的GCRoots+遍历记忆集。参考资料
分代收集算法
有了分代收集的理论,我们又可以又另一个收集的算法,即分代收集算法。顾名思义,根据年新生代和老年代的不同特征代采用不同、适当的收集算法:
- 新生代:每次垃圾收集时会有大批对象死去,只有少量存活,所以选择复制算法,只需要少量存活对象的复制成本就可以完成收集。
- 老年代:对象存活率高、没有额外空间对它进行分配担保,必须使用"标记-清理"或"标记-整理"算法进行回收。
Minor GC、Major GC与Full GC
- Minor GC:指目标只是新生代的垃圾收集。因为 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般回收速度较快。
- Major GC/Old GC:指目标只是老年代的垃圾收集,速度一般比 Minor GC 慢 10 倍以上。
- Full GC:收集整个Java堆和方法区的垃圾收集。