JVM垃圾回收模型

2020-12-20  本文已影响0人  得力小泡泡

一、垃圾判断算法

1、引用计数算法
2、根搜索算法

既然引用计数算法存在对象循环引用的问题,所以此算法出现了,下面具体看下该算法:
①、在实际的生产语言中(Java、C#等),都是使用根搜索算法判断对象是否存活。
②、算法基本思路就是通过一系列的称为“GC Roots”的点作为超始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。
③、在Java语言中,GC Roots包括【也就是什么是GC Roots?】:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。

image.png

二、GC算法

1、标记-清除算法(Mark-Sweep)

1、算法分为“标记”“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象。

2、缺点:

下面用图表说明一下整个算法的过程,首先初始内存为:


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、复制收集算法在对象存活率高的时候,效率有所下降。,因此老年代一般不嫩直接使用这种算法

image.png image.png image.png

特点:
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)
一般情况

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)
Full GC【这个在实际中一定得要避免】

这个是对整个堆进行整理回收的方法,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。

触发时机:

三、垃圾回收器的实现和选择

每个蓝色的盒子都代表了一个收集器,用来收集某一代。黄色区域中的蓝色盒子是用来收集新生代的,灰色区域中的蓝色盒子是用来收集老年代的。


image.png

怎么选择GC回收器

最小化地使用内存和并行开销,新生代和老年代分别选择Serial GC 和 Serial Old
最大化地使用应用程序的吞吐量,新生代和老年代分别选择Parallel Scanvenge 和 Parallel Old
最小化GC的中断或停顿时间,新生代和老年代分别选择ParNew 和 CMS

垃圾收集器的“并行”和“并发”
1、Serial收集器
2、ParNew收集器
3、Parallel Scavenge收集器

parallel Scavenge收集器也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(既GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。

4、Serial Old收集器

Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器。

5、Parallel Old收集器
6、CMS(Concurrent Mark Sweep)收集器【特别复杂的一种收集器】
7、GC垃圾收集器的JVM参数定义
image.png

四、Java内存泄漏的经典原因

1、对象定义在错误的范围(Wrong Scope)。

image.png

2、异常(Exception)处理不当。

3、集合数据管理不当。
当使用Array-based的数据结构(ArrayList,HashMap等)时,尽量减少resize:
a、比如new ArrayList时,尽量估算size,在创建的时候把size确定。
b、减少resize可以避免没有必要的array copying,gc碎片等问题。
如果一个List只需要顺序访问,不需要随机访问(Random Access),用LinkedList代替ArrayList
a、LInkedList本质是链表,不需要resize,但只适用于顺序访问。

上一篇 下一篇

猜你喜欢

热点阅读