说到垃圾回收,肯定要知道jvm的分区,jvm 主要分为堆,虚拟机栈,程序计数器,方法区(元数据区),本地方法栈。其中虚拟机栈,程序计数器,和本地方法栈是线程级别的,生死跟随线程,所以是能确定销毁时间的。所以垃圾回收会集中在堆和方法区上。至于原因,堆是线程共享的区域,而内存的分配和回收是动态,所以需要特别关注这两个区域。
在进行垃圾回收前,肯定要确定垃圾回收的应该是那些对象,确定那些应该回收,那些不应该回收。这一部分也就是面试官最喜欢问的垃圾回收算法。
引用计数法
引用计数法是最早期的策略,既对象实例被创建出来就有一个引用计数,而该对象实例没分配一个变量该计数值加1,当对象实例的引用超过了声明周期或者被分配了新值,引用计数-1。而垃圾回收的就是计数值为0的对象。
这样做的好处就是引用计数器可以在程序中运行中计算,垃圾回收执行的很快,适合程序不能被长期打断的环境中运行,缺点就是不能检测循环引用。循环引用的程序计数器永远不会为0。
可达性分析算法
可达性分析算法是从离散数学中引入的,程序把所有的引用看做是一张图,从一个节点开始,寻找这个节点引用的对应节点,以此类推查找节点,当所有的几点都查找完毕,那些没有被查到的节点就被认为是没有被引用的可回收对象。
这样就可以避免循环引用不能被回收的问题,如何保证被引用的对象不被回收呢?这就要从可达性分析的开始节点来说了
一般可达性分析的几点为一下几个:
虚拟机栈中栈帧中的程序变量表中对对象的引用;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中引用的对象;
可达性分析对一次查找后的不可达对象进行标记,第一次标记后紧接着后进行二次标记,会查找当对象是否重写了fanlize()方法并在方法内重新建立连接关系,如果建立关系,将会逃离本次回收。
方法区的回收:
方法区的回收主要是废弃的常量和无用的类。对于常量来说可以用可达性分析来计算,但是类的回收要满足一下几个条件。
1、该类的实例全部已经被回收。
2、加载该类的加载器已经被回收。
3、对应该类的java.lang.Class对象没有任何地方引用。
同时满足以上三个条件方法区的类才会被GC回收。
常用的垃圾回收算法
1、引用计数法:实现简单,效率较高。一般情况下是很好的选择。
2、标记-清除算法:基础的垃圾回收算法,容易实现,原理简单:分为两个阶段:标记阶段和清除阶段,标记阶段标记那些需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
其缺点就是容易产生内存碎片,碎片过多就会导致后期大对象申请空间失败,导致提前触发新一次的垃圾回收。
3、复制算法:为了改进标记清除算法,复制算法被提出,原理就是把内存空间分成两部分,开始只在一部分中生成对象,当内存满了之后就触发算法,将存活的类copy 到另一片区域,然后将这一片空间整体回收,解决了标记清除中内存碎片的问题,
但是这样就造成可用内存缩小,垃圾回收机制更频繁的触发,如果存货的类越多,算法的效率就越低。
4、标记-整理算法:为了解决内存缩小问题,提出了标记-整理算法,标记和标记-清除的算法一样,当时清除时是先将存活的类向一端移动,然后将端终点以外的内存清理掉。
5、分代收集算法:目前大部分jvm采用的垃圾回收算法,思路就是将堆内存分为几个不同分区,一般分为老年代和新生代。他们的特点就是来年代垃圾回收时只有少量的对象需要回收,而新生代择优大量的对象需要回收。那么根据不同的分区就可以采用不同的回收算法。
新生代由于每次都会有大量的对象回收,存活对象较少,所以使用复制算法比较合适,新生代划分也不是1:1 而是分为三片区域 Eden区和两个Survivor区比例为8:1:1,新对象创建主要在Eden区。当Eden区满了的话就会将存活的对象copy 到survivor0区,然后清除Eden区,当这两个区都满了,就会将这两个区存活的对象复制到survivor1区,然后清除这两个区,然后将survivor0和survivor1两个区交换,保持survivor1区为空。
当新生代分区全满了之后,存活的对象会被放入老年代,如果老年代也满了,就会触发full GC (新生代和老年代一起回收)
老年代对象存活的对象一般是经过新生代的数次回收依然存活的生命周期比较长的对象,当老年代满了就会触发full GC,因为老年代的对象生命周期比较长,标记次数多,另外老年代的内存空间比新生代高一倍左右,所以full GC的几率比较低。