众所周知,Java有自己的垃圾回收机制,它可以有效的释放系统资源,提高系统的运行效率。那么它是怎么运行的呢,这次就来详细解析下Java的垃圾回收
1.什么是垃圾?
垃圾回收回收的自然是垃圾,那么java中是垃圾指的是什么呢?java中的垃圾指的是内存中不再使用的对象,这些对象不再会被使用,但是依然存在于内存中,如果不及时清理,久而久之会使得内存中存在大量的无用对象,显然会影响系统的正常运行
2.垃圾是如何判定的?
既然知道了内存中不再使用的对象就是垃圾,那么JVM是如何从堆内存中找到这些垃圾的呢?主要有两种方法:
2.1:引用计数算法
引用计数算法是垃圾收集器比较早的方法,堆内存中的所有对象实例都有一个引用计数,当任何其他变量被赋值为这个对象的引用时,这个对象的计数就+1,当一个对象实例
的某个引用超过了生命周期或被设置为其他新值时,对象的实例引用计数-1,当对象实例的引用计数为0时,就会被当做垃圾回收,如下示例
public static void gcTest1(){
User user = new User();//创建一个User对象实例,并新建User变量引用这个对象,此时引用计数+1之后=1
user = new User();//user变量执行了一个新的User对象实例,上一行中创建的User对象实例引用计数-1=0,会被垃圾回收
}
但是引用计数算法会有循环引用的问题,如
User user1 = new User();
User user2 = new User();
user1.user = user2;
user2.user = user1;
由于user1和user2互相引用对方,导致他们的引用计数器始终不会为0,这样垃圾收集器就永远无法回收它们。
2.2可达性分析算法
可达性分析算法是将所有的引用关系看做一张图,从GC Root节点开始寻找对应的引用节点,找到节点之后继续寻找这个节点的引用节点,当所有的引用节点寻找完后,剩余的没有被寻找到的节点就是不可达节点,不可达的节点就会被判定为可以回收的对象,可以作为GC ROOT节点的对象由四种分别如下:
a.虚拟机栈中引用的对象(栈帧中的本地变量表)
b.方法区中类静态属性引用的对象
c.方法区中常量引用的对象
d.本地方法栈中的JNI(Native方法)引用的对象
而即使可达性分析算法中剩下的不可达对象也并非直接被垃圾回收,而只是暂时处于“缓刑”阶段,真正被垃圾回收至少还需要经历两次标记的过程
第一次标记:当对象经过可达性分析被判定位不可达对象之后,会被第一次标记
第二次标记:第一次标记之后,会继续判断该对象是否有必要执行finalize()方法,在finalize()方法中没有重新与引用链建立关联关系的会被第二次标记
被第二次标记之后的对象就会被垃圾收集器进行垃圾回收处理
3.垃圾怎么回收
上一段是关于怎么寻找垃圾的,那么找到垃圾之后需要怎么进行回收呢,主要分为四种收集算法
3.1、标记-清除算法(Mark-Sweep)
标记-清除算法是从所有的GC-ROOT集合进行扫描,对可达的对象进行标记表示是存活的,标记完后再扫描整个内存中未被标记对象进行回收。标记-清除算法只对不存活的对象进行处理,而对于存活的对象不做任何处理,在存活对象较多的情况下效率较高,因为只需要操作少量的不存活的对象即可,但是同时也会造成内存碎片,所谓内存碎片是指内存中连续空间比较小的内存,如下图示:(绿色区域为存活对象,红色区域为垃圾对象)
通过标记-清除算法进行垃圾回收之后,将垃圾对象进行清除,此时内存地址为3和内存地址为7的位置是空闲的,如果此时新建一个对象需要占据内存为2,虽然位置3和位置7位置是空闲的,且加起来空闲内存也为2,但是由于不是连续的显然无法满足新对象的需求,所以新对象需要在地址12及之后来给它分配内存,而如果之后都不会再有内存为1的对象创建的话,那么内存地址3和7的位置就会一直无法得到使用,这两个位置就会被称作为内存碎片
所以这种算法的优点是操作简便,适用于存活对象较多,垃圾对象较少的情况,但是缺点是内存碎片化严重,
3.2、复制算法(Coping)
复制算法是将内存划分为两块大小相等的内存空间,每次只使用一块,当被使用的这块内存满了之后将这块内存上存活的对象复制到另一块内存上,然后然后再将这块内存清除掉,如下图
由于是将所要存活的对象依次复制到另一块内存,所以不会再有内存碎片问题,可以很有效的使用内存,但是这种算法需要将内存一分为二,一半可用而另一半必须是空闲状态。
而且如果内存中的存活的对象很多的话,需要每个存活对象都进行复制一次,效率会比较低,比较适用于存活对象较少而垃圾对象较多的场景
3.3、标记-整理算法(Mark-compact)
标记-整理算法是结合了以上两种算法优点而设计出来的,开始和标记-清除算法一样,先将所有需要回收的垃圾对象进行标记,然后不是进行清除,而是将存活对象移动到内存的一端,然后清除
端边界外的对象,如下图示:
很显然这种算法既不会出现内存碎片,同时又不需要将内存一分为二,同时满足了标记-清除算法和复制算法的优点。唯一的缺点就是需要将所要对象的位置进行移动
3.4、分代收集算法(Generational Collection)
分代收集算法是目前大部分JVM所采用的垃圾收集方法,它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般区域分为老生代(Old generation)和新生代(Young generation)
老生代的特点是每次垃圾回收时只要少量的对象需要被回收
新生代的特点是每次垃圾回收时都有大量的对象需要被回收
而针对不同区域再选择不同的收集算法进行垃圾收集。
新生代区域的内存划分是将内存按8:1:1的比例将内存划分为一个较大Eden区域和两个较小的Survivor区域,而两个Survivor区域分别叫From Space和To Space,如下图
对象的内存使用只会用到Eden区和其中的一个Survivor区,当进行垃圾回收时,会将Eden区和使用的Survivor区(From Space)中的所有存活的对象复制到另一块Survivor区(To Space)中去,此时Eden区和Survivor(From Space)的内存会全部清空,而只有另一块Survivor(To Space)会被使用,然后系统在运行一段时间之后,Eden区又会有很多对象,再进行垃圾回收的时候,Eden区会和之前保存数据的Survivor区(To Space)再将所有存活的对象复制到之前清空的Survivor区(From Space),而此时Eden和Survivor(To Space)又会被清空,反复这样操作,直到其中一块Survivor满了无法再存放对象的时候,就会直接把存活对象存放到老年代去,同时在新生代经过了15次垃圾回收后还依然存活的对象,也会被放到老年代。而老年代如果内存已经满了,则再触发一次垃圾回收(Full GC),Full GC的频率比较低,因为老年代中存活的对象一般生命周期都比较长。而在新生代触发的垃圾回收叫Minor GC,Minor GC触发的频率较高,因为新生代存放的大多数生命周期较短的对象。
针对新生代和老年代的特点,垃圾回收的算法也不一样,新生代存活对象较少,采用的是复制算法,老年代存活对象较多,采用的是标记-整理算法。分代收集算法如下图示:
当老年代区域满了的时候就会触发一次Full GC,不过由于老年代中存储的对象生命周期一般都比较长,所以Full GC的频率会比较低,回收的对象也比较少
现在知道了Java中的垃圾是什么,垃圾如何寻找以及使用何种算法及方式进行垃圾回收,那么垃圾回收的工具是什么呢?Java虚拟机靠的是垃圾收集器来进行垃圾回收,详情参看下一篇
tips:以上分析的都是针对堆内存的垃圾回收,实际上方法区也是会有垃圾回收的出现,比如废弃的常量以及没有被使用的类信息。
废弃的常量被垃圾回收的条件就是没有被引用;
类信息被垃圾回收的前提是:1.该类的所有实例已经被回收 2.加载该类的ClassLoader已经被回收 3.该类对应的Class对象在任何地方没有被引用,确保无法通过反射来访问该类的方法。