java虚拟机如何实现垃圾回收

时间:2022-12-24 18:38:57
java虚拟机垃圾回收机制: java虚拟机的垃圾回收,顾名思义就是将不在需要的内存进行自动回收,已节省内存资源,那么问题就是哪些内存是需要回收的,java虚拟机又是怎么回收的呢?我们知道java内存运行时区域的各个部分,其中包括程序计数器、虚拟机栈、本地方法栈,栈中的栈帧随着方法的进入和退出而有条不紊地执行出栈和入栈的操作,而每一个栈帧分配多少内存基本上是在类结果确定下来时就已经知道了,因此这几个区域的内存分配都是具有确定性的,在这几个区域内存其实根本不需要过多的考虑回收的问题,因为一个方法结束或者线程的结束,这个内存就已经回收了,但是java堆和方法区的内存回收就没那么简单,因为我们只有在程序处于运行期间时才能知道创建了哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的其实是这部分内存。
我们都知道java堆中存放的是对象实例,那么java虚拟机需要确定哪些对象还活着,哪些对象已经死了,这才是他需要考虑的问题。 我们知道一种非常普遍的算法就是用一个计数器来确定这个对象是否已经被使用,例如一个对象如果被其他对象引用,那么该对象的计数器就加一,当一个对象的引用被取消,那么计数器就减一,如此可以判断,如果任何时候计数器的值都等于0的对象应该就是不可能被使用的对象。这种算法效率很高,但是java虚拟机并没有使用这种算法,因为当两个对象之间循环引用,但除次之外没有任何一个对象在引用这个对象,那么按照我们的想法,这两个是应该被回收的, 如果用的是计数器算法,可能最后的结果不会是0,因为他们两者相互引用,所以这种算法至少在java虚拟机中是不可取的
其实java虚拟机使用的是根搜索算法,这种算法的思想就是利用一系列的GC Roots作为起点(类似于树的根节点),通过该节点往后遍历,查看是否包含某一个对象的引用,那么最后的结果就判断出这个对象能否可以被回收,即使有多个对象的循环引用,只要没有从根找到这几个对象的其他引用,就可以默认这几个对象是可回收的
什么是引用?这个可能我们会说一个对象要么被其他对象引用,要么没有引用,可以被回收,但是其实我们更希望的是再我们内存比较充足的情况下保留一些没有被引用的对象,在内存非常短缺时才进行回收,在jdk1.2版本就提出引用的几种类型:强引用,软引用,弱引用,虚引用,他们的强度由大到小,强引用只要存在就不会被回收,软引用只在内存短缺才会被回收,弱引用是在下次回收时被回收,虚引用的唯一目的就是希望在对象被收集时收到一个通知。

我们知道Object类中定义了一个finalize()方法来实现回收,如果我们没有重写fianlize()方法或者fianlize()方法已经被调用,那么这个对象就没有必要执行finalize()方法,如果一个对象被认为需要执行finalize()方法,那么这个对象会被放在一个等待队列中,并在稍后由一条由虚拟机自动建立、低优先级的Finalizer线程去执行,这里的执行是指虚拟机会触发方法,但不会等待它运行结束,因为如果一个对象在finalize()方法执行过程中发生死循环,那么这个对列就可能会一直等待,而造成回收器崩溃,如果我们重写finalize()方法,将这个对象重新与引用链上的任何一个对象建立了联系,那么此时的这个对象又会被移除等待队列,如果这个对象仍没有逃脱,那么这个对象就真的离死不远了,我们可以重写finalize()方法来看看效果:

public class JvmGC {
public static JvmGC jg=null;

@Override
protected void finalize() throws Throwable
{
super.finalize();
System.out.println("重写的finalize方法被执行");
JvmGC.jg=this;//该对象重新被引用
}

public static void main(String[] args) throws InterruptedException {
jg=new JvmGC();
jg=null;
//回收
System.gc();
//等待Finalizer
Thread.sleep(500);
if(jg!=null){
System.out.println("我还没有被回收");
}else{
System.out.println("我已经被回收了");
}

jg=null;
System.gc();
Thread.sleep(500);
if(jg!=null){
System.out.println("我还没有被回收");
}else{
System.out.println("我已经被回收了");
}
}
}
运行结果:

对内存进行回收

重写的finalize方法被执行
我还没有被回收
我已经被回收了

上面的程序结果是第一次没有被回收,第二次就被回收了,这可以说明任何一个对象的finalize()方法都只会被系统自动执行一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,所以第二次自救就失败了

