如何判断一个对象是可回收的?
Java虚拟机采用可达性分析算法来判断对象是否存活。算法基本思想:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索锁走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的,将会被判定为可回收对象。
可作为GC Roots的对象:
虚拟机栈(栈帧中的局部变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(native方法)引用的对象
强引用、软引用、弱引用、虚引用
强引用(Strong Reference):类似Object obj = new Object)的引用,存在强引用的对象,永远不会被GC。
软引用(Soft Reference):还有用但是非必须的对象。只存在软引用的对象,只有在内存不足时才被GC。可用来实现缓存(还没有被回收,直接获取对象),JDK中java.lang.ref.SoftReference用来实现软引用。
弱引用(Weak Reference):具有弱引用的对象具有更短的生命周期。只具有弱引用的对象,在垃圾收集器时会立即被回收,不管内存是否充足。JDK中java.lang.ref.WeakReference用来实现弱引用。
虚引用(Phantom Reference):一个对象是否有虚引用的存在不会影响对象的生命周期,也无法通过虚引用来获取一个对象的实例。唯一的用处:能在对象被GC时收到系统通知。JDK中java.lang.ref.PhantomReference用来实现虚引用。
垃圾收集算法
标记-清除算法:先标记所有要回收的对象,标记完成后统一回收。不足之处:标记清除效率较低;标记清除后会产生大量不连续的内存碎片,在后续为较大对象分配内存时,无法找到足够的连续内存而不得不再触发一次GC。
复制算法:将堆内存分为新生代和老年代,新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。当回收时,将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和使用过的Survivor空间。解决了内存碎片问题。不足之处:在对象存活率较高时要进行较多的复制操作,效率会变低,如果第二块Survivor没有足够空间存放,需要依赖于其他内存(老年代)。
标记-整理算法:先标记所有要回收的对象,然后让存活的对象向一端移动,最后清理端边界意外的内存。
分代收集算法:对新生代和老年代采用不同的垃圾收集算法。新生代中每次垃圾收集时都发现有大量对象死去,只有少量对象存活,采用复制算法。老年代中对象存活率高,且没有额外空间进行分配担保,采用标记-清除算法或标记-整理算法。
垃圾收集器
Serial收集器:是一个单线程收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到收集结束。试用于单个CPU的环境,由于没有线程交互的开销,收集效率高。(适用于在Client模式下运行的虚拟机,可以用java -version查看虚拟机运行在什么模式下)
ParNew收集器:是Serial收集器的多线程版本。是运行在Server模式下的虚拟机首选的新生代收集器,原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。
Parallel Scavenge收集器:是一个新生代收集器,使用复制算法。主要用于控制垃圾收集的吞吐量,可以通过参数精确的控制吞吐量,分别是控制最大垃圾收集停顿时间和吞吐量大小。常被称为“吞吐量优先”收集器。
Serial Old收集器:是serial收集器的老年代版本,也是一个单线程收集器,使用标记-整理算法。此收集器同Serial收集器一样,主要用于给Client模式下的虚拟机使用,如果在Server模式下使用,主要有两大用途:一是在JDK1.5之前与Parallel Scavenge收集器搭配使用;二是作为CMS收集器的后备方案,在并发收集器发生Concurrent Mode Failure时使用。
Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。在JDK1.6之后可以和Parallel Scavenge收集器组合使用。
CMS收集器:(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器,是基于标记 -清除算法实现的。运作过程分为4个步骤:
初始标记:标记GC Roots能直接关联到的对象,速度很快。
并发标记:进行GC Roots Tracing。即从GC Toots向下搜索并标记没有引用链的对象。
重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。此阶段停顿时间会比初始标记阶段稍长,但远比并发标记的时间短。
并发清除:清除对象的过程。
其中初始标记和重新标记仍要暂停所有线程。整个过程中耗时最长的并发标记和并发清除过程收集器 线程都可以和用户线程一起工作,从整体上来说CMS收集器的内存回收过程与用户线程是并发执行的。
CSM收集器的缺点:
1、CMS收集器对CPU资源非常敏感。CMS默认启动的垃圾收集器线程数是(CPU数量+3)/4,当CPU数量较少时,垃圾收集器会占用大量CPU资源,导致用户线程执行速度降低,应用程序吞吐量降低。
2、CMS收集器无法处理浮动垃圾(CMS在并发清除阶段用户线程仍在运行,可能会产生新的垃圾,只能等待下一次GC时处理)。CMS收集器需要预留足够的内存给用户线程使用,因此不能等到老年代几乎满了再进行收集。JDK1.6之后,CMS收集器启动阈值设置为92%,即老年代使用了92%之后就启动CMS收集器。当CMS运行期间预留的内存无法满足程序的需要时,会出现“Concurrent Mode Failure”失败,这时会启用后备方案:临时启动Serial Old收集器来进行老年代垃圾回收。
3、CMS是基于标记-清除算法实现的,收集结束会产生大量空间碎片,可能会出现老年代还有大量空间剩余,但是无法找到足够大连续空间来为新对象分配内存,而不得不再触发一次Full GC。CMS提供了一个整理碎片的参数,默认开启,用于在CMS需要在Full GC时开启内存碎片的合并整理过程,内存整理过程无法并发执行,导致用户线程停顿时间变长。
G1收集器:(Garbage-First)是一款面向服务端应用的垃圾收集器。与其他GC收集器相比,G1具备如下特点:
并行与并发:G1充分利用多CPU、多核环境下的硬件优势使用多个CPU(CPU或核心)来缩短停顿时间。
分代收集:使用G1收集器时,Java堆划分为多个大小相等的独立区域(Region),多个region组成新生代和老年代,并且新生代和老年代不需要物理上连续。也采用分代收集的方式。
空间整合:G1从整体上看是基于“标记-整理”算法实现,从局部上看是基于“复制”算法实现,在收集期间不会产生内存碎片。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在 一个长度为M毫秒的时间片内,消耗在垃圾收集上的时间不超过N毫秒。
G1收集器中,Region不可能是孤立的,一个对象在某个Region中分配内存,但是这个对象可能被其他Region中的对象引用,在进行可达性分析时,为了避免全堆扫描,每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,检查Reference引用的对象是否处于不同的Region之中,如果是,就把相关引用信息记录到对象所属的Region的Remembered Set中,当进行垃圾回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
G1收集器的运行过程:
初始标记:标记GC Roots能直接关联到的对象,并且修改Next Top at Mark Start的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。这一阶段需要停顿,但耗时很短。
并发标记 进行GC Roots Tracing。虚拟机将这段时间对象变化记录在Remembered Set Logs里面。
最终标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。把Remembered Set Logs的数据合并到Remembered Set中。这一阶段需要停顿线程,但是可以并行执行(多CPU并行执行标记操作)。
筛选回收:对各个Region的回收价值和成本进行排序,筛选出回收空间较大,时间成本较低的Region,即Garbage First,可以大幅度提高回收效率。
内存分配与回收策略
对象优先在Eden区分配内存,大对象直接进入老年代,长期存活的对象将进入老年代。
每个 对象都有一个对象年龄计数器。在Eden区出生的对象,经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移动到Survivor区,对象年龄设置为1,对象在Survivor区每经历一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认为15岁),将会被移动到老年代中。如果Survivor区中相同年龄的对象大小总和大于Survivor区的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等待年龄到达阈值。
GC对程序有什么影响?当发现虚拟机频繁GC时应该怎么办?
GC进行时必须停顿所有Java执行线程(Stop The World),GC日志中出现 "[Full GC" 说明这次垃圾回收发生了STW。
新生代GC(Minor GC)非常频繁,回收速度比较快;老年代GC(Major GC/Full GC)速度比Minor GC慢10倍以上。
虚拟机频繁GC的原因:1、堆内存设置的过小;2、程序中频繁初始化对象又释放对象,如使用String来连接字符串。
解决方法:1、通过虚拟机提供的参数调整堆的大小,-Xms设置初始堆大小,-Xmx设置最大堆大小;2、尽量重用对象,避免频繁new对象之后又不使用,如使用StringBuilder或StringBuffer替代String来连接字符串。