深入理解Java虚拟机之垃圾收集器与内存分配

  • 概述
    程序计数器,虚拟机栈,本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随方法的进入和退出有条不紊地进出栈,每一个栈分配多少内存,在类的结构被确定下来就已知。因此这几个区域的垃圾回收不需要过多考虑。
    java堆和方法区,一个接口的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也不一样,只有程序处于运行期间才知道会创建哪些对象,这部分内存的内存分配和回收是动态的,垃圾收集器所关注的是这部分内存

  • 对象存活判定算法
    a.引用计数算法

    给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。

    • Java虚拟机没有选用引用计数算法的主要原因:很难解决对象之间相互循环引用的问题。

    b.可达性分析算法
    通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
    在这里插入图片描述如图Object5,6,7虽然互相有关联,但是他们到GC Roots是不可达的,则为可回收对象。
    在Java语言中,可以作为GC Roots的对象包括下面几种

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

- 谈谈引用
引用分为 强引用,软引用,弱引用,虚引用 强度依次减弱
强引用:程序中普遍存在,类似于 Object o = new Object();只要强引用还在,程序就不会回收此类的引用对象。
软引用:在程序发生内存溢出之前,Java虚拟机会GC软引用关联对象,如果GC完成了还是内存不足,则会抛出内存溢出异常。
弱引用:每次在GC的时候,Java虚拟机都会GC掉软引用对象。
虚引用:对对像的生命周期没有任何影响,设置虚引用关联的作用:在对象被回收时,会收到一个系统通知。

- 对象的生存<------>死亡

可达性算法中不可达的对象也并非非死不可。真正宣告一个对象的死亡至少会经历两次标记过程。

通过可达性分析算法分析后,如果此对象没有与之关联的GC Roots相连接的引用链,则会把此对象标记筛选,筛选的判定标准:如果此对象的finalize()方法执行过,或者对象没有覆盖finalize()方法,则标记为“没有必要执行”,否则有必要执行。

对象被判定为可执行后,会由一个F-Queue队列去管理,稍后虚拟机自动建立一个低优先级的Finalize线程去执行finalize()方法。并不会等待执行完成,避免一个对象执行慢,或者死循环导致整个过程崩溃。

在finalize()方法中会对对象进行第二次标记,如果对象重新与GC Roots相连接则可以判活,比如把自己(通过this关键字)赋值给某个类变量。

回收方法区

回收方法区就是回收永久代,这里的回收效率很低,远比不上Java堆的回收。
永久代的回收对象主要是无用类和废弃常量。
废弃常量:比如一个“abc”的String类型的字符串进入了常量池,但是系统没有任何一个String对象叫做“abc”,相当于系统没有任何对象引用常量池的“abc”,如果发生回收,“abc”就就会被清除出常量池。
常量池的其他类(接口),方法,字段的符号引用与此类似。

无用类的判定的三个条件:

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

垃圾收集算法

  1. 标记—清除算法
    在这里插入图片描述
    缺点与不足:
    1.效率问题,标记和清除两个过程效率都很低
    2.空间问题,标记和清除后会产生大量内存碎片,如果一个大对象进入内存,找不到足够大的空间存储这个大对象就不得不提前触发一次GC。
  2. 复制算法
    在这里插入图片描述
    算法思想:将内存分为容量大小相等的两块,每次使用其中的一块,当其中一块对象使用完了,就把存活对象复制到另一块,把已使用的那块内存一次清理掉。

复制算法主要应用在新生代,根据IBM公司的研究,对象98%朝生夕死。所以不需要1:1分配内存空间。

现在多采用把内存分成3块,Eden,两块survivor。Eden:survivor=8:1。则每次可使用的内存占90%,进行GC时,处理Eden和当前使用的那块survivor内存,把这两块上的存活对象复制到另外一块survivor上,最后清除掉Eden和刚才用过的那块survivor空间。

