首页 > 技术文章 > 垃圾收集器和内存分配策略

xiaofeiyang 2020-03-07 22:07 原文

 

了解垃圾收集器和内存分配的意义,处理内存溢出、内存泄露问题、垃圾收集系统成为高并发系统的瓶颈时就需要调节自动化技术实施细节

1、哪些内存需要回收

引用计数法、可达性分析算法,GCRoots作为根节点,向下搜索所走过的路径称为引用链,没有任何引用链说明不可达。可以作为GCRoot的对象有虚拟机栈本地变量表中引用的对象,

方法区类静态属性引用对象,方法区常量引用的对象,本地方法栈中JNI引用对象

 

2、什么时候回收

 

强引用只要引用还存在,就不会回收。

软引用内存不够就回收,够就不回收。

弱引用内存够不够都回收。

虚引用,就是垃圾回收时会收到一个通知。

 

方法区回收

所有实例回收,java堆中不存在该类的实例,加载该类的classloader也回收。

 

3、怎么回收

垃圾回收算法有如下几种

1、标记清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。不足有两处效率问题,标记和清除两个过程效率都不高,空间问题会产生大量不连续的内存空间。大对象分配时,无法找到

足够的内存空间触发,提前触发另一次垃圾收集动作。

2、复制算法

内存分为两个相同大小,每次只使用一块内存。一块内存使用完毕后就将所有还存活的对象都复制到另外一块上,再将已经使用的那一块一次清除掉。每次对整个半区进行内存回收,

不用考虑内存碎片等复制问题,实现简单运行高效,只是将内存缩小了一半。一般新生代都采用这种算法来进行收集。新生代基本98%的朝生夕死,不需要按照1:1进行分配

将内存分为一块较大的Eden和两块较小的survivor,每次使用Eden和其中的一块survivor。当回收时将Eden和survivor复制到另外一块中,然后清除掉Eden和survivor。survivor不足时需要依赖

其他内存进行内存分配担保,主要是老年代。如果另外一块survivor没有足够内存存放上一次垃圾回收存活的对象,那么这些对象会直接进入老年代。

3、标记整理算法

复制收集算法在对象存活率较高的情况下进行复制,效率就很低,如果不想浪费50%空间就需要进行额外空间担保,以应对被使用的内存中所有对象都被100%存活的极端案例,老年代一般不采用这种算法。

标记整理算法的后一段就是直接对所有存活对象移动到内存的一端,然后直接清除掉端边界另外一边所有内存。

4、分代收集算法

根据对象存活周期的不同将内存划分为几块,一般是把java堆划分为新生代和老年代,然后根据每个代的不同来使用不同的垃圾收集算法。新生代每次垃圾回收大批量对象死去,少量存活就采用复制算法。

只需要付出少量存活对象复制成本就可以完成收集。老年代中的因为存活率高,没有额外空间进行内存分配担保,就必须使用标记清理和标记整理算法进行回收。

 

4、HotSpot的算法实现

1、枚举根节点

从可达性分析中从GCRoots节点找引用链这个操作为例,可以作为GCRoots的节点,主要在全局性的引用与执行上下文中,很多应用仅仅方法区就有数百兆,如果要检查这里面的引用,那么必然消耗很多时间。

可达性分析执行必须确保在一个一致性的快照里面进行,这里的一致性意思整个系统看起来冻结在某一个时间点,不可以出现分析过程引用还在不断变化。这点就是gc进行时必须停顿所有进程,号称不停顿

的cms在枚举根节点时也必须停顿。不用一个不漏的检查所有全局引用位置和上下文。hotspot中采用一个oopmap数据结构达到这个目的,在类加载完毕,hotspot就把对象内什么偏移量上存放什么类型的数据

计算出来,在jit过程中也会在特定位置记录下栈和寄存器中哪些位置的引用

2、安全点

hotspot不会为每条指令记录oopmap,只会在安全点才能停下来记录并且开始GC。如何让所有线程处于安全点,有两种方案抢占式中断,就是将所有线程全部中断,如果有线程不在安全点,就恢复线程,让他

跑到安全点,没有虚拟机选择抢占式中断。主动式中断就是设置一个标志,各个线程主动轮询这个标志,轮询时间点和安全点重合,再加上创建对象分配内存的地方。

3、安全区域

安全点似乎已经完美解决了怎么进入GC的问题,safepoint保证GC在程序执行时不太长时间就能进入到安全点。当程序没有执行就是没有分配cpu执行时间,例如sleep和block状态,这时线程不太会响应

JVM的中断请求,走到安全点去挂起。安全区域就是一段代码中,引用关系不会发生变化。当线程执行safe region中代码时,首先标识自己已经进入安全区域,gc进行垃圾回收时不用管进入安全区域的,线程

离开安全区域要检查是否已经完成枚举根节点,如果完成那线程就继续运行,否则就必须等待到可以离开安全区域为止。

 

5、垃圾收集器

 

 1、Serial收集器

单线程,必须停止所有线程去完成所有垃圾收集工作。Serial 新生代采用复制算法,Serial Old老年代采用标记整理算法。

2、ParNew收集器

多线程,必须停止所有线程去完成所有垃圾收集工作。ParNew新生代采用复制算法,多线程工作,Serial Old老年代采用标记整理算法,单线程工作

3、Parallel Scavenge

多线程,必须停止所有线程去完成所有垃圾收集工作。Parallel Scavenge新生代采用复制算法,多线程工作,Serial Old老年代采用标记整理算法,单线程工作。可以控制吞吐率和最大垃圾回收停顿时间

4、Parallel Old

多线程,必须停止所有线程去完成所有垃圾收集工作。Parallel Scavenge新生代采用复制算法,多线程工作,Paralle Old老年代采用标记整理算法,多线程工作。

5、CMS

1)初始标记

需要停止所有线程,只是标记GCroot能关联到的对象,速度很快,单线程

2)并发标记

GCRoot Tracing的过程,单线程

3)重新标记

需要停止所有线程,修正并发标记期间用户程序继续运作导致标记产生变动的那一部分对象的标记记录。多线程

4)并发清除

不需要暂停所有线程。单线程

无法处理浮动垃圾。因为与用户线程一起运行,所以不能等到老年代快满了才开始运行。jdk1.5老年代68%就会启动垃圾回收。采用标记清除算法会产生大量内存碎片,

大对象分配很麻烦,会造成老年代内存足够,但是无法找到合适空间来分配对象,触发full GC。

6、G1收集器

将内存划分为多个区域,新生代,老年代都是多个区域的集合。跟踪所有区域垃圾回收的价值大小,回收后收获的空间大小和回收所需要时间,维护一个优先列表,然后根据回收允许时间

优先回收价值最大的region。G1收集器通过remember set来避免全堆扫描,每一个region对应一个remenber set,虚拟机发现程序对reference类型进行写操作的时候会产生一个write中断,

检查reference引用的对象是否处于不同region中,如果是就记入remenberset。

1)初始标记

2)并发标记

3)最终标记

4)筛选回收

 

6、理解GC日志

 

 1、33.125,100.667是垃圾回收时间,就是jvm启动到回收的时间的秒数。

2、GC,FullGC表示发送垃圾回收的停顿类型,full 就表示停止了所有线程

3、[DefNew,[Tenured [Perm表示内存GC区域,区域和垃圾收集器强相关,Serial 叫做DefNew,ParNew收集器就是[ParNew如果是Parallel Scavenge

收集器,它配套名称叫做[PSYoungGen

4、后面方括号表示收集钱内存区域占了多少,收集后使用量和堆内存总数量,0.0025925表示该内存区域已经回收该内存区域占用时间。

推荐阅读