一、如何判断对象已死
1.1、引用计数法
给对象中添加一个引用计数器,每当有一个地方引用他,计数器值就加1;当引用失效,计数器值就减1,任何时刻计数器都为0的对象就是不可能再被使用的。
1.2、根搜索算法
通过一系列的名为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即:从GC Roots到这个对象不可达),则证明这个对象是不可以用的。
在Java语言中可以作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈JNI(即Native方法)的引用的对象
1.3、引用
在JDK1.2之后,Java对引用的概念进行了扩充, 将引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),四种引用强都依次递减。
-
强引用:指程序代码中普遍存在的,类似
Object obj = new Object()
这样的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。 - 软引用:描述一些还有用, 但非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
- 弱引用:描述非必需的对象,但是强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉被弱引用关联的对象。
- 虚引用:也称为幽灵引用或者幻影引用,他是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被垃圾收集器收回时收到一个系统通知。
1.4、对象生存还是死亡
一个对象真正死亡至少要经过两次标记,一个是通过根搜索算法标记出该对象无引用之后,再进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者已经执行过此方法,则会认为没有必要执行。
如果对象有必要执行finalize()方法,那么对象将会被放进F-Queue()的队列中,然后虚拟机会建立一个低优先级的Finalizer线程去执行他,如果对象在finalize()方法中又与引用链上的任何一个对象建立关联则不会被回收。
1.5、回收方法区
永久带的垃圾收集主要回收:废弃常量和无用的类。
废弃常量的回收与回收Java堆中的对象相似,只要没有地方引用则会被回收。
无用类的回收条件则更加严格,需要同时满足以下三个条件才算无用的类:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
-Xnoclassgc
:该参数可以控制虚拟机是否对类进行回收
-verbose:class
、-XX:+TraceClassLoading
、-XX:+TraceClassUnLoading
:可以查看类的加载和卸载信息
二、垃圾收集算法
2.1、标记-清除算法
分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
- 缺点:
效率问题:标记和清除过程的效率都不高,
空间问题:标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能会导致当程序以后需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次的垃圾收集。
2.2、复制算法
它将可用内存按容量分为大小相等的两部分,每次使用其中一块,当这一块的内存用完了就将还存活着的对象复制到另一块内存上,然后把已经使用过的内存空间一次清理掉。
HotSpot虚拟机新生代就是采用复制算法,将内存划分为Eden和两块Survivor区域,比例为8:1,每次使用Eden和一块Survivor区域,回收时将还存活着的对象一次拷到另一块Survivor区域,如果Survivor区域内存不够则采用分配担保机制直接进入老年代。
- 优点:每次只对其中一块的内存进行内存回收分配时,分配时不用考虑内存碎片化等复杂情况,只要移动堆指针,按顺序分配即可,实现简单,运行高效。
- 缺点:将内存缩小为原来的一半,每次只能使用一块内存区域。
2.3、标记-整理算法
先标记出存活的对象,将所有存活对象都移向一端,然后直接清除掉边界以外的内存。
2.4、分代收集算法
该算法根据对象的存活周期的不同将内存划分为几块,一般将Java堆分为新生代和老年代,根据不同年代的特点采用不同的垃圾收集算法。
三、垃圾收集器
垃圾收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。
3.1、Serial收集器
Serial收集器是一个单线程的收集器,他在进行垃圾收集操作时候需要暂停其他所有的工作线程,直到收集结束。
优点:简单而高效,Serial收集器没有线程交互的开销,专心做垃圾收集。
3.2、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,使用多条线程进行垃圾收集,其余与与Serial收集器一致。
它默认开启的线程数与CPU的数量相同,可以使用
-XX:ParallelGCThread
参数来限制垃圾收集器的线程数
3.3、Paraller Scavenge收集器(并行收集器)
它是一个新生代收集器,使用复制算法实现,是并行的多线程收集器。
Paraller Scavenge收集器的目标是达到一个可控制的吞吐量(即CPU用于运行用户代码的时间与CPU总消耗时间的比值)。高吞吐量可以最高效率的利用CPU时间,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精准控制吞吐量
-XX:MaxGCPauseMillis
:控制最大垃圾收集停顿时间-XX:GCTimeRatio
:设置吞吐量大小。
GCT IME Ratio参数的值在0~100之间,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数,默认99,即允许最大1%(1/(1+99))的垃圾收集时间-XX:+UseAdaptiveSizePolicy
:这是一个开关参数,打开它之后虚拟机会自动调整参数以提供最合适的停顿时间或最大吞吐量。
3.4、Serial Old收集器
Serial Old是Serial收集器的老年代版本,是一个单线程收集器,使用标记-整理算法。
3.5、Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。这个收集器是JDK1.6才开始提供的。
3.6、CMS收集器(Concurrent Mark Sweep)
CMS收集器是以获取最短回收停顿时间为目标的收集器。CMS是基于标记-清除算法实现的,整个过程分为4个步骤:
- 初始标记:标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing
- 重新标记:修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录
- 并发清除
初始标记和重新标记仍然需要停止用户线程
CMS收集器的缺点:
- CMS收集器对CPU资源非常敏感。在并发阶段会占用一部分线程而导致应用程序变慢,总吞吐量会降低
- CMS 收集器无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。CMS收集器默认在老年代使用68%的空间后会被激活,可以通过
-XX:CMSInitiatingOccupancyFraction
来调整这个值。- CMS收集器基于标记-清除算法实现,所以会导致大量空间碎片产生。CMS提供了
-XX:+UseCMSCompactAtFullCollection
参数设置在Full GC之后进行空间整理,-XX:CMSFullGCsBeforeCompaction
参数设置在执行多少次不压缩的Full GC之后进行一次压缩。
3.7、G1收集器(Garbage First)
G1收集器是基于标记-整理算法实现的,不会产生空间碎片,它可以非常精准的控制停顿时间,
垃圾收集器相关的常用参数:
- UseSerial'GC :虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收
- UseParNewGC:打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收
- UseConcMarkSweepGC:打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。
- UseParallelGC:虚拟机运行在Server模式下的默认值,打开此开关之后,使用Parallel + Scavenge + Serial Old的收集器组合进行内存回收
- Use ParallelOldGC :打开此开关后,使用Paradellel Scavenge + Parallel Old的收集器组合进行内存回收
- SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认是8,代表Eden:Survivor = 8:1
- PretenureSizeThreshold:直接晋升到老年代的对象大小,设置这个参数之后,大于这个参数的对象将直接进入老年代分配
- MaxTenuringThreshold:晋升到老年代的对象年龄,每个对象在坚持过一次Minor GC 之后,年龄加1,当超过这个数值之后就进入老年代
- UseAdaptiveSizePolicy:动态调整Java堆中各个区域的大小以及进入老年代的年龄
- HandlePromotionFailure:是否允许分配担保失败,即老年代的剩余空间不足以应对新生代的整个Eden和Survivor区的所有对象都存活的极端情况
- ParallelGCThreads:设置并行GC时进行内存回收的线程数
- GCTimeRatio:GC时间占总时间的比率,默认为99,即允许1%的GC时间,仅在使用Parallel Scavenge收集器生效
- MaxGCPauseMills:设置GC最大停顿时间,仅咋使用Parallel Scavenge收集器时生效
- CMSInitiatingOccupancyFraction:设置CMS收集器在老年代被使用多少之后触发垃圾收集默认为68%
- UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片化整理
- CMSFullGCsBeforeCompaction:设置CMS收集器在进行若干此垃圾收集之后再启动一次内存碎片管理
4、内存分配与回收策略
- 4.1、对象优先在Eden分配
- 4.2、大对象直接进入老年代
- 4.3、长期存活的对象将进入老年代
参考《深入理解JAVA虚拟机》