垃圾收集器与内存分配策略

垃圾收集器与内存分配策略

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来.

GC需要完成的三件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

哪些内存需要回收?

无用的对象会被回收,即不可能再被任何途径使用的对象.

如何判定一个对象是否可用?

  1. 引用计数算法
  2. 可达性分析算法

引用计数算法给对象添加一个引用计数器,为0时代表这个对象不可能再被使用,优点是简单快捷,缺点是无法处理循环引用的对象.

可达性分析算法的基本思路就是通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的.优点是解决了循环引用问题,

JVM采用的是可达性分析算法.

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象.
  • 方法区中类静态(static)属性引用的对象.
  • 方法区中常量(final)引用的对象.
  • 本地方法栈中JNI(Native方法)引用的对象.

为了管理对象的生命周期,Java将引用分为强引用,软引用,弱引用,虚引用4种,这4种引用强度依次逐渐减弱.

强引用(Strong Reference):不会被回收的对象,类似于new出来的对象.

软引用(Soft Reference):用来描述一些还有用并非必需的对象.在将要发生内存溢出之前回收.对应SoftReference类

弱引用(Weak Reference):用来描述非必需的对象,下一次垃圾收集时被回收.对应WeakReference类

虚引用(Phantom Reference):也称为幽灵引用幻影引用,一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用取得一个对象实例,为对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知.对应PhantomReference类.

宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法.当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机执行过,虚拟机将这两种情况都视为没有必要执行.

可以通过在finalize()方法中将对象赋给别的引用来进行自救.不推荐.


永久代(方法区)的垃圾收集主要回收两部分内容:废弃常量无用的类.

满足三个条件才算是无用的类:堆中没有该类的实例,加载该类的ClassLoader已经被回收,该类对应的Class对象没有在任何地方被引用且无法在任何地方通过反射访问该类的方法.


如何回收?

垃圾收集算法有

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法.

标记-清除算法:分为标记和清除两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象.它的主要不足有两个:一个是效率问题,标记和清除两个过程效率都不高,一个是空间问题,标记和清除之后会产生大量不连续的内存碎片,导致运行时需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作.

复制算法:(主要用于回收新生代)为了解决效率问题,提出了复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块.当这一块的内存用完了,就将还存活着的对象按顺序复制到另外一块上面,然后再把已使用过的内存空间一次清理掉.解决了碎片问题.简单高效.代价是将内存缩小为了原来的一半,代价太高,因此将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor.当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间.HotSpot虚拟机默认Eden和Survivor的大小比例是8:1.当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保.分配担保就是当另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代.

标记-整理算法(主要用于老年代):标记过程和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存.

分代收集算法根据对象存活周期的不同将内存划分为几块,一般把堆分为新生代老年代,这样就可以根据各个年代的特点采用最适当的收集算法.新生代选用复制算法,老年代使用标记-清理或者标记-整理算法.


什么时候回收?

枚举根节点:可达性分析算法从GC Roots节点找引用链这个过程必须在一个能确保一致性的快照中进行,这导致GC进行时必须停顿所有Java执行线程(称为Stop The World),目前主流的Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置.虚拟机应当有办法直接得知哪些地方存放着对象的引用.在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的.

安全点(Safepoint):在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举.但如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会非常高.所以HotSpot只是在特定的位置记录了这些信息,这些位置称为安全点,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停.安全点的选定基本上是以程序"是否具有让程序长时间执行的特征"为标准进行选定的.长时间执行的最明显特征就是指令序列复用,例如方法调用,循环跳转,异常跳转等.所以具有这些功能的指令才会产生安全点.

对于安全点,另一个需要考虑的问题就是如何在GC发生时让所有线程都"跑"到最近的安全点上再停顿下来,有两种方案:抢先式中断主动式中断.

  • 抢先式中断:在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它"跑"到安全点上.现在几乎没有虚拟机使用.
  • 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起.轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方.

线程处于Sleep状态或者Blocked状态时,线程无法响应JVM的中断请求,"走"到安全点去中断挂起,这种情况下,就需要安全区域(Safe Region)来解决.

安全区域:是指在一段代码片段中,引用关系不会发生变化.在这个区域中的任意地方开始GC都是安全的.

在线程执行到安全区域中的代码时,首先标识自己已经进入了安全区域,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为安全区状态的线程了.在线程要离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开安全区域的信号为止.


垃圾收集器

HotSpot虚拟机下的垃圾收集器:

连线代表可以搭配使用,所处区域代表可工作与新生代还是老年代,亦或者皆可.

Serial收集器(新生代):单线程,使用复制算法,它只会使用一个CPU或一条收集线程去完成垃圾收集工作,它在进行垃圾收集时,必须暂停其他所有的工作线程(Stop The World),直到它收集结束,它是虚拟机运行在Client模式下的默认新生代收集器.优点是简单高效(与其他收集器的单线程比).

ParNew收集器(新生代):多线程,使用复制算法,它是ParNew收集器的多线程版本.缺点是受CPU数量影响,数量越少性能越差,反之越好,它默认开启的收集线程数与CPU的数量相同.使用-XX:ParallelGCThreads参数限制垃圾收集的线程数.

Parallel Scavenge收集器(新生代):多线程,使用复制算法,关注吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),适用于运算任务,也称为吞吐量优先收集器,两种优化目标:-XX:MaxGCPauseMillis参数可以控制停顿时间,-XX:GCTimeRatio参数可以控制吞吐量,使用-XX:+UseAdaptiveSizePolicy参数可以开启GC自适应的调节策略(自动调整新生代大小,新生代内部比例大小,晋升老年代对象大小等)

