【学习笔记】—JVM(二)垃圾收集器和内存分配策略

一、为什么要回收

  在上一部分在对每个数据区分析了,最后指出了他会抛出什么异常,最多的就是OOM,内存溢出异常。在Java堆中一个程序要创建太多的实例对象,但有些数据只用了一次之后却再没有使用,如果不将它清除掉,对内存而言永远是不够用的。

二、如何判断对象不再使用,需要清理

1.引用计数法算法

给对象添加一个引用计数器,每当有一个地方引用它时计数器加1,失效时减1;为0时则不会再被使用。

Java虚拟机没有选用这个方式,因为它很难解决对象相互循环引用的问题。

Object a = new Object();
Object b = new Object();
a.name = b;
b.name = a;

这两个之间都不可能在被访问到,理应清楚,但是相互引用,都没有失效,计数器不会为0,所以不会被清楚。

2.可达性分析算法

通过一些被称"GC Roots"的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

可达性分析算法
Java中可作为GC Root的对象包括下面几种:
  - 虚拟机栈中的引用对象。
  - 方法区中静态属性引用的对象。
  - 方法区中常量引用的对象。
  - 本地方法栈中JNI(Native方法)引用的对象。

3.再给你一次机会

  被可达性分析算法标记为不可达的对象中,有些并不是马上就回收了,至少经历两次标记才真的被回收:
    第一次标记进行筛选,条件是该对象有没有finalize()方法,没有就是真的没用的了。有的话将其放入一个叫Q-finalize的队列。
    第二次标记对其调用方法(并不会承诺等待它运行结束,避免如果进入死循环等永久等待的情况),只要他的方法中与其他对象建立了关系,那他就不会被回收。每个对象的finalize()方法只会被调用一次,第二次之后会视为他没有finalize()方法。
  finalize()方法运行代价高昂,不确定性大,无法保证每个对象的调用顺序。所以一般用try-finally或者其他方法代替。

三、垃圾收集算法

1.标记-清除算法

  Mark-Sweep,直接上图
标记-清除算法
  不足
    - 效率低下
    - 产生空间碎片,由图可知,虽然清理后有了很多空间,但这时有一个较大的对象需要分配时就要再回收一次垃圾。

2.复制算法

  Conping 将内存划分为两部分,每次使用其中一部分。快要用完时将还活着的复制到另一块上面去,并对已经使用过的空间一次清理掉。运行高效,实现简单
复制算法
  不足:
    - 代价昂贵,所以一般不划分一半,而是划分为一块较大的Eden空间和两个较小的Survivor空间默认比例时8:1。当Survivor空间不够用时,需要其他内存(老年代)进行分配担保。

3.标记-整理算法

  Mark-Compact
标记-整理算法
  一般根据每一代的特性来选择不同的算法的进行分代收集,新生代用复制算法,老年代使用标记-整理或者标记-清除算法

新生代:主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
老年代:主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。

四、算法的实现(HotSpot)

1.枚举根节点

  上文中的可达性分析GC Root找引用链,这一瞬间必须在一个能确保一致性的快照之中进行。"一致性"指整个系统冻结在一瞬间,不可以出现分析过程中对象的引用关系还在不断变化的情况,称之为"Stop The Word",对用户来说显然不能停顿太久,所以不可能一个不漏的去检查完所有执行上下文和全局的引用变量而是使用一组叫做OopMap的数据结构来实现

OopMap:记录了栈上本地变量到堆上对象的引用关系。

2. 安全点

  通过OopMap可以快速完成GC Roots的枚举,但是为每一条指令都生成对应的OopMap成本太高。其实也没有每条都生成,OopMap只是在特定的位置生成记录。而这些特定的位置就叫做"安全点"。即程序执行时并不是在所有地方都停顿下来开始GC,而是在到达安全点的时候才能暂停
  选定标准:是否具有让程序长时间执行的特征。
  如何让所有线程都到安全点上再停下来:
    - 抢断式中断
      在GC发生时,把所有线程先中断,如果不在安全点上则让线程继续执行,直到安全点为止。(现在几乎没有用这种方式的)
    - 主动式中断
      设置一个轮询标志,和安全点是重合的,每个线程执行时主动去轮询这个标志,当发现中断标志为真的时候,就自己主动中断挂起。这样就实现了线程都在安全点停下来

3.安全区域

  线程运行时可以到达安全点自己中断,但是不执行的线程呢,处于Sleep,Block的线程。设置一个安全区域,在这个区域中的任意地方都是安全的,GC时不会去管它。但是当一个线程要从安全区出来时,就要先检查GC是否完成,没有完成就需要等待完成才能出来。

五、垃圾收集器

1.Serial收集器

  新生代,复制算法,单线程
在这里插入图片描述
  缺点:在GC时必须停止其他所有的工作线程,单核嘛
  最主要的优点:简单而高效
  适用于Client模式下的虚拟机

Client模式:启动的JVM采用的是轻量级的虚拟机,更注重编译的速度,启动快。更适合在客户端的版本下
Server模式:启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化,启动慢,但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多,更注重编译的质量,多用于服务端

2.Serial Old收集器

  老年代,标记-整理算法,单线程。
  可以和Parallel Scavenge搭配使用。

3.ParNew收集器

  新生代,复制算法,多线程。
  简单来说就是Serial的多线程版本
在这里插入图片描述
  除了Servial收集器外,目前只有他能和CMS搭配使用
  适用于Server模式的虚拟机

4.Parallel Scavenge收集器

  新生代,复制算法,多线程
  上面的收集器都是注重尽可能的缩短Stop The Word的时间,而Parallel Scavenge注重的是达到一个可控制的吞吐量

吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

在这里插入图片描述
  主要适用在后台运算而不需要太多交互的任务

5.Parallel Old收集器

  老年代,标记-整理算法,多线程
  Parallel Scavenge收集器的老年代版本

6.CMS收集器

  老年代,标记-清除算法
CMS示意图
  分为一下4个步骤
    1.初始标记
    2.并发标记
    3.重新标记
    4.并发清除
  其中初始标记和重新标记需要Stop The Word,但是很快,主要时间是在并发标记上,但是并发标记又是与线程并发进行的。
  缺点:
    - 对CUP资源非常敏感,虽说是并发进行的不会StopTheWord,但是占用了一部分的CPU资源,会导致应用程序变得缓慢,总吞吐量降低。
    - 整理-清除算法会产生碎片
    - 无法处理浮动垃圾,因为并行运行,你妈打扫房间,你一边嗑瓜子,她只有先打扫一边,你新产生的垃圾只有等那边打扫完了才能过来打扫。

7.G1收集器

  范围是整个Java堆,将新生代,老年代划分为多个大小相等的Region,G1跟踪各个Region里面的垃圾堆积的价值大小(回收空间大小以及所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
在这里插入图片描述
  大致分为以下几个步骤
    1.初始标记
    2.并发标记
    3.最终标记
    4.筛选回收

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值