java相较于c、c++语言的优势之一是自带垃圾回收器,垃圾回收是指不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。
强引用
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动置null,可以通过System.gc方法进行回收处理
我们一般声明对象时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,就说明他不是垃圾,则不会被垃圾回收。
软引用
软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。
弱引用
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,**被弱引用关联的对象只能生存到下一次GC发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉该类对象。**弱引用对象在回收时会被放入引用队列(ReferenceQueue)。
虚引用
虚引用被称为幽灵引用或幻象引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得对象实例。任何时候都可能被回收,一般用来跟踪对象被垃圾收集器回收的活动,起哨兵作用。必须和引用队列(ReferenceQueue)联合使用。
这些概念可能有点抽象,不过这四种引用具有不同的垃圾回收时机应该是很清楚的。我们会发现,引用的强度从强、软、弱、虚依次递减,越往后的引用所引用的对象越容易被垃圾回收。
引用计数算法是判断对象是否存活的算法之一:它给每一个对象加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被使用的,即将被垃圾回收器回收。
缺点:
1.他需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2.无法解决对象减互相循环引用的问题。即当两个对象循环引用时,引用计数器都为1,当对象周期结束后应该被回收却无法回收,造成内存泄漏。
public class GcTest {
public static void main(String[] args) {
MyObject myObject_1 = new MyObject();
MyObject myObject_2 = new MyObject();
myObject_1.instance = myObject_2;
myObject_2.instance = myObject_1;
myObject_1 = null;
myObject_2 = null;
System.gc();
}
// 对象循环引用,用引用计数算法时,无法回收这两个对象
目前主流使用的都是可达性分析算法来判断对象是否存活。算法基本思路:以“GC Roots”作为对象的起点,从此节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
哪些对象可作为GC Roots?
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(Native方法)引用的对象;
活跃线程的引用对象。
finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
在可达性分析算法中被标记为不可达的对象,也不一定是一定会被回收,它还有第二次重生的机会。每一个对象在被回收之前要进行两次标记,一次是没有关联引用链会被标记一次,第二次是判断该对象是否覆盖finalize()方法,如果没有覆盖则真正的被定了“死刑”。
代码实现
public class FinalizeTest {
public static FinalizeTest ft;
/**
* 用于判断对象是死亡还是存活
*/
public static void judge(){
if(ft == null){
System.out.println("i am dead");
}else{
System.out.println("i am alive");
}
}
public static void main(String[] args) throws InterruptedException {
ft = new FinalizeTest();
// 将引用指向null,那么对象就没有任何关联了
ft = null;
// 触发一次gc
System.gc();
// 因为Finalizer线程的优先级低,因此sleep 1秒后再看结果
Thread.sleep(1000);
//因为FinalizeTest对象覆盖了finalize方法,并在该方法中重新建立与引用的关联,所以对象会复活
judge();
//下面的代码和上面的一模一样,但是对象不会再复活了,因为finalize方法最多执行一次
ft = null;
System.gc();
Thread.sleep(1000);
judge();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("执行finalize方法");
// 对象复活的关键:重新建立与引用的关联
ft = this;
}
}
确认完垃圾之后肯定要想办法回收垃圾。回收垃圾主要有下面四种方法:标记清除算法、标记整理算法、复制算法、分代收集算法。
算法思路:算法分为“标记”和“清理”两个步骤,首先标记处所有需要回收的对象,在标记完成后再统一回收所有被标记的对象。
缺陷:
1.标记和清理的两个过程效率都不高;
2.容易产生内存碎片,碎片空间太多可能导致无法存放大对象。
算法思路:标记过程和标记-清理算法一样,而后面的不一样,它是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
算法思路:将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完后,就将还存活的对象复制到另一块去,然后再把已使用过的内存空间一次清理掉。
缺陷:
可用内存缩小为了原来的一半
算法执行效率高,适用于存活对象占少数的情况。
当前大多数垃圾收集都采用的分代收集算法,这种算法并没有什么新的思路,只是根据对象存活周期的不同将内存划分为几块,每一块使用不同的上述算法去收集。在jdk8以前分为三代:年轻代、老年代、永久代。在jdk8以后取消了永久代的说法,而是元空间取而代之。
在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。
年轻代分为伊甸园区和幸存区,幸存区分为from和to区。每次执行GC操作,会将伊甸园区和from区存活的对象放到to区(如果to区不够,则直接进入老年代),然后将from区和to区的职责交换(from区变为to区,to区变为from区,谁空谁是to区)。
新生代中的对象98%都是朝生夕死的,因此把新生代分为较大的一块Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor(此处Survivor区也叫From区,另一块空的未使用的空间叫To区,From和To区是会交换的,保证空的总是To区)。
当Eden区没有足够的空间分配时,会进行一次Minor GC,Eden区大部分对象都被回收,而Eden区和From区存活的对象会放入到To区,然后From和To区进行交换。(如果To区空间不够,直接进入老年代)
以下几种情况会进入老年代。
1、 大对象
大对象就是指需要大量连续内存空间的对象,最典型的就是那种很长的字符串和数组。大对象会直接进入到老年代,这样做的目的主要是为了避免新生代发生大量的内存复制(大对象的复制成本较高)。
2、长期存活的对象
虚拟机给每个对象都定义了一个对象年龄计数器。每当进行一次Minor GC,年龄就增加1岁,当年龄超过一定值时(默认是15,可以通过参数配置),就进入到老年代。
动态对象年龄判断
虚拟机并不要求对象年龄一定要到达15岁才进入到老年代。如果Survivor空间中有某年龄相同的所有对象大小总和大于Survivor空间的一半,则年龄大于等于该年龄的对象就会直接进入老年代。