垃圾回收一般是在Java堆,方法区中进行,因为堆中几乎存放了Java中所有的对象实例。我们讨论的垃圾回收机制一般仅仅作用于jvm堆区和方法区,因为它们是thread共享的,这部分内存的分配和回收都是动态的。
1⃣️.如何确定某个对象是“垃圾”?
在所有语言中,几乎都要涉及对象是否存活的问题,比如C++/OC中给对象添加了一个引用计数机制,当需要的时候,这个引用的计数器就会+1;当引用失效的时候它的计数器-1;当计数器=0时,说明该对象不可能再被使用。但是引用计数机制有个问题,就是很难发现,解决循环计数问题:
程序创建了一个指针,指向了一个计数为1的A对象,对象A创建了对象B(计数器也为1)作为组合的子对象。对象A在创建完了,也会创建对象B,因为依赖,所以对象A拥有对象B的强引用(指针)。现在如果子对象B也有一个指向对象A的强引用(指针),那么A的计数器会变为2,如下图pic2.
当有一天pointer不需要A对象了(调用A的析构函数),A的counter值变为1,不过由于对象B的计数值仍然为1,两个对象的计数值!=0,所以都没有释放掉,造成内存泄露。
C++和OC也都有各种方法解决这个问题,自动化的智能指针/arc技术等,但是还是会遇到更棘手的循环计数问题。所以Java的用了更加适合,方便的清理技术。
Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。可达性分析法具体是如何操作的,我觉得是图论中的连通性分析算法。(P.S. GC Roots = jvm栈的引用对象 or 方法区static对象 or 方法区常量 or JNI的引用对象)。
2⃣️.典型的垃圾收集算法
1.Mark-Sweep(标记-清除)算法
标记-清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。
这个算法有如下缺点:
-标记和清除过程的效率都不高。
-标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
2.Copying(复制)算法
复制算法是针对标记—清除算法的基础上进行改进而得到的,它将jvm内存按容量分为大小相等的两块(但是实际应用中不是),每次只使用其中半块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。
复制算法优点是 一.简单,只需移动栈顶指针,按顺序分配内存即可。 二. 高效, 每次只对一块内存进行回收。三. 不会有内存碎片问题。
缺点很明显,可一次性分配的最大内存缩小了一半。
3.Mark-Compact(标记-整理)算法
为了解决Copying算法的缺点,充分利用内存空间,有了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后去除掉端边界以外的内存。具体过程如下图所示:
4.Generational Collection(分代收集)算法
分代收集算法是目前Hotspot垃圾收集器采用的算法。它将内存划分为若干个不同的区域:分为老年代(Tenured Generation)和新生代(Young Generation),在新生代中,每次垃圾收集时都会发现有大量对象死去(90%以上),只有少量存活,因此可选用复制算法来完成收集,不会有太大的复制开销,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
新生代:新创建的对象都存放在这里。因为大多数对象很快变得不可达,所以大多数对象在年轻代中创建,然后消失。这里的垃圾清除叫“minor GC”。
老年代:没有变得不可达,存活下来的年轻代对象被复制到这里。因为它更大的规模,GC发生的次数比在年轻代的少。这里的垃圾清除叫“major GC”(或“full GC”)发生了。其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。
永久代(permanent generation)也称为“方法区(method area)”,他存储class对象和字符串常量,静态变量。不是永久的存放从老年代存活下来的对象的。在这块内存中有可能发生垃圾回收。发生在这里垃圾回收也被称为Full GC。
3⃣️. 年轻代组成部分
-对象优先在Eden分配。
-大对象直接进入老年代。
-长期存活的对象将进入老年代。
年轻代总共有3块空间,其中2块为Survivor区。
执行顺序如下:
绝大多数新创建的对象分配在Eden区。
在Eden区发生一次GC后,存活的对象移到其中一个Survivor区。
在Eden区发生一次GC后,对象是存放到Survivor区,这个Survivor区已经存在其他存活的对象。
一旦一个Survivor区已满,存活的对象移动到另外一个Survivor区。然后之前那个空间已满Survivor区将置为空,没有任何数据。
经过重复多次这样的步骤后依旧存活的对象将被移到老年代。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。
4⃣️.典型的垃圾收集器
1.Serial/Serial Old 2.ParNew 3.Parallel Scavenge 4.Parallel Old 5.CMS
6. G1:G1是jdk1.7的新的收集器,替换1.5的CMS,其具有并发,分代收集,空间整合,可预测停顿时间模型等特点的新一代收集器技术。
代码来讲解:
public class Main
{
public static final int_1MB=1024*1024;
public static voidmain(String[] args)
{
byte[] a1,a2,a3,a4;
a1 =new byte[2*_1MB];
a2 =new byte[2*_1MB];
a3 =new byte[3*_1MB];
a4 =new byte[4*_1MB];
}
}
结果:[GC (Allocation Failure) [PSYoungGen: 5439K->464K(9216K)] 5439K->4568K(19456K), 0.0037039 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 7922K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 91% used [0x00000007bf600000,0x00000007bfd48af8,0x00000007bfe00000)
from space 1024K, 45% used [0x00000007bfe00000,0x00000007bfe74010,0x00000007bff00000)
to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
ParOldGen total 10240K, used 4104K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 40% used [0x00000007bec00000,0x00000007bf002020,0x00000007bf600000)
Metaspace used 2918K, capacity 4494K, committed 4864K, reserved 1056768K
class space used 324K, capacity 386K, committed 512K, reserved 1048576K
(VM参数是:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8)
从结果来看,分配a4的时候发生了一次minor gc,这次gc结果是年轻代从5439K变为464K。a1,a2,a3占用了7MB,新生代一共10MB,所以a4要分配进来,必须发生minor gc,但是此时又发现survivor区不能放进a1或者a2或者a3(1MB大小),所以只好通过分配担保机制前转移到老生代去。gc结束后,4MB的a4放入老生代。
5⃣️.空间分配担保
在发生gc之前,vm先检查老年代最大的可用连续空间是否大于年轻代所有对象总空间。如果条件成立,minor gc确保成功,如果不成立vm看HandlePromotionFailure设置的值是否允许担保失败,如果允许,那么会检查老年代最大可用的连续空间是否大于历次老年代对象的平均值,大于就进行minor gc,小于就该为full gc。