前言
关于JVM的运行时内存,请看我的上一篇文章Java虚拟机的运行时数据区。
引用判定的算法
引用计数算法
传统的引用计数算法一般采用计数器方式,即给每一个新生成的对象 一个计数器,每当有新的地方引用它,计数器就+1,引用失效计数器-1,当计数器的值为0时,对象就不可以再被使用了,这样的对象就需要被回收。但是这个算法很难避免对象间循环引用的问题,容易造成“闭包”引发内存泄露。可达性分析算法
以一系列的”GC ROOT”对象为起点,向下搜索,搜索走过的路径称为引用链,当一个对象到GC ROOT没有任何引用链说明该对象是不可达的,即该对象不可用,java就是用的这种算法来回收对象的。
在java中,能够被作为GC ROOT的对象包括如下几种:
- 虚拟机栈栈帧中的本地变量表中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中native方法引用的对象
垃圾收集算法
1.标记清除算法
在可达性分析完毕之后,标记清除算法会首先标记出要回收的对象,在标记完成之后再统一回收所有被标记的对象。
这个算法的标记和清除效率都不高,并且按这种方式回收对象会由于对象空间分布太零散产生大量的内存碎片,这样可能会引起分配大对象时找不到连续的内存空间而不得不重新触发一次gc。标记清除算法的执行过程如下图:
2.复制算法
复制算法将可用的容量分为大小相等的两块,每次只使用其中的一块,当其中一块内存用完了,就将还存活的对象复制到另一块内存中,再将用过的内存清理掉。这样每次都是对半边内存进行回收,而另一半也不会产生内存碎片的问题,不过代价是内存缩小为原来的一半。在java堆中就是用这种算法来回收新生代的,因为一般情况下新生代超过80%的对象都是朝生夕死的,所以Eden:FromSurvivor:ToSurvivor按照8:1:1的比例来分配的。当另一块内存空间没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象将通过分配担保机制直接进入老年代。
3.标记整理算法
复制算法在对象存活率较高的情况下,效率会变低。像老年代这种对象存活率较高的内存空间不能使用这种算法进行垃圾回收。
根据老年代的特点,有人提出这种算法。标记过程跟标记清除一样,但是后续步骤是将所有存活的对象都向一端移动,然后直接清理掉端边界外的内存。
内存分配回收策略
对象主要分配在新生代的Eden区上。如果启动了本地线程缓冲(TLAB)
将按线程优先在TLAB上分配。少数情况(例如:当Eden区内存不足以分配时)会直接分配在老年代中。分配的规则取决于具体的收集器和jvm中内存相关的参数。
优先Eden分配
大多数情况下,对象都是在新生代的eden区中分配,当eden没有足够的空间进行分配的时候,虚拟机将发起一次gc。虚拟机GC完成后会将Eden区和已经分配的Survivor区中还存活的对象一次性复制另一个 Survivor区,如果这块Survivor空间不够用,则会在老年代中进行分配担保。
大对象直接进入老年代
当需要大量连续空间的java对象需要分配内存的时候(如byte[]数组),如果需要的空间大于虚拟机设置的参数,会直接进入老年代进行分配。
长期存活的大对象直接进入老年代
虚拟机采用粉黛收集的思想来管理内存,为了能清楚的区分那些对象应该放在新生代,哪些对象能够放在老年代,虚拟机给每个对象定义了“年龄”计数器,如果对象开始在eden区分配并且gc后任然存活并且能够被survicor容纳,将会移动到survivor空间,并且将年龄+1,没过一次gc如果还没被清除,那么年龄还是会+1,直到达到了晋升老年代的阀值,就会被晋升到老年代中。