因为通过引用计数的方法无法解决循环引用的问题,所以目前的虚拟机都是通过可达性分析算法来判断一个对象是否该回收。
可达性分析算法的基本思路就是通过一系列的GC Roots对象作为起始点,通过这个节点向下搜索,如果一个对象没有一条从GC Roots到该对象的路径,则不可达,即可被回收。
JAVA中的引用:
- 强引用:永远不会被回收的引用对象 Object o = new Object()
- 软引用:在OOM之前会把这些对象作为垃圾对象进行回收,如果还没有足够的内存,则OOM,通过SoftReference类来实现
- 弱引用: 存活到下一次垃圾收集发生之前WeakReference实现
- 虚引用:最弱的,唯一目的是在对象被回收时收到一个系统通知。 PhantomReference实现。
当一个对象要被回收时需要进行两次标记,经历两次标记后的对象才会被回收,第一次就是GC roots不可达的的,第二次是调用finalize()后还没被拯救的。
方法区的回收
方法区主要回收:废弃的常量和无用的类。
- 废弃的常量是没有引用指向的常量。
- 无用的类需要满足三个条件
- 所有类的实例都已经被回收了
- 加载该类的ClassLoader被回收了
- 没有该类的Class对象了
垃圾收集算法:
标记—清除算法
标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。标记—清除算法的执行情况如下图所示:
回收前状态:
回收后状态:
该算法有如下缺点:
- 标记和清除过程的效率都不高。
标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
复制算法
复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它讲课用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:
每次只对一块内存进行回收,运行高效。
- 只需移动栈顶指针,按顺序分配内存即可,实现简单。
- 内存回收时不用考虑内存碎片的出现。
它的缺点是:可一次性分配的最大内存缩小了一半。
对新生代的回收采用此方法,一般不是采用1:1的比例,而是将其化为y一个 Eden和2个Survivor区,Eden:Survivor为8:1 ,每次只使用Eden区和1个Survivor,将活着的放到剩下的Survivor区中
复制算法的执行情况如下图所示:
回收前状态:
回收后状态:
标记—整理算法
复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记—整理算法的回收情况如下所示:
回收前状态:
回收后状态:
分代收集
当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
分代机制
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代:虚拟机对每个对象定义一个Age计数器,如果对象在Eden出生并经历一个minor GC后仍然存在并且能被Survivoe容纳的话,将被移动到Survivor中,并且年龄加1,没经过一次Minor GC年龄就加1,年龄达到一定程度就进入老年代中。
年轻代的GC叫young GC,有时候也叫 minor GC。年老代或者永久代的GC叫major GC。
ull gc是对新生代,旧生代,以及持久代的统一回收,由于是对整个空间的回收,因此比较慢,系统中应当尽量减少full gc的次数。
也就是说,所有的代都会进行GC。
一般的,首先是进行年轻代的GC,(使用针对年轻代的GC),然后是年老代和永久代使用相同的GC。如果要压缩(解决内存碎片问题),每个代需要分别压缩。
有时候,如果年老区本身就已经很满了,满到无法放下从survivor熬出来的对象,那么,YGC就不会再次触发,而是会使用FullGC对整个堆进行GC
常用JVM参数
分析gc日志后,经常需要调整jvm内存相关参数,常用参数如下
-Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制
-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与jmap -heap中显示的New gen是不同的。整个堆大小=新生代大小 + 老生代大小 + 永久代大小。在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-XX:SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。
-Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:”-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了。
-XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。
-XX:MaxPermSize:设置持久代最大值。物理内存的1/4。
按分区对待的方式分
增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
按系统线程分
串行收集:串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。
串行处理器:
在垃圾收集时需要暂停用户线程,导致卡顿。
–适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
–缺点:只能用于小型应用
并行处理器:
同样在垃圾收集时需要暂停用户线程,之后在收集的时候采用并行,同样会造成卡顿,但充分发挥多核CPU的好处。
–适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。
–缺点:垃圾收集过程中应用响应时间可能加长
并发处理器:
–适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。
总结
其实JVM的垃圾收集机制会根据所选的垃圾收集器的组合而改变。可以手动改变参数选择合适的垃圾收集器。默认client情况下是series/series old即串行收集器,server下是并行,他们在新生代都是采用复制算法 在老生代采用标记整理。并发处理器(CMS)采用是的复制和标记清除,将耗时的标记的过程和工作线程并发,使其加快收集速度,但是因为采用标记清除 所以会导致内存碎片,也比较容易导致full gc ,在jdk1.7中新增了G1收集器(garbage first)它采用并行和并发同时,一方面采用并行减少stop-the-word的时间,另一方面采用并发是用户线程继续工作。 同时采用分代收集,不需要和其他垃圾收集器一起组合使用。采用标记整理算法 减少full gc。另一个好处是可建立可预测的停顿时间模型,能让用户明确指定在一个长度在M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。G1不是采用传统的内存布局 而是将整个JAVA堆划分为多个大小相等的独立区域进行管理(还是保留了分代的概念)。