垃圾收集器关注的是Java堆和方法区这部分内存。
GC大概需要关注的事情有:
哪些内存需要回收
什么时候回收
怎样回收
复制代码
如何判断对象是否存活
引用计数算法
给一个对象添加一个引用计数器,每当有个地方引用时,计数器加1;引用失效时,计数器减1;引用计数器为0的对象不可能再被使用。
优点
实现简单,判断效率高。
复制代码
缺点
难解决对象之间存在循环引用的场景。
复制代码
主流的Java虚拟机没有用引用计数器
可达性分析算法
以GC Roots对象作为起始点,从这些节点开始向下搜索,搜索走过的路径为引用链。从GC Roots到这个对象不可达时,说明此对象不可用,会被判断为可回收的对象。
可作为GC Roots的对象
虚拟机栈中引用的对象【栈帧中的本地变量表】
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
复制代码
引用
JDK1.2之后Java对引用做了进一步的细分。由强到弱依次分为:强引用、软引用、弱引用、虚引用。
强引用
普遍存在的,类似于
Object obj = new Object();
复制代码
只要强引用还存在,垃圾收集器就不会回收被引用的对象
我们可以将对象的引用显示地置为null:o=null; 【可以帮助垃圾收集器回收此对象】
软引用
描述一些可能用到的对象但是非必需的。对于这种引用,在内存充足的时候垃圾回收器不会回收他,在内存不足的时候会回收。
软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
在Java中用java.lang.ref.SoftReference类来表示。
// 获取对象并缓存
Object object = new Object();
SoftReference softRef = new SoftReference(object);
// 从软引用中获取对象
Object object = (Object) softRef.get();
if (object == null){
// 当软引用被回收后重新获取对象
object = new Object();
}
复制代码
弱引用
弱引用用来描述非必需对象,其强度比软引用更弱。被弱引用关联的对象只能活到下次垃圾收集器回收之前,不管内存是否充足。
如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。 可以解决内存泄露问题。
例如对象池、缓存中的过期对象都有可能引发内存泄露的问题。用 WeakHashMap 来作为缓存的容器可以有效解决这一问题。WeakHashMap 和 HashMap 几乎一样,唯一的区别就是它的键使用弱引用。当 WeakHashMap 的键标记为过期时,这个键对应的条目就会自动被移除。这就避免了内存泄漏问题。
虚引用
虚引用也称为幻影引用,是最弱的一种引用方式。
一个对象是都有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。
唯一的用处:能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用。
String temp = "hello world";
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> phReference = new PhantomReference<String>(temp, queue);
System.out.println(phReference.get());//get返回null
复制代码
不同分类的引用,让内存管理更容易,不同的对象实例有不同的回收管理方式
对象是死是活
对对象进行可达性分析发现他到GC ROOTS没有可达的引用链,就会作为GC回收的对象,如果到GC Roots可达,那么就还没死,不会回收。
但是即使到GC Roots对象不可达,对象也还有自我救赎的机会,也并非死亡。
如果重写了finalize方法,并且重新指向该对象,该对象还是存活,不会死亡。如果这个自我救赎的机会也错失,那么一般都会被回收掉。
public class FinalizeTest {
public static FinalizeTest testFinalize = null;
@Override
public void finalize() throws Throwable {
super.finalize();
System.out.println("正在执行finalize方法~!");
//自救
testFinalize = this;
}
public static void main(String[] args) throws InterruptedException {
testFinalize = new FinalizeTest();
//对象第一次成功拯救自己
testFinalize = null;
System.gc();
//因为finalize()方法优先级很低,所以暂停1S等待它
Thread.sleep(1000);
//finalize()方法确实被GC触发了,但是收集前成功逃脱了。
if (testFinalize != null) {
System.out.println("alive~!");
} else {
System.out.println("dead~!");
}
//对象第二次成功拯救自己未遂,因为任何一个对象的finalize()只会被系统调用一次。
testFinalize = null;
System.gc();
//因为finalize()方法优先级很低,所以暂停1S等待它
Thread.sleep(1000);
if (testFinalize != null) {
System.out.println("alive~!");
} else {
System.out.println("dead~!");
}
}
}
复制代码
finalize方法实际中一般不会使用,运行代价大,不确定性大,可以用try-finally更好的关闭外部资源。
回收方法区
永久代主要可回收的两部分分别是:
废弃的常量
无用的类
复制代码
无用类要同时满足3个条件:
该类的所有实例都被回收掉
加载该类的ClassLoader被回收
对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法
复制代码
HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收无用。
还可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看类的加载和卸载信息。
垃圾收集算法
标记-清除算法【mark-sweep】
原理
1、标记阶段。从根集合开始扫描,标记存活的对象
2、清除阶段。扫描整个内存空间,回收未被标记的对象,使用free-list记录被释放的区域
优点
实现简单
不需要额外的空间
复制代码
缺点
效率问题,标记、清除两个阶段扫描性能不高
会产生内存碎片。分配大对象时,无法找到匹配的内存,会导致另一次垃圾收集的触发
复制代码
使用场景
针对老年代的CMS收集器;
复制代码
复制算法
原理
从根集合开始扫描,从一块内存中找到存活的对象,复制到另一块空闲的内存中,然后回收第一块内存中的对象。下次这两块内存交换身份。
优点
实现简单,运行高效,没有标记
没有内存碎片
复制代码
缺点
内存使用缩小为原来的一半
复制代码
使用场景
现在商业JVM都采用这种算法=来回收新生代;
Serial收集器、ParNew收集器、Parallel Scavenge收集器、G1;
复制代码
标记整理算法
原理
1、标记阶段。和标记清除算法的一样
2、整理阶段。让所有存活的对象向一端移动,然后直接清理掉端外界边的内存
优点
没有内存碎片
复制代码
缺点
需要移动对象,增加成本
复制代码
使用场景
很多垃圾收集器采用这种算法来回收老年代;
如Serial Old收集器、G1;
复制代码
分代收集算法
原理
当前商业虚拟机基本上都是采用分代垃圾回收算法来回收垃圾,思想也很简单,就是根据对象的生命周期将内存划分,然后进行分区管理,根据各个年代的特点采用最合适的收集算法。
一般把Java堆分为新生代和老年代;
新生代
每次垃圾收集都有大批对象死去,只有少量存活;
所以可采用复制算法;
复制代码
老年代
对象存活率高,没有额外的空间可以分配担保;
使用"标记-清理"或"标记-整理"算法;
复制代码
优点
根据各个年代的特点采用最适当的收集算法
复制代码
缺点
仍然不能控制每次垃圾收集的时间;
复制代码
使用场景
目前几乎所有商业虚拟机的垃圾收集器都采用分代收集算法;
如HotSpot虚拟机中全部垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
复制代码
垃圾收集器
JVM在进行GC时,并非每次都对新生代、旧生代、永久代一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC),它们所针对的区域如下。
普通GC(minor GC):只针对新生代区域的GC。
全局GC(major GC or Full GC):针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC。
复制代码
由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC。
新生代的收集器们
Serial收集器
单线程的收集器,进行垃圾收集时,必须暂停其他的工作线程,也就是“Stop The World”。
使用串行回收;复制算法
使用场景
单CPU、新生代小、对暂停时间要求不高的应用
Client模式下的默认新生代收集器
复制代码
参数控制
-XX:+UseSerialGC 串行收集器
复制代码
ParNew收集器
ParNew收集器就是Serial收集器的多线程版本。通过多线程扫描并压缩堆。
新生代并行,老年代串行;新生代复制算法、老年代标记-整理
使用场景
Server模式下虚拟机中首选的新生代收集器
复制代码
参数控制:
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量
复制代码
Parallel Scavenge收集器
新生代收集器,使用复制算法,多个线程来通过扫描并压缩堆。
Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,也称吞吐量优先的收集器。
吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集器时间)
复制代码
高吞吐量可以高效的利用CPU,尽快完成程序任务,适合在后台运算而不需要太多交互任务。
使用场景
多CPU、对暂停时间要求比较短的应用
Server模式上的默认选择
复制代码
控制参数
-XX:MaxGCPauseMillis 最大垃圾收集停顿时间
-XX:GCTimeRatio 设置吞吐量大小
-XX:+UseAdaptiveSizePolicy GC自适应的调节策略,把内存管理的调优任务交给虚拟机去完成
复制代码
老年代的收集器们
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,使用标记-整理算法
使用场景
这个收集器主要在于给Client模式下的虚拟机使用。
如果在Server中,主要用途是:1,在JDK1.5前和Parallel Scavenge搭配使用。2,作为Concurrent Mode Failure时候使用。
复制代码
Parallel Old
Parallel Scanvenge收集器的老年队收集器,使用标记-整理方式。
在这个方式没有产生之前,Parallel Scavenge只能选择Serial Old。
由于被拖了后腿,那么Parallel Scavenge并不能在整体上获取吞吐量最大化的效果。甚至比不上CMS+ParNew的吞吐量。
CMS收集器
并发标记-清除算法。 以获取最短回收停顿时间为目标的收集器
标记清除过程
初始化标记-stop the world【简单标记下GC Roots能直接关联到的对象】
并发标记-耗时【进行GC Roots Tracing 】
重新标记-stop the world【修正并发标记期间用户程序继续运行而导致标记发生变动那一部分对 象标记记录】
并发清除-耗时
复制代码
缺点:
无法处理浮动垃圾
对CPU资源敏感
会产生大量的空间碎片
复制代码
使用场景
重视服务器响应速度的应用
复制代码
参数控制
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
复制代码
G1收集器
G1收集器时面向服务端应用的垃圾收集器。
在G1中,堆被划分成 许多个连续的区域(region)。采用G1算法进行回收,吸收了CMS收集器特点。
支持很大的堆,高吞吐量
可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收
特点
并行和并发
分代收集
空间整合【整体上:标记-整理算法,局部上:复制算法】
可预测停顿【可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收】
复制代码
运行过程
初始标记;【标记GC Roots直接关联的对象,stw】
并发标记;【进行GC Roots Tracing 】
最终标记;【再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾】
筛选回收【stw】
复制代码
使用场景
需要大堆空间、限制的垃圾回收延迟的应用
复制代码
参数控制
–XX:+UseG1GC 使用G1垃圾回收器
复制代码