JVM介绍篇二:垃圾回收器与内存分配策略

csdn中看到了很多关于JVM介绍的,但是并没有看到什么讲解很好的资料
所以这里自己写一个记录下,方便日后工作中需要时可以查阅

概述

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

对象已死吗?

在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

引用计数法

给对象添加一个引用计数器。但是难以解决循环引用问题。

可达性分析法

通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

可作为 GC Roots 的对象:

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

再谈引用

前面的两种方式判断存活时都与‘引用’有关。但是 JDK 1.2 之后,引用概念进行了扩充,下面具体介绍。
下面四种引用强度一次逐渐减弱

  • 强引用
    最传统的引用定义 ,是指代码程序中普遍存在的引用赋值,类似于 Object obj = new Object()这种引用关系,只要强引用在就不回收

  • 软引用
    SoftReference类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收

  • 弱引用
    WeakReference类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

  • 虚引用
    PhantomReference类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。

当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
finalize() 方法只会被系统自动调用一次。

如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。

运行代价高昂,不确定性大,无法保证各对象调用顺序,如今官方明确声明不推荐使用的语法。

回收方法区

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。

永久代垃圾回收主要两部分内容:废弃的常量和无用的类

判断废弃常量:一般是判断没有该常量的引用。

判断无用的类:要以下三个条件都满足:

  • 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法

垃圾回收算法

复制算法

把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。

解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间

可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将
Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor
空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10%
怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。

标记-清除算法

直接标记清除就可。

两个不足:
效率不高
空间会产生大量碎片

标记-整理算法

不同于针对新生代的复制算法,针对老年代的特:存活率高,创建该算法。主要是把存活对象移到内存的一端。

分代回收

根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。

新生代:
每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。

老年代:
老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记—清除 或者 标记—整理 算法回收。

Parallel Old收集器是基于标记-整理算法(内存回收时复杂),注重吞吐量
CMS收集器是基于标记-清除算法(内存回收时简单),注重延迟

垃圾回收器

这里只介绍两种目前常用的垃圾回收器,想具体了解可参考[Java基础]-- Java GC 垃圾回收器的分类和优缺点

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

G1收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

其他的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

垃圾回收器如何选择

CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
CPU多核,关注吞吐量 ,那么选择PS+PO组合。
CPU多核,关注用户停顿时间,可用内存低于6G,那么选择CMS。
CPU多核,关注用户停顿时间,JVM可用内存6G以上,那么选择G1。

G1优势: 可指定最大停顿时间,分Region内存布局,按收益动态确定回收空间,基于标记整理算法

G1劣势: 垃圾收集的内存占用和运行时额外负载更高

内存分配与回收策略

对象优先在 Eden 分配

对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。

一般来说 Java 堆的内存模型如下图所示:
在这里插入图片描述

新生代 GC (Minor GC)

发生在新生代的垃圾回收动作,频繁,速度快。

老年代 GC (Major GC / Full GC)

发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

大对象直接进入老年代

指定大于该数值的对象直接进入老年代,避免在新生代的Eden和两个Survivor区域来回复制,产生大量内存复制操作。

长期存活的对象将进入老年代

虚拟机为每个对象定义了一个对象年龄计数器。

如果对象在Eden出生并经过一次Minor GC仍然存活,并且能被Survivor容纳,将被移动到Survivor区,并且对象年龄设置为1.对象每经过一次Minor GC后仍保持存活,年龄+1

当对象年龄到达一定程度(一般15岁),那么它会晋升到老年代。对象晋升的年龄限制 -XX:MaxTenuringThreshold设定。

动态对象年龄判定

正常情况下, 年轻代空间里边的对象熬过 MaxTernuringThreshold 才能晋升到老年代;但是如果 survivor 空间中
age1 + age2 +age3 +…. + agen (n>1)的这些对象大小的总和大于 survivor 空间的一半, 年龄大于或等于该年龄 n 的对象就会直接进入到老年代,不需要熬过那么多年龄

空间分配担保

JVM使用分代收集算法,将堆内存划分为年轻代和老年代,两块内存分别采用不同的垃圾回收算法,空间担保指的是老年代为新生代进行空间分配担保。
 
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,

  • 如果大于,则此次Minor GC是安全的

  • 如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值