垃圾收集算法的思想:标记-清除算法:这是最基础的算法,下面的所有算法都是在这种算法上进行改进的,这种算法的思想就是将步骤分为两个部分,首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象,这种算法有两种问题,一个是效率问题,即清除和标记的效率不高,另一个是空间问题,即标记清除后会产生大量的内存碎片。
复制算法:复杂算法是在标记-清除算法上的改进,打个比方,它会将内存容量分为两部分,每次只用其中一个部分,当这一块内存用完时就将这块内存所有未被收集的对象复制到另一个内存,然后把已使用过的内存一次清理掉,这样每次都是对其中的一快内存进行回收,复制到另一个内存也不会产生碎片,所以这个算法比较高效和简单,但是存在一个问题就是另一个内存占据一半空间其实是很浪费的,其实真是情况并不是完全按照1:1来分配的,而是把一整块内存类似分为成8:1:1三个部分,分别是大的内存Eden和两个小内存Survivor,每次使用的都是一个Eden和一个Survivor,然后把没有回收的部分拷到另一个survivor,清除掉Eden和Survivor中被回收的内存,如此循环,也就是默认可用的内存占据总内存的90%,只用10%会被浪费,但当我们存活的数据超过10%的话,就需要依赖其他内存,对多余未使用的对象进行担保
标记-整理算法:复制算法在对象存活比较多时会执行较多的复制运算,所以效率可能会不高,那么对于老年代一般不会使用复制算法,所以为了应对这种问题,就提出标记-整理算法,这种算法和标记-清除比较类似,但是标记整理算法不是直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后清理掉端边界的部分,所以说这种算法对于新生代的情况就会比较慢,因为它需要移动多次未回收的对象
所以目前为了应对各种算法上的优缺点,就使用了分代收集算法,我们都知道java堆分为新生代和老年代,新生代时每次都要收集大量的对象,而老年代相反,每次都只收集很少的对象,那么我们可以这么来设计,通过对象的存活周期不同将内存分成几块,一般都是新生代和老年代,然后新生代使用复制算法,老年代使用标记-清理或者标记-整理算法来
几种常见的收集器以及如何配置参数: java虚拟机如何实现垃圾回收
在介绍收集器之前需要介绍串行,并行,并发之间的区别: 串行:串行就是只有一个单线程收集器执行,而其他所有用户线程需要等待 并行:并行就是有多个收集器线程同时执行,但其他所有用户线程需要等待 并发:并发就是多个用户线程与垃圾收集器同时执行,用户程序继续执行,垃圾收集器运行于另一个cpu上 1、Serial收集器,这是最基本的收集器,在jdk1.3.1之前也是是所有新生代收集器的唯一选择,它也是一种串行收集器,也就是说当垃圾收集器工作时其他用户线程必须等待,我们可能人会这是一种已经淘汰的收集器,但是实际上它比其他单线程收集器更加简单高效,因为没有线程交互,所以Serial收集器可以获得最高的单线程收集效率,如果应用程序不是很大,其实垃圾收集的停顿时间仅仅只有几十毫秒的时间,对于用户体现上没有太大影响,而这种收集器也是采用了标记整理算法,所以比较适合老年代使用。 2、ParNew收集器,这是Serial收集器的多线程版本,也就是说是一种并行执行的收集器,但他是运行在Server模式下虚拟机的首选新生代收集器,因为除了Serial收集器,也只有ParNew收集器可以和CMS(Concurrent Mark Sweep 一种老年代收集器)收集器同时使用,在单cpu的情况下,ParNew收集器性能会不如Serial,因为存在线程交互的开销,但是在多cpu的情况下,垃圾回收时对系统资源的回收还是非常有效的。 3、Parallel Scavenge收集器,这是一种新生代收集器,采用的是复制算法实现的,同样也是多线程并行执行的收集器,它与ParNew不同之处是这种收集器考虑更多的是吞吐率(运行用户代码时间/(运行用户代码时间+垃圾收集时间))而不是停顿时间(用户线程等待垃圾收集执行时间),那么停顿时间缩短对于用户交互程序有很大帮助,但是对于后台运算,吞吐率提高更加有效,但是Parallel Scavenge提供了-Xx:MaxGCPauseMillis停顿收集参数和-Xx:GCTimeRatio吞吐率参数,可以调整停顿时间和吞吐率大小,但这里调整停顿时间是以牺牲吞吐率和新生代空间来换取的,所以说即使调低了停顿时间,但吞吐率也降了下来。另外Parallel Scavenge收集器不能和CMS收集器同时使用,因为这种收集器和CMS等收集器使用了不同的框架结构。 4、Serial Old收集器,这是Serial收集器的老年版本,和Serial收集器没有什么太大区别,可以和Parallel Scavenge收集器搭配使用。 5、Parallel Old收集器是Parallel Scavenge收集器的老年版本,在Parallel Old还未发布的时候,Parallel Scavenge收集器只能和Serial Old收集器搭配只用,我们知道Serial Old是一种单线程收集器,在多cpu情况下可能吞吐量并没有太大提高,在jdk1.6版本之后这种收集器和Parallel Scavenge收集器搭配使用才真正意义上提高吞吐率 6、CMS收集器,CMS(Concurrent Mark Sweep)收集器故名思意就是利用标记清除算法实现的,他的运行步骤分为初始标记、并发标记、重新标记、并发清除四个步骤,其中初始标记和重新标记分别是串行和并行来实现的,所以会暂停用户线程,分别完成GC Root能直接关联的对象的简单标记和修正并发标记用户程序变动的部分,其余两个分别是并发标记和清除,并发标记就是做更细致的一次检索,并发清除就是回收未使用对象,这两个都可以和用户线程并发执行,速度比串行慢,但整体来说特点就是并发收集低停顿,它的缺点就是占用部分线程,吞吐率底,然后就是并发可能会产生浮动垃圾,必须要等到下一次GC才会被清除,最后就是标记清除算法本身的缺陷就是会产生大量的内存碎片 7、G1收集器,这是目前最新版本的收集器,他是基于标记-整理算法实现的收集器,所以不会产生什么内存碎片,并且他能很好控制停顿时间,G1收集器将java堆划分成了多个大小固定的独立区域,并且根据这些区域里的垃圾堆积程度在后台维护一个优先列表,每次收集的都是垃圾最大的区域,这也就大大提到了效率
垃圾收集相关参数: UseSerialGC:使用的是Serial+Serial Old收集器组合 UseParNewGC:使用的是ParNew+Serial Old收集器组合 UseConcMarkSweepGC:使用的是ParNew+CMS+Serial Old收集器组合 UseParallelGC:使用的是Parallel Scavenge+Serial Old收集器组合 UseParallelOldGC:使用的是Parallel Scavenge+Parallel Old收集器的组合 SurvivorRatio:调整新生代和老年代区域的比例,默认为8,即Eden:Survivor=8:1