垃圾收集器(Garbage Collection,GC) 在java中的运行时区域的各个部分,其中,程序计数器、虚拟机栈、本地方法栈3个区域随线程生,随线程灭;栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上实在类的结构确定下来时就已知了,因此在以上三个区域,内存分配和回收都具有确定性,这几个区域内就不需要过多的考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。java堆和方法区则需要重点进行垃圾回收的关注和优化。
处理方案:
5.1 引用计数算法
给对象中添加一个引用计数器,当对象在被使用时+1,当引用实效时-1,任何时刻,计数器为0 的对象就是不可能在被使用的。但是主流的虚拟机没有采用这种方式,因为它很难解决对象之间的互相循环引用的问题。
5.2 可达性分析算法
在主流的商用程序语言(java C#)的主流实现中都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路经称之为引用链(reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots到这个对象不可达时),则证明此对象是不可用的,即是可回收的对象。
在java语言中,可作为GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象
4.本地方法栈中JNI(挤一般说的Native方法)引用的对象。
5.3 对象是生存还是死亡
对象的死亡一般经历两个过程:1.对象在进行可达分析后,发现没有与GC Roos相连接的引用链,那它将会成为被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行回收”。
当对象被判定为有必要执行finalize()方法,那么这个对象将会放置在F-Queue的队列中,并在稍后一个由虚拟机自建的、低优先级的Finalizeer线程来执行它,只是调用,不会等到该方法执行完(会有死循环的危险),任何一个对象的finalize()方法都只会被系统调用一次。(此方法不好。)
5.4回收方法区
方法区(在HotSpot)的永久代,是没有垃圾收集的。java虚拟机规范中:可以不要求虚拟机在方法区实现垃圾收集,在java堆中,垃圾回收可以得到70%-95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾回收有两个部分“废弃常量和无用的类。一般类变为无用的类的条件:
1.该类的所有的实例都已经被回收,即java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
5.5 垃圾手机算法:
1:标记-清除算法:首先标记中所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:效率低,标记和清除的两个过程效率都不高(书上),第二是空间问题,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后程序在为较大对象分配内存时,无法找到连续的内存而不得不提前触发另一次垃圾收集动作。
2:复制算法:将内存容量,划分成大小相等大两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块内存区上,然后再把已经使用过的内存空间,一次清理,使得每次操作都是对整个半区进行内存回收,也不需要考虑内存碎片等复杂情况。只要移动堆顶指针,按顺序分配内训即可。这种方式,将内存缩小为原来的一半(有的书上说这有点高)
3:标记-整理算法:复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,浪费空间。标记-整理与标记-清除算法一样,但后续步骤不是直接对内存对象进行清理,而是让所有存活的对象都像一端移动,然后清除掉端边界以外的内存。
安全点GC回收:抢先式中断(Preemptive Suspension) 和主动式中断(Voluntary Suspension)。抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现线程中断的店不在safepoint上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而影响GC事件。主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时回主动去轮询这个标志,发现中断标志为真时,就自己中断挂起,轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
对象内存分配,主要分配在新生代Eden区上。运行时配置,-Xms20M(java堆最小值)-Xmx20M(java堆最大值) -Xmn10M(新生代) -XX:PrintGcDetails(打印出对象内存分配信息) -XX:SurvivorRatio=9决定了新生代中eden区与一个Survivor区的空间比例是8:1。
大对象直接进入老年区,虚拟机提供-XX:PretenureSizeThreshold=1992参数,令大雨这个设置值的对象直接在老年代分配。
长期存活的对象将进入老年代。虚拟机采用了分代收集的思想来管理内存,虚拟机给每个对象定义了一个对象年龄(Age)计数器。默认最大值为15岁,通过参数 -XX:MaxTenuringThreshold=1设置。
动态对象年龄判断。为了更好的适应不同的程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor(幸存者代中年代)空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