垃圾回收需要确认以下几件事情:
1、哪些内存需要回收
2、什么时候回收
3、如何回收
1、哪些内存需要回收
程序计数器、虚拟机栈、本地方法栈三个区域随着线程生而生,随着线程灭而灭;栈中的栈帧随着方法的进入和退出有条不紊地执行者出栈和入栈的操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。
java堆和方法区不同,这两个区域是线程共享的,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态德,所以垃圾回收器关注的是这部分内存。
2、什么时候回收
也就是要判断,哪些对象已经“死掉”。
(1)堆区的垃圾收集
比较常见的判断方法是引用计数器法:给对象中添加一个引用计数器,每当有一个地方引用时,计数器值就加1;引用失效,计数器减一,任何时刻计数器为0的对象就不可能再被引用。
该算法较为简单,判断效率也高,但是缺点是很难解决对象之间相互循环引用的问题。比如,有两个对象已经不再被使用,但是这两个对象是互相引用的,那么它们的引用计数都不为0,GC收集器就不会回收它们。
所以主流的java虚拟机使用的都是可达性分析算法:通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链的时候(即从GC Roots到这个对象是不可达的),则该对象不可用。
GC Roots包括以下几种:
1、虚拟机栈中引用的对象
2、方法区中类静态属性引用的对象
3、方法去中常量应用的对象
4、本地方法栈中JNI引用的对象
在可达性分析算法中不可达的对象也不是一定会被回收的
- 对于不可达对象,会进行一次筛选,判断该对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize方法已经被虚拟机调用过(对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法),都视为没有必要回收
- 若该不可达对象被判定为有必要执行finalize()方法,那么这个对象会被一个由虚拟机自动创建的、低优先级的Finalizer线程回收。
(2)方法区的垃圾收集:
|–判断常量是否被弃用:没有任何对象引用常量池中的常量
|–判断类是否被弃用:
1、java堆中是否已经不存在该类的实例
2、加载该类的ClassLoader已经被回收
3、该类对象的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3、如何回收
垃圾收集算法:
标记-清除(mark-sweep)算法:首先标记出所有要回收的对象,然后统一回收这些对象,这是最基础的算法。不足之处:1、效率低。2、产生大量的不连续的内存碎片,导致当有大对象需要分配内存时,无法找到足够而连续的空间,这样就会再一次触发垃圾收集动作。
复制(copying)算法:将可用内存按容量划分为大小相等的两块,每次只是用其中一块。当这一块的内存用完了,垃圾回收时,遍历当前区域,将还存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。此算法每次只处理正在使用的中的对象,因此复制成本小,同时复制过去之后还能进行相应的内存整理,不会出现碎片问题。不足:代价是将内存缩小了一半。
标记-整理(mark-compact)算法:首先标记出所有要回收的对象,然后让所有存活的对象都向一端移动,按顺序排放,然后直接清理掉端边界以外的内存。此算法避免了“标记-清除”算法的碎片问题,同时也避免了“复制”算法的空间问题。
分代收集算法:当前商业虚拟机的垃圾收集都采用“分代收集算法”,根据对象存活周期的不同将内存划分为几块(新生代和老年代),然后根据各个年代的特点采用最适当的收集算法。新生代每次只有少量对象存活,就采用复制算法;老年代对象存活率高、没有额外空间,那么就采用“标记-清理”或“标记-整理”算法。
将对象按其生命周期的不同划分成:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。其中持久代主要存放的是类信息,所以与java对象的回收关系不大,与回收息息相关的是年轻代和年老代。
年轻代:是所有新对象产生的地方。年轻代被分为3个部分——Enden区和两个大小严格相同的Survivor区(From和to)。大多数情况下,新生对象在Eden区中被分配区域,当Eden区被对象填满时,就会执行Minor GC,并把所有存活下来的对象转移到其中一个survivor区。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。( 某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用)。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系。
假设两个survivor区分别是A和B,详细过程如下: 垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制 Old Gen;扫描A Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个Old对象,则将其移到Old Gen;扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和BSuvivor Space。
年老代:在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法。通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。
提到Minor GC和Full GC:
Minor GC—即新生代GC,指的是在新生代的垃圾收集动作,因为java对象大多都具有朝生夕灭的特点,所以minor GC非常频繁,一般回收速度也较快
Full GC/Major GC—即老年代GC,指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(不一定是一次,某些收集器的收集策略里就有直接进行Major GC的策略选择过程),major gc的速度一般会比minor gc慢10倍以上。
持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响。 这个部分的空间一般不会溢出,除非一次性加载了很多类。
注:
如何判断某个对象该进入那个代呢?
虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并且对象年龄设为1,。对象在survivor中每经过一次minor gc,年龄就增加一岁,当它的年龄增加到一定程度(默认15岁),就将会挪到老年代中。如果在survivor空间中所有相同年龄对象到校的总和大于survivor空间的一半,年龄大于或等于该年龄的对象也可以直接进入老年代。
JVM有 2个GC线程
第一个线程负责回收Heap的Young区
第二个线程在Heap不足时,遍历Heap,将Young 区升级为Older区
JVM(采用分代回收的策略),用较高的频率对年轻的对象(young generation)进行YGC,而对老对象( tenured generation)较少( tenured generation 满了后才进行)进行Full GC。这样就不需要每次GC都将内存中所有对象都检查一遍。
GC不会在主程序运行期对PermGen Space进行清理,所以如果你的应用中有很多CLASS(特别是动态生成类,当然permgen space存放的内容不仅限于类)的话,就很可能出现PermGen Space错误。