一、垃圾判断算法
1、引用计数算法
- 给对象添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能再被使用
- 引用计数算法无法解决对象循环引用的问题。例如JVM中存在A、B两个对象,而A、B是相互引用着的,也就是A里面持有B的引用,而B里面又持有A的引用,当A只会被B引用,而B只会被A引用,A和B的引用计数器都是1,是一对孤立的对象,如果采用引用计数来进行垃圾回收,则这俩对象永远不会被回收。
2、根搜索算法
既然引用计数算法存在对象循环引用的问题,所以此算法出现了,下面具体看下该算法:
①、在实际的生产语言中(Java、C#等),都是使用根搜索算法判断对象是否存活。
②、算法基本思路就是通过一系列的称为“GC Roots”的点作为超始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。
③、在Java语言中,GC Roots包括【也就是什么是GC Roots?】:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
二、GC算法
1、标记-清除算法(Mark-Sweep)
1、算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象。
2、缺点:
- a、效率问题:标记和清理两个过程效率都不高。需要扫描所有的对象,堆越大,GC越慢
- b、空间问题:标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。GC次数越多,碎片越严重。
下面用图表说明一下整个算法的过程,首先初始内存为:
image.png
最终内存的可达情况为:其中标红的则是不能被Root GC所能引用的,也就是应该是被回收掉的
image.png
2、复制算法(Copying)
1:1模式
1、将可用内存分为两块,每次只使用其中的一块,当半区内存用完了,仅将不存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。
2、这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价高昂。
8:1:1模式
1、现在的商业虚拟机中都是用了这一种收集算法来回收新生代【啥叫新生代呢?通常情况下刚new出来的对象都会位于新生代当中,当新生代经历了几轮垃圾回收之后,尚未被回收的对象,这时JVM就会认为这些对象的存活时间比较长,则会将它们晋升到老年代中】。
2、将内存分为一块较大的eden空间和2块较少的survivor【幸存者】空间区,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次性拷贝到另外一块survivor(空间上,然后清理掉eden和用过的survivor。
3、Oracle Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。
4、复制收集算法在对象存活率高的时候,效率有所下降。,因此老年代一般不嫩直接使用这种算法
特点:
1、只需要扫描存活的对象,效率更高。
2、不会产生碎片。
3、需要浪费额外的内存作为复制区。
4、复制算法非常适合生命周期比较短的对象,因为每次GC总能回收大部分的对象,复制的开销比较小。
5、根据IBM的专门研究,98%的Java对象只会存活1个GC周期,对这些对象很适合用复制算法。而且不用1:1的划分工作区和复制区的空间。
3、标记-整理算法(Mark-Compact)
它的标记过程跟上面的复制搜索算法是一样的,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。
如图:
image.png
特点:
1、没有内存碎片
2、比Mark-Sweep(标记清除算法)耗费更多的时间进行compact(压缩整理)
4、分代算法(Generational)
一般情况
- 当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法,根据对象不同的存活周期将内存划分为几块。
- 一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量对象的复制成本就可以完成收集。
New代表新生代,如之前所说它里面的垃圾回收算法可以采用复制算法,而对于Old老年代的内存则可以通过标记清除算法或者标记整理算法。
image.png
Hotspot JVM6
年轻代(Young Generation)
1、新生成的对象都放在新生代。年轻代用复制算法进行GC(理论上,年轻代对象的生命周期非常短,所以适合复制算法)。
2、年轻代分三个区。一个Eden区,两个Survivor区(可以通过参数设置Survivor个数)。对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到一个Survivor区,当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当第二个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。2个Survivor是完全对称,轮流替换。
3、Eden和2个Survivor的缺省比例是8:1:1,也就是10%的空间会被浪费。可以根据GC log的信息调整大小的比例。
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
老年代(Old Generation)
1、存放了经过一次或多次GC还存活的对象。
2、一般采用Mark-Sweep(标记 - 清除算法)或者Mark-Compact(标记 - 整理算法)进行GC。
3、有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个GC算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器(追求吞吐量?追求最短的响应时间?)。
永久代(已经改名为元空间)
1、并不属于堆(Heap),而是所谓的方法区。但是GC也会涉及到这个区域。
2、存放了每个Class的结构信息,包括常量池、字段描述、方法描述。与垃圾收集要收集的Java对象关系不大。
图解3代关系
在最开始时,对象是处于年轻代中,如下:
image.png
然后对象是存在其中的Eden Space和From Space中,这俩的比例是可以调整的,其整个默认比例是8:1:1,而当进行垃圾回收时,则会将Eden Space和From Space留下来的对象都转到To Space上去,此时Eden Space和To Space又可以搭配工作,此时图中的From Space就变为了To Space,而图中的To Space又变成了From Space。而经过了几次回收之后,年轻代的对象就会进入到老年代,如下:
image.png
而最终还有一个永久代,它是针对JDK8之前而存在的,如下:
image.png
三、什么情况下触发垃圾回收
由于根据对象的生命周期进行了分代,所有不同区域的回收时间和方式是不一样的,主要有两种类型:Scavenge GC和Full GC。
Scavenge GC(Minor GC)
- 触发时机:新对象生成时,Eden空间满了。
- 理论上Eden区大多数对象会在Scavenge GC回收,复制算法的执行效率会很高,Scavenge GC时间比较短。
Full GC【这个在实际中一定得要避免】
这个是对整个堆进行整理回收的方法,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。
触发时机:
- 年老代(Tenured)被写满
- 永久代(Perm)被写满
- System.gc()被显示调用
三、垃圾回收器的实现和选择
每个蓝色的盒子都代表了一个收集器,用来收集某一代。黄色区域中的蓝色盒子是用来收集新生代的,灰色区域中的蓝色盒子是用来收集老年代的。
image.png
怎么选择GC回收器
最小化地使用内存和并行开销,新生代和老年代分别选择Serial GC 和 Serial Old
最大化地使用应用程序的吞吐量,新生代和老年代分别选择Parallel Scanvenge 和 Parallel Old
最小化GC的中断或停顿时间,新生代和老年代分别选择ParNew 和 CMS
垃圾收集器的“并行”和“并发”
- 并行(Parallel):指多个收集器的线程同时工作,但是用户线程处于等待状态。
- 并发(Concurrent):指收集器在工作时同时,可以允许用户线程工作。
并发不代表解决了GC停顿的问题,在关键的步骤还是要停顿。比如在收集器标记垃圾的时候。但在清除垃圾的时候,用户线程可以和GC线程并发执行。
1、Serial收集器
- 单线程收集器,收集时会暂停所有工作线程(Stop The World,简单STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代会采用此收集器。
- 最早的收集器,单线程进行GC。
- 在新生代,采用复制算法:在老年代,采用Mark-Compact算法。
- 因为是单线程GC,没有多线程切换的额外开销,简单实用。
-
Hotspot Client模式缺省的的收集器
image.png
2、ParNew收集器
- ParNew收集器就是Serial的多线程版本,除了使用多个收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一模一样。
- 对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。
- Serial收集器在新生代的多线程版本。
- 使用复制算法(因为针对新生代)。
- 只有在多CPU的环境下,效率才会比Serial收集器高。
- 可以通过-XX:ParallelGCThreads来控制GC线程数的多少。需要结合具体CPU的个数。
- Server模式下新生代的缺省收集器。
3、Parallel Scavenge收集器
parallel Scavenge收集器也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(既GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。
4、Serial Old收集器
Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器。
5、Parallel Old收集器
- 老年代版本吞吐量优先收集器,使用多线程和标记一整理算法,JVM1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。【了解既可】
- Parallel Scavenge在老年代的实现
- 在JVM1.6才出现Parallel Old
- 采用多线程,Mark-Compact算法
- 更注重吞吐量
-
Parallel Scavenge + Parallel Old = 高吞吐量,但GC停顿可能不理想
image.png
6、CMS(Concurrent Mark Sweep)收集器【特别复杂的一种收集器】
- CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,CMS收集器使用的是标记-清除算法。
- 追求最短停顿时间,非常适合Web应用。
- 只针对老年区,一般结合ParNew使用。
- Concurrent,GC线程和用户线程并发工作(尽量并发)。
Mark-Sweep。 - 只有在多CPU环境下才有意义 。
- 使用-XX:+UseConcMarkSweepGC打开。
- CMS以牺牲CPU资源的代价来减少用户线程的停顿。当CPU个数少于4的时候,有可能对吞吐量影响非常大。
- CMS在并发清理的过程中,用户线程还在跑。这时候需要预留一部分空间给用户线程。
- CMS用Mark-Sweep,会带来碎片问题。碎片过多的时候会容易频繁触发Full GC。
7、GC垃圾收集器的JVM参数定义
image.png四、Java内存泄漏的经典原因
1、对象定义在错误的范围(Wrong Scope)。
- 如果Foo实例对象的生命较长,会导致临时性内存泄漏。(这里的names变量其实只是临时作用)
-
JVM喜欢生命周期短的对象,这样做已经足够高效【调整】
image.png - 这样一改之后,只要是doIt()方法一结束names的临时变量就立马会被回收。
2、异常(Exception)处理不当。
-
错误的做法
image.png
对于有经验的程序员应该不会出现上面的问题,但是这里只是抛出泄漏的场景。
-
正确的做法
image.png
3、集合数据管理不当。
当使用Array-based的数据结构(ArrayList,HashMap等)时,尽量减少resize:
a、比如new ArrayList时,尽量估算size,在创建的时候把size确定。
b、减少resize可以避免没有必要的array copying,gc碎片等问题。
如果一个List只需要顺序访问,不需要随机访问(Random Access),用LinkedList代替ArrayList
a、LInkedList本质是链表,不需要resize,但只适用于顺序访问。
网友评论