但是并不能保证每次都只有不到10%的对象存活,如果复制对象接收的那块survivor内存不够,对象就会进入老年代进行分配担保
3. 标记–整理算法
在这里插入图片描述

由于**老年代**对象存活率较高的特点,回收空间率低,不采用复制算法(浪费空间),而是采用标记整理算法。
  1. 分代收集算法
    将Java堆分为新生代和老年代,新生代采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,老年代对象存活率较高,没有空间为它作分配担保,采用标记–整理算法进行回收。

HotSpot虚拟机算法实现

1.枚举根节点
可达性分析对执行时间的敏感体现在GC停顿上。这整个分析工作在执行时,不可以出现对象引用关系还在不断变化,否则分析结果不准确。所以GC时必须停顿所有Java执行线程,也成为Stop the World,保证了准确式GC。
为了当执行系统停顿下来的时候能够不需要一个不漏地检查完所有执行上下文和全局引用的位置(GC Roots)。HotSpot虚拟机使用了OopMap这种数据结构,在虚拟机类加载完成的时候HotSpot就把对象内什么偏移量是什么数据类型计算出来,这样GC在扫描就可以直接得知信息。
2.安全点
程序将在安全点才会停顿下来GC,安全点一般选用具有长时间执行特征的指令,如方法调用,循环跳转,异常跳转等,具有这些功能的指令会产生Safepoint。
抢先式中断:发生GC时中断所有线程,有线程中断的地方不在安全点,就恢复线程让它跑到中断点。
主动式中断:设置一个标志,各个线程执行时轮询这个标志,发现中断标志为真就中断挂起。轮询标志与安全点重合。
3.安全区域
安全区域是指一段代码中,引用关系不会发生变化。在这个区域任何地方发生GC都是安全的。
线程执行到安全区域(Safe Region)时,首先标识已进入Safe Region,则在这段时间要发生GC,就不用管标识自己为Safe Region的线程了。当线程要离开Safe Region了,先检查是否完成了根节点枚举,完成了线程就继续执行,否则必须等待收到可以安全离开Safe Region的信号。

垃圾收集器

在这里插入图片描述
上图为HotSpot虚拟机中的垃圾收集器
7种不同的分代收集器,如果两个收集器之间有连线,则说明他们可以搭配使用。
上半区为新生代收集器,下半区为老年代收集器。
1.Serial收集器
Serial收集器的工作流程如下图:
在这里插入图片描述
Serial是最基本,发展历史最悠久的收集器。曾经在JDK1.3.1之前新生代收集器的唯一选择
它是单线程收集器,只会由一个CPU或者一条收集线程去完成垃圾收集工作。
不足:进行垃圾收集时必须暂停掉所有其他工作线程,直到它收集结束。
优点:简单而高效(相对于其他收集器的单线程),对于限定为单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心作垃圾收集可获得最高的单线程收集效率。
2.ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本。除了使用多条线程垃圾收集与Serial不同外,其余包括所有控制参数,收集算法,Stop the World,对象分配规则,回收策略都与Serial收集器完全一样,与Serial共用了相当多的代码。
ParNew收集器的工作流程图:
在这里插入图片描述

此垃圾收集器是许多运行在Server模式下的虚拟机首选新生代收集器,其中一个与性能无关的重要原因是除了Serial收集器外,目前只有它能和CMS收集器配合工作
ParNew收集器在单CPU环境下没有Serial效果好,默认开启的收集线程数与CPU数相同。

并发与并行
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 (不一定是同时的)
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
在这里插入图片描述
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
在这里插入图片描述

