目录
堆里面存在大量对象实例,垃圾收集器在对堆进行回收前,要先判断哪些对象已死(“死去”即不可能再被任何途径使用的对象)。
判断对象生死的方法:
-
引用计数法:(脑门刻字法)
在对象中添加一个引用计数器,记录有多少引用指向这个对象,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;当对象的计数器为零时,就代表没有引用指向这个对象。(对象已死)
优点:原理简单,判断高效; 问题:有额外情况需要考虑,比如循环指向,互相引用,计数器永不为0,无法回收
-
可达性分析:(平地长树法)有根活,无根死
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈中引用的对象,例如 各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,例如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,例如字符串常量池里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
四种引用
在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为四种,分别是:
强引用(Strongly Re-ference):无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。例如:Student a = new Student();
软引用(Soft Reference):只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。(如果这次回收还没有足够的内存,才会抛出内存溢出异常)
弱引用(Weak Reference):每一次垃圾收集器开始工作,无论当前内存是否足够,都会被回收掉。
虚引用(Phantom Reference):与对象实例无任何影响,与垃圾收集无任何关系。唯一作用:这个对象被收集器回收时会收到一个系统通知。
这4种引用强度依次逐渐减弱。
垃圾收集算法:
弱分代假说:绝大多数对象都是朝生夕灭的。
强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
跨代引用假说:跨代引用相对于同代引用来说仅占极少数。...
分代收集理论:收集器应该将Java堆划分出不同的区域,根据对象年龄分配到不同的区域。
Java堆划分为新生代和老年代两个区域。在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
标记-清除算法:
标记活着的,或者死了的,来区别哪些对象要被回收
好处:速度快
缺点:1、执行效率不稳定,进行大量标记和清除的动作时,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
2、空间碎片化问题(标记、清除之后会产生大量不连续的内存碎片,使以后再分配较大对象时无法找到连续内存空间 而不得不提前触发另一次垃圾收集动作。)
标记-复制算法:
(解决上一个算法进行大量标记和清除的动作时,执行效率低的问题)
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,再把已使用过的内存空间一次清理掉。
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可(类似指针碰撞分配内存)。
优点:实现简单,运行高效 缺点:只利用一半的空间,空间浪费大
优化:把新生代分为Eden空间和两块Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。默认Eden和Survivor的大小比例是8∶1,所以每次新生代中可用内存空间为整个新生代容量的90%,只有一个Survivor空间,即10%的新生代是会被“浪费”的。
但是无法保证每次回收都只有不多于10%的对象存活。Survivor存不下Eden和其中一块Survivor中存活的对象。
如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代(因为老年代存活的多,空间分配的大)。
标记-整理算法:
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象。
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
分代算法:
把新生代特点的对象放在新生代,老年代特点的对象放在老年代,按照各种不同的特点用合适的算法进行收集。
内存分配与回收策略:
对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代,大对象就是指需要大量连续内存空间的Java对象,因为在分配空间时,内存明明还有不少空间时就提前触发垃圾收集,而且当复制对象时,大对象就意味着高额的内存复制开销。
长期存活的对象将进入老年代,一直活着,来回复制很费劲
动态对象年龄判定,在相同年龄下的对象占了Survivor空间的一半,我们就把这个年龄以及比这个年龄大的对象,全部放到老年区。
空间分配担保,在发生Minor GC(标记复制算法)之前,JVM必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。
如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC