Java中的引用按照强度分为4种:
类似 Object obj = new Object(); 只要强引用存在,系统就不会回收被引用的对象;
有用但非必须的对象。对于软引用的对象,在系统将要发生内存溢出前,会把这些对象列入回收范围进行二次回收。如果回收之后还没有足够的空间,才会抛出内存溢出异常。
有用但非必须的对象,当垃圾收集器工作时,一定会被收集掉。
虚引用不对对象的生存时间构成影响,只是为了使一个对象在被收集器回收时收到一个系统通知。
(1)引用计数法
在对象中添加一个引用计数器,当对象被引用时计数器加1,当引用失效时,计数器减1。当回收时就检查这个计数器,如果为0,就判为垃圾对象进行回收。
优点:简单高效。
缺点: 无法解决相互引用的问题,造成内存泄漏,因此这种方法几乎没有应用。
(2)可达性分析法
通过一系列的"GC Roots"的对象作为起点,从这些节点开始向下搜索,走过的路径称为引用链,如果一个对象不能通过引用链与任何GC Roots相连时,就是不可达对象。
如上图所示,Obj1、Obj2、Obj3、Obj4是可达的, Obj5、Obj6、Obj7虽然相互关联,但是到GC Roots是不可达的,所以会被判定为可回收的对象。
可作为GC Roots的对象包括以下几种:
(1)标记-清除算法
分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点: a. 效率问题, 标记和清除两个过程的效率都不高;
b.空间问题,标记清除之后会产生大量不连续的内存碎片,无法为较大对象分配连续空间而导致提前触发垃圾收集。
标记-清除算法是一种基础的算法,其他的算法都是针对这个算法的不足而改进的。
(2)复制算法
复制算法是为了解决标记-清除算法的效率而提出的。
将内存分为大小相等的两块,每次只使用其中一块。当一块用完后,就将还存活的对象复制到另一半内存中,然后将使用过的内存空间一次清理掉。这样每次都是对整个半区进行回收,内存分配时不用考虑内存碎片,只需移动堆顶指针按序分配即可。 这种算法在年轻代的Survivor区使用。
优点:实现简单、运行高效
缺点:浪费一半内存空间。
(3)标记-整理算法
类似于标记-清理算法,但标记之后不是直接对可回收对象进行清理,而是让所有存活对象向一个方向移动,然后清理掉对象边界以外的内存。 这种算法适用老年代,因为老年代存活对象较多且占用空间较大不适用复制算法。
(4)分代收集算法
根据对象存活周期不同将内存分为不同的几块,每块根据各自特点采用各自适合的收集算法。
年轻代对象朝生夕死采用复制算法,老年代对象存活率高且占用空间大用标记-整理算法。
新生代收集器
(1)Serial
单线程串行垃圾收集器,使用复制算法进行垃圾回收,GC时需要暂停所有用户线程。
优点:简单高效
缺点:单线程,GC时暂停用户线程,复制算法浪费一半空间
(2)ParNew
Serial的多线程版本,其他性能同Serial。
当老年代选取了 CMS收集器时,年轻代只能选择ParNew收集器与之搭配。
参数: -XX:ParallelGCThreads 来设置参与垃圾回收的线程数。
(3)Parallel Scavenge
为了达到一个可控的吞吐量(个人觉得叫吞吐率更合适)。 吞吐量 = 运行用户代码时间/CPU总时间
通过控制两个参数 -XX:GCTimeRatio 和 -XX:MaxGCPauseMills 来控制吞吐量。
优点: 可控吞吐量,且吞吐量优先
缺点:GC时暂停用户线程,复制算法浪费一半空间
老年代收集器
(4)CMS (Concurrent Mark Sweep)
过程: 初始标记 标记GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
并发标记 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程
重新标记 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程
并发清理 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。
优点: 并发收集,低停顿
缺点: 并发占用较多CPU资源
无法处理浮动垃圾(开始标记后程序仍在运行,新产生的垃圾本次无法回收只能等下次,故称浮动垃圾)占用空间
会发生 Concurrent Mode Failure(浮动垃圾占用空间导致内存不够用),启用Serial后备方案更耗时间。
标记-清理算法带来的内存碎片
(5)G1 (Gargage First)
将内存分为多个区,每个区分别管理。要回收时,对每个区的收集价值(回收所获得的空间大小和所需时间的经验值)进行比较,优先回收价值最高的区域。
步骤:初始标记 标记GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
并发标记 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程
最终标记 修改因为并发标记时程序运行而导致的标记变动,需要暂停线程
筛选回收 比较收集价值,优先回收价值最高的区域。
优点: 并发,可控吞吐量,可预测的停顿
缺点: 并发占用CPU资源
(1)根据需求或者出现的问题进行定位
(2) 用状态查看器查看运行状态
JStat
JConsole 监控与管理控制台,可以动态观察JVM运行情况
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
参考资料: 《深入理解Java虚拟机:JVM高级特性与最佳实践 周志明》