3.Parallel Scavenge收集器(新生代收集器)
与ParNew一样是多线程收集器,同样使用复制算法的收集器。
Parallel Scavenge收集器的特点是它关注于达到一个可控制的吞吐量(吞吐量=运行用户代码时间/运行用户代码时间+垃圾收集时间),所以也被称为吞吐量优先收集器。而CMS收集器是尽可能缩短垃圾收集用户线程的停顿时间
Parallel Scavenge收集器提供两个参数精确控制吞吐量,分别是控制最大垃圾收集停顿时间,直接设置吞吐量大小。
GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。
4.Serial Old收集器
Serial Old是Serial收集器的老年代版本,单线程收集器,使用标记整理算法(符合老年代特点)。
这个收集器的主要意义是给Client模式下的虚拟机使用;
在Server模式下的两用途:
1.在JDK1.5以前的版本种与Parallel Scavenge收集器搭配使用;
2.作为CMS收集器的后备预案;
在这里插入图片描述
5.Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法,JDK1.6之后开始提供。
吞吐量优先收集器组合:Parallel Scavenge+Parallel Old
在这里插入图片描述
6.CMS收集器
CMS收集器是以获取最短回收停顿时间为目标的收集器。对于一些集中在互联网站或者B/S系统的服务端上的Jva应用来说,他们注重响应速度,希望系统停顿时间最短,CMS收集器是很好的选择。
基于标记清除算法实现
CMS收集器的运作过程:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除
    初始标记与重新标记需要Stop the World。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记进行GC Roots Tracing的过程;重新标记则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段时间比初始标记时间长,比并发标记时间短。
    由于整个过程耗时最长的并发标记和并发清除都可以与用户程序并发工作,所以整体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的。
    在这里插入图片描述
    CMS是一款优秀的收集器:并发收集,低停顿。
    CMS收集器的三个明显缺点:
  • .CMS收集器对CPU资源非常敏感,可以从并发来想
  • CMS收集器无法处理浮动垃圾。由于在并发清理阶段,用户线程也在运行,这一部分垃圾出现在标记之后,所以无法处理,只好留待下一次GC时清理,称为浮动垃圾。
  • 由于基于标记清除算法实现,所以会出现内存碎片。如果大对象在老年代没有足够的内存空间,不得不提前触发Full GC。虽然设置了碎片合并整理的开关参数,碎片问题解决了,停顿时间就不得不变长。
    7.G1收集器(收集器前沿成果之一)
    面向服务端应用的垃圾收集器。具备以下特点:
  • 并发与并行:有些收集器需要停顿的过程G1仍然可以通过并发的方式让用户程序继续执行;
  • 分代收集:可以不使用其他收集器配合管理整个Java堆;
  • 空间整合:使用标记-整理算法,不产生内存碎片;
  • 可预测的停顿:G1除了降低停顿外,还能建立可预测的停顿时间模型;

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

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了有限时间最高的收集效率。

G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)
    初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短

  • 并发标记(Concurrent Marking)
    并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

  • 最终标记(Final Marking)
    最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

  • 筛选回收(Live Data Counting and Evacuation)
    筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,**这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分价值高的Region区的垃圾对象,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。**回收时,采用“复制”算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存。
    在这里插入图片描述
    内存分配策略
    概述:对象的内存分配,从大方向上讲就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代种,分配的规则不100%固定,取决于垃圾收集器的组合,虚拟机与内存相关的参数设置。
    1.对象优先在Eden上分配,Eden空间不足发起Minor GC。
    2.大对象直接进入老年代,大队现象需要大量连续的内存空间的Java对象,如很长的字符串以及数组。
    3.长期存活的对象将进入老年代。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor空间容纳的话,将移入Survivor,对象年龄设为1,每熬过一次Minor GC年龄加1,年龄增加到15(默认),将晋升到老年代。
    动态对象年龄判定
    如果在Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一般,年龄大于等于该年龄的对象都可以直接进入老年代。
    空间分配担保
    在发生Minor GC之前,虚拟机现场检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,Minor GC是安全的,如果不成立,查看HandlePromotionFailure设置值是否允许担保失败。允许,则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代的平均大小,如果大于,尝试Minor GC,有风险如果还是不能容纳就会Full GC,如果小于HandlePromotionFailure设置为不允许冒险,执行Full GC。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值