1. 概述
首先对比Java于C++语言在垃圾收集技术与内存动态分配上的区别,Java语言是提供自动垃圾回收功能的,C++没有自动垃圾回收,但垃圾回收也不是Java首创的(早在1960年,第一门开始使用内存动态分配和垃圾收集技术的 Lisp 语言诞生)。
GC主要关注于堆中和方法区的垃圾收集(重点关注堆)。
1.1 什么是垃圾?
垃圾是指在运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾。
1.2 为什么需要垃圾回收?
若对内存中的垃圾清理不及时,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至可能会导致内存溢出(OOM)。
内存溢出: 内存不够用,报内存溢出错误OOM;
内存泄漏: 有些对象已经不再被使用了,但是垃圾回收对象又不能回收它,这样悄悄的占用着内存资源的现象。
在早期像C++代码,需要程序员手动销毁垃圾对象,这样会增加程序员的工作量,并且若忘记删除,会导致内存泄漏,引进自动的内存管理降低了程序员的工作量,降低了内存溢出和内存泄漏的风险。
1.3 垃圾回收区域
垃圾收集器可以对年轻代回收,也可对老年代回收,甚至是全栈和方法区的回收,重点关注堆。
其从次数上讲:频繁收集Young区;较少收集Old区;基本不收集元空间(方法区)。
2. 垃圾回收算法
2.1 垃圾标记阶段算法
垃圾标记阶段: 主要是为了判断对象是否存货
(1)在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先要区分出内存中的存活的对象和已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,其过程可称为垃圾标记阶段。
(2)在JVM中如何标记一个死亡对象?
当一个对象已经不再被任何存活对象继续引用时,就可以宣判为已经死亡。
(3)判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
2.1.1 引用计数算法
是每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。
该算法给每个对象分配一个计数器,当有引用指向这个对象时,计数器+1,当指向该对象的引用失效时,计数器-1。只要这个对象的引用计数器的值为0,即表示这个对象不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判断效率高,回收没有延迟性。
缺点:每次引用对象时,都会更新计数器(伴随着加法和减法操作),有时间消耗;不能解决循环引用问题。
2.1.2可达性分析算法
可达性分析算法:也可称为根搜索算法,追踪性垃圾收集。
可以理解为:一棵树有很多的分支,那么若其中一个分支断掉,那么它和树本身就木有了联系,那么我们就可以将其回收了。
可达性分析实现思路:
(1)可达性分析算法是通过一种被称作“GC Root”的对象作为起点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达。
(2)使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或简介连接着,搜索所走过的路径称为引用链(Reference Chain)。
(3)如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
(4)在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots可以是哪些元素?
(1)虚拟机栈中引用的对象(如:各个线程被调用的方法中使用到的参数、局部变量等);
(2)方法区中类静态属性引用的对象(如:Java类的引用类型静态变量);
(3)方法区中常量引用的对象(如:字符串常量池(StringTable)里的引用);
(4)本地方法栈内JNI(通常说的本地方法)引用的对象;
(5)所有被同步锁synchronized持有的对象;
(6)Java虚拟机内部的引用。
2.1.3 对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
其Object类中finalize()源码:
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由有以下三点:
(1)在finalize()时可能会导致对象复活;
(2)finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会;
(3)一个糟糕的finalize()会严重影响GC的性能。
在这抛出一个小问题:在进行垃圾回收时,被判断为不可达的对象还会被回收么?
即使是不可达的,对象也不一定会被回收:
1)要先判断对象是否有必要执行finalize()方法,对象是必须重写finalize()方法并且没有被运行过;
2)如果有必要去执行,我们可以放在队列中,JVM会开一个线程(Finalizer)去执行它。
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态:
可触及的:从根节点开始,可以到达这个对象。
可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会调用一次。
这三种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才会被回收。
2.2 垃圾回收阶段算法
当在内存区分出存活的对象与死亡的对象以后,GC下面的任务就是执行垃圾回收,释放掉无用对象所占用的内存对象,以便有足够的可用空间为新对象进行内存分配。
2.2.1 复制算法
使用到两块内存空间(对标两个幸存者区),将正在使用的区域中的存活对象,复制到另一个区间,排放整齐,清除原来的空间。
优点:
-没有标记和清除的过程,实现简单,运行高效;
-内存碎片少
缺点:
使用到两块内存,GC垃圾回收期每个区域又分成多个小的区域,需要记录地址。不论是内存占用或者是时间开销也不小。
2.2.2 标记-清除算法
堆的内存区域分为一个个内存块,某个对象可能占用2个内存块,也可能占用若干个内存块,如果定位找到了垃圾对象,那么对垃圾对象进行标记,之后,在执行GC内存回收时,会将标记的内存回收。
清除:不是真的置空(不是直接将垃圾对象清理),而是将垃圾对象的地址记录在一个空闲列表里面。下次有新对象需要加载时,判断垃圾的位置空间是否足够,如果足够就存放(也就是覆盖原有的地址)。
优点:
-实现简单,不需要移动对象
缺点:
-产生了很多不连续的内存(会产生内存碎片),如果对象比较大,会出现OOM
那么复制算法与标记-清除算法的区别是什么?
复制算法是针对于新生代对象较少的情况,效率高,需要移动对象,不会产生内存碎片,但对于老年代对象较多的情况是不适应的。
标记-清除算法是针对于老年代存活对象较多,不需要移动对象,但会产生内存碎片。
2.2.3 标记-压缩算法
是针对于标记清除算法的不足,将存活的对象进行整理,然后清除掉垃圾对象,这样就不会产生内存碎片了。
优点:
-消除了内存碎片
缺点:
-效率低,低于复制算法;在移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址