今天来说一说JVM的垃圾收集机制,这个是Java语言的一大特点
目录
什么是“垃圾”
收集“垃圾”的方法
用什么机器收集“垃圾”
内存分配策略
1:什么是“垃圾”
Java堆中存放的是对象实例,而Java堆也是JVM收集对象垃圾的场所,用什么方法可以判断对象是否是“垃圾”?
引用计数算法: 在对象中添加一个计数器,每当对象被引用的什么,计数器就会加1,当引用失效的时候计数器就会减1,如果计数器为0,则表示该对象不可能在被使用,标记为“垃圾”。
可达性分析算法:有一个“GC Roots“的对象作为起始点,这些起始点会向下搜索,搜索的路径称为”引用链“。当一个对象和”GC Roots“之间没有任何引用链的时候,则判断该对象不在被使用,标记为“垃圾”。obj5,obj6与GC Roots之间没有任何引用链,所以视为“垃圾”。
什么样的对象可以当做GC Roots对象?
虚拟机栈中引用的对象
方法区类静态属性引用的对象
方法区常量引用对象
本地方法栈中,Native方法的引用。
即使经过可达性分析算法过后,对象没有被任何引用链链接,也不能判断该对象一定是“垃圾”,它还是有自救的机会,一个对象要被真正宣布是“垃圾”,要经过两次标记,如果对象经过可达性分析过后,没有被任何引用链链接,则视为第一次标记并进行一次筛选,筛选的依据是,是否覆盖finalize()方法,如果finalize()方法没有被虚拟机调用,加载过(也就是第一次调用该方法),那么在该方法中,只用重新与引用链相连,那么该对象就可以救活。否则,该对象被视为第二次标记,判断死亡,视为“垃圾”。
2:收集“垃圾”的方法(垃圾收集算法)
2.1:标记-清除算法(Mark-Sweep)
标记-清除算法,是一种最基础的收集算法。该算法分为两个阶段。
标记:也就是上面提到的,被标记为“垃圾”的对象
清除:清除掉那些被标记的对象。
不足:
一个是效率问题,标记和清除的过程效率都不是很高。
第二个问题就是,会产生大量的内存碎片,浪费内存空间
2.2:复制算法
为了解决效率问题,复制算法出现。
算法思路:将内存平分为二,每次只是用其中一块,当着一块内存使用完时,将该块中的存活对象复制到另一块新的内存中,然后再将该内存清除干净。
不足:这种算法是奢侈的,将内存一分为二,且只是用内存的一般,这种代价很高。
2.3:标记-整理算法
标记-整理算法
标记:和标记-清除算法一样,标记那些可回收的对象
整理:所有的存活对象都向一端移动,然后直接清理掉端边界以外的内存。
2.4:分代收集算法
现在商业的虚拟机大部分都是采用这种“分代收集”的算法,根据对象的存活周期,将内存划分为对应的几个部分,一般把Java堆分为新生代和老年代,不同代中实现不同的收集算法
新生代:在新生代中Java对象大多都是“朝生夕死”,只有少量存活,所以在新生代中使用复制算法。
老年代:由于老年代中对象的存活率较高,所以一般使用,标记-清除或者标记-整理算法。
3:HotSpot的算法实现
前面提到了,标记垃圾的算法和收集垃圾的算法,而在HotSpot虚拟机中,在实现这些算法的时候,对算法的执行效率有着严格的要求。
在可达性分析中,可以作为GC Roots的节点有很多,但是现在很多应用仅仅方法区就有上百MB,如果逐个检查的话,效率就会变得不可接受。
而且,可达性分析必须在一个一致性的快照中进行-即整个分析期间,系统就像冻结了一样。否则如果一边分析,系统一边动态表化,得到的结果就没有准确性。这就导致了系统GC时必须停顿所有的Java执行线程。(sun将这件事成为“Stop The Word”)
目前主流Java虚拟机使用的都是准确式GC,所以当执行系统都停顿下来之后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应该有办法直接知道哪些地方存放着对象引用。在HotSpot实现中,使用一组称为 OopMap 的数据结构来达到这个目的。OopMap会在类加载完成的时候,记录对象内什么偏移量上是什么类型的数据,在JTI编译过程中,也会在特定的位置记录下栈和寄存器哪些位置是引用。这样,在GC扫描的时候就可以直接得到这些信息了。
如果OopMap内容变化的指令非常多,HotSpot并不会为每条指令都产生OopMap,只是在特定的位置记录了这些信息,这些位置成为“安全点”(SafePoint)。程序执行时只有在达到安全点的时候才停顿开始GC。一般具有较长运行时间的指令才能被选为安全点,如方法调用、循环跳转、异常跳转等。
接下来要考虑的便是,如何在GC时保证所有的线程都“跑”到安全点上停顿下来。这里有两种方案: 抢先式中断 (Preemptive Suspension) 和主动式中断 (Voluntary Suspension)。
抢先式中断会把所有线程中断,如果某个线程不在安全点上,就恢复线程让它跑到安全点上。几乎没有虚拟机采用这种方式。
主动式中断思想是需要中断线程时,不直接对线程操作,而是设置一个GC标志,各个线程会轮询这个标志并在需要时自己中断挂起。这样,轮询标志的地方和安全点是重合的。
安全点机制保证程序执行时,在不太长的时间内就会遇到可进入GC的安全点,但是,程序“不执行”的时候呢,程序不执行就是没有分配CPU时间,这时线程无法响应JVM的中断请求,JVM显然不太可能的等待线程重新被分配CPU时间。
安全区域是指一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。
在线程执行到安全区域代码时,首先标识自己进入安全区域,当这段时间里JVM发起GC,不用管标识为安全区域的线程了。在线程要离开安全区域时,要检查系统是否已经完成了根节点枚举,如果完成,线程继续执行,否则等待直到收到可以安全离开安全区域的信号为止。
4:用什么机器收集垃圾(垃圾收集器)
前面提到了垃圾收集算法是内存回收的理论。那么垃圾收集器就是具体实现。随着时间的发展Java虚拟机出现了很多的收集器,如图
上面是新生代,下面是老年代。连线表示不同收集器可以一起使用。比如Serial和CMSyiji以及Serial Old
4.1:Serial收集器
Serial收集器是最基本,发展最悠久的收集器,这是一个单线程的收集器。它在进行垃圾收集的时候,会停止所有其他线程,直到它收集结束。
Serial/Serial Old
优点:在与其它单线程收集器相比,简单而高效,是Client模式下的默认新生代收集器
缺点:由于存在“Stop The Word”的情况,就相当于,应用运行一个小时,要停止几分钟,这样响应较敏感的应用,就不怎么合适了。
4.2:ParNew收集器
ParNew收集器是Serial收集器的多线程版。
ParNew收集器与Serial收集器并没有太多的改进,但是他确实在Server模式下的首选新生代收集器,还有个与性能无关的因素,那就是出来Serial,目前只有ParNew可以和CMS(后面会提到)搭配使用。
接下来会提到并发和并行,下面来了解一下
并发:指用户线程和垃圾收集线程同时运行,用户线程在执行,而垃圾收集线程在另一个cup上执行。
并行:多条垃圾收集线程并行工作,而这个时候用户线程处于等待状态。
4.3:Parallel Scavenge收集器
Parallel Scavenge收集器是新生代的收集器,采用复制算法收集,有又是并行的多线程收集器。
Parallel Scavenge收集器的特点在于,它和其他收集器的关注点不同,像CMS等收集器的关注点是缩短垃圾收集时所造成用户线程停顿的时间。而Parallel Scavenge收集器收集器的目标是达到一个可控的吞吐量。吞吐量=运行代码时间/(运行代码时间+垃圾收集时间)。
停顿时间越短就越合适需要与用户交互的程序,良好的响应速度能提高用户体验,而高吞吐量可以提高cup的利用率。尽快完成程序。主要适合后台运行计算不需要太多交互的程序。
4.4:Serial Old收集器
Serial Old收集器是Serial的年老版的收集器,在老年代中它是一个单线程的收集器,使用标记-整理算法。主要给Client模式下的虚拟机使用。
Serial/Serial Old
4.5:Parallel Old收集器
Parallel Old收集器收集器是Parallel Scavenge的年老版,使用多线程和标记-整理算法
Parallel Scavenge/Paeallel Old
Parallel Old收集器 的出现“吞吐量优先”的收集器有了合适的应用组合。在注重吞吐量和cup资源敏感的场所,ParallelScavenge/Paeallel Old
组合优先考虑。
4.6:CMS收集器。
CMS(Concurrent Mark Sweep)的收集器,是一种以最短回收停顿时间为目标的收集器。对于互联网网站或者B/S(浏览器和服务器)系统的服务端上,对响应速度有着严格的要求,希望停顿时间最短,CMS收集器就比较合适。
CMS收集器是基于一种标记-清除的算法,相对于前面几种,它的实现过程较为复杂。分四个步骤
初始标记
并发标记
重新标记
并发标记
其中初始标记和重新标记这两个步骤仍然需要“Stop The Word”(应用线程停顿),初始标记标记的是GC Roots能直接关联的对象。速度很快。
并发标记就是进行GC Roots Tracing的过程,而重新标记则是为了修正在并发标记阶段因用户线程继续运作而导致产生变动的那一部分对象的标记记录。这个阶段要比初始标记时间长一点,但是远没有并发标记的时间长。
由于整个过程耗时最长的在并发标记阶段和并发清除阶段,而这两个过程收集线程都可以和用户应用线程并发工作,所以总体来说CMS收集器是和用户线程并发工作的。
CMS虽然很优秀,但是也是有一些不足的。
1:CMS由于在并发标记和并发清除阶段都是和应用线程同时进行,那么这样也就导致了,垃圾收集清理过程会占用一部分cup资源,导致应用程序变慢,吞吐量下降。为了应付这种情况,虚拟机提供了一种“增量式并发收集器”。在并发标标记和并发清除的时候,让GC现场和应用线程交替运行,一减少GC占用CPU资源。
2:CMS收集器无法处理浮动垃圾,所谓浮动垃圾,就是在并发清除的过程中,由于应用线程还在工作,这段时间也会产生“垃圾”。这这一部分垃圾,是这次垃圾收集清除过程无法清除的,只能留到下次清除过程。
3:由于CMS采用的是标记—清除算法,所以会产生大量的磁盘碎片。
4.7:G1收集器
G1收集器收集器是现在收集器的最新成果。是一款面向服务器端的垃圾收集器。未来可以替换CMS收集器,G1收集器有一下特点
1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,
G1收集器的执行过程
初始标记
并发标记
最终标记
筛选回收
虽然G1收集器是最新的也是最先进的收集器,但是由于才开发不久,所以还没有大规模应用和测试。所以CMS还是最好的选择。
5:内存分配策略
Java技术体系中所提倡的是自动化内存管理,也就是解决两个问题对象的内存分配以及内存的回收,回收前面已经讲过了,现在讲讲对象的内存分配。
对于对象的分配也就是在Java堆上分配,Java对象的分配大部分在新生代的Eden区上面(关于Java分区,前一篇文章做了详细讲解)。但是也有一部分对象会被分在老年代。
首先还是讲一下新生代和老年代
新生代GC:(Minor GC)是指发生在新生代的垃圾收集动作。Java对象大多都有朝生夕灭的特性,所以在Minor GC非常频繁,回收速度也比较快。
老年代GC:(Major GC/Full GC是指发生在老年代的GC 经常会伴随一次Minor GC 但也不是绝对。一般Major G回避Minor GC慢10倍。
5.1:对象优先分在新生代的Eden区
5.2:大对象直接进入老年代。所谓大对象就是大量连续占用内存。比如较长的字符串和数组。
5.3:长期存活的对象进入老年代
5.4:动态对象年龄判断:如果在Survivor空间(新生代中的一种空间)中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象将会直接进入老年代