Serial Old收集器(老年代):单线程,使用标记-整理算法,主要用于Client模式下的虚拟机使用,在Server模式下,可作为CMS收集器的后备预案.

Parallel Old收集器(老年代):多线程,使用标记-整理算法,是Parallel Scavenge收集器的老年代版本,关注吞吐量.

CMS收集器(老年代):多线程,使用标记-清除算法,运作过程分为4个步骤:初始标记,并发标记,重新标记,并发清除.优点:并发收集,低停顿.也称为并发低停顿收集器.缺点:1.CMS收集器对CPU资源非常敏感,CPU少于四个会导致吞吐量降低,默认开启的收集线程数是(CPU数量+3)/4, 2.CMS无法处理浮动垃圾,老年代在92%的空间被占用后就会被启动,如果启动后无法获取足够空间,会启动后备预案Serial Old收集器利用标记-整理算法重新进行垃圾收集,3.标记-清除会产生大量空间碎片,

G1收集器(整个堆):从整体看,是基于标记-整理算法实现的收集器,从局部(两个Region)上来看是基于复制算法实现的.它将整个java堆划分为多个大小相等独立区域(Region).它可以避免在整个堆中进行全区域的垃圾收集.G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先队列,每次根据允许的收集时间,优先回收价值最大的Region.G1收集器的运作步骤:初始标记,并发标记,最终标记,筛选回收.如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处.


理解GC日志

使用-XX:+PrintGCDetails参数可以打印详细GC日志

下面是一段GC详细日志:

[GC (Allocation Failure) [PSYoungGen: 5354K->504K(6144K)] 5354K->3721K(19968K), 0.0489109 secs] [Times: user=0.03 sys=0.00, real=0.05 secs] 
[GC (Allocation Failure) [PSYoungGen: 6046K->504K(6144K)] 9263K->8256K(19968K), 0.0101421 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 6136K->0K(6144K)] [ParOldGen: 10917K->12842K(13824K)] 17053K->12842K(19968K), [Metaspace: 2575K->2575K(1056768K)], 0.2218310 secs] [Times: user=0.48 sys=0.00, real=0.22 secs] 
[Full GC (Ergonomics) [PSYoungGen: 3556K->3001K(6144K)] [ParOldGen: 12842K->13354K(13824K)] 16399K->16355K(19968K), [Metaspace: 2575K->2575K(1056768K)], 0.1863928 secs] [Times: user=0.61 sys=0.02, real=0.19 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 3001K->3001K(6144K)] [ParOldGen: 13354K->13342K(13824K)] 16355K->16343K(19968K), [Metaspace: 2575K->2575K(1056768K)], 0.1529951 secs] [Times: user=0.36 sys=0.00, real=0.15 secs]

开头的GC和Full GC的区别是什么?

GC和Full GC说明的是这次垃圾收集的停顿类型,而不是用来区分新生代GC和老年代GC的,Full GC说明这次垃圾收集是发生了Stop The World的,反之没有.如果是调用System.gc()方法所触发的收集,将会显示[Full GC(System).

GC日志的基本格式:

[GC (Allocation Failure) [PSYoungGen: 5354K->504K(6144K)]                                      5354K->3721K(19968K), 0.0489109 secs]                                  [Times: user=0.03 sys=0.00, real=0.05 secs] 
[停顿类型(描述)            [GC发生的区域:GC前该区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)]  GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量),该区域回收所占用的时间]  [Times:  用户态消耗的CPU时间,内核态消耗的CPU时间,操作从开始到结束所经过的墙钟时间]

Java语言倡导自动内存管理,分为两个部分给对象分配内存回收给分配给对象的内存.由垃圾收集器负责回收,由JVM负责分配内存,接下来讲一讲比较普遍的内存分配规则.

对象优先在Eden分配,当Eden区空间不足时,虚拟机将发起一次Minor GC.

大对象直接进入老年代,大对象指需要大量连续内存空间的Java对象,比如很长的字符串以及数组,可能导致提前触发GC,通过-XX:PretenureSizeThreshold参数(针对Serial,ParNew)可以令大于这个设置值的对象直接在老年代分配,可以避免在Eden区及两个Survivor区发生大量的内存复制

长期存活的对象将进入老年代,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,就将对象年龄设置为1,对象在Survivor区中每熬过一次Minor GC,年龄增加1岁,当它的年龄增加到一定程度(默认15岁),就会被晋升到老年代中.年龄阈值可以通过-XX:MaxTenuringThreshold设置.

动态对象年龄判定:虚拟机并不是永远要求对象的年龄必须达到阈值才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于单个Survivor空间大小的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到年龄大于阈值.

空间分配担保:在Minor GC之前,虚拟机会检查,只要老年代的剩余最大连续空间大于新生代对象总大小或者历次晋升对象的平均大小就会进行Minor GC,否则进行Full GC.可以避免Full GC过于频繁.

  • 如果老年代的剩余最大连续空间>新生代对象总大小,则这次Minor GC可以确保是安全的.进行Minor GC.
  • 如果老年代的剩余最大连续空间>历次晋升对象的平均大小,则这次Minor GC是有风险的,但还是尝试进行一次Minor GC,如果担保失败,则在失败后重新发起一次Full GC.
  • 两者都不成立,则认为是不安全的,直接进行Full GC

空间分配担保是为了保证在极端情况下,使用复制算法进行Minor GC后,大量对象仍然存活,导致Survivor无法容纳,进而使用老年代进行担保.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值