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

前言:

好像蛮久没有更新《深入理解jvm》这本书了,又回来填坑了。

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

程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭。这几个区域的内存分配和回收都具备确定性,在这几个区域基本不需要考虑回收问题。因为方法结束或者线程结束,内存自然就回收了。而java堆和方法区则不一样。方法区存放类信息,一个接口中的多个实现类需要的内存不一样,并且只有在程序运行期间才知道会创建哪些对象。这部分内存的分配和回收都是动态的,所以垃圾回收器所关注的就是java堆和方法区。

如何判断对象已死
引用计数算法

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

不过java不是用此算法,因为此算法难以解决对象之间相互循环引用问题。

例如:

public class Person {
    public Object instance=null;

    public static void main(String[] args) {
        Person personA=new Person();
        Person personB=new Person();
        personA.instance=personB;
        personB.instance=personA;
        personA=null;
        personB=null;
        System.gc();
    }
}

看上方代码可以开出对象personA和personB都有字段instance,并且相互引用,但是这两个对象已经不可能再被访问了,但是他们因为互相引用着对方,导致引用计数器不为0,于是引用计数算法无法通知gc收集器回收他们。

可达性分析算法

此算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

作为GC Roots对象包括:

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

定义:

如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

引用分为四类:

  • 强引用

    是指代码中普遍存在的,类似“Object obj=new Object()”这一类的引用,只要强引用还在,垃圾收集器就永远不会回收掉被引用的对象。

  • 软引用

    用来描述一些还有用但并非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列出回收范围之中进行第二次回收。SolftReference类实现软引用

  • 弱引用

    用来描述非必需对象,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference类来实现弱引用

  • 虚引用

    也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,PhantomReference来实现虚引用

对象死亡过程

需要经历两次标记过程:

  • 一、如果一个对象在经过可达性分析后发现没有与GC Roots相连接的引用链,那么它将会被第一次标记并且进行一次筛选。筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法或者finalize方法已经被虚拟机调用过,虚拟机都将这两种情况是为“没有必要执行”
  • 二、如果这个对象被判定有必要执行finalize()方法,那么这个对象将会放置在F-Queue的队列之中,并稍后有一个虚拟机自动建立的、低优先级的finalizer线程去执行。finalise方法是对象最后一次拯救自己的机会,稍后gc会对f-queue中的对象进行第二次小规模标记,如果对象要在finalize中成功拯救自己—需要重新与引用链上的人和一个对象建立关联即可(例如把自己赋值给某个类变量或者对象的成员变量),那么在第二次标记的时候就会移出集合;如果此时对象还没有逃脱,那么基本上就真的被回收了。

流程:一个对象没有与gc roots相连接的引用链-------->筛选有无执行过finalize()--------->有则被放置在F-Queue的队列,无则直接回收------->虚拟机执行finalize()方法(对象可在此处重新建立连接逃脱回收)------->对F-Queue队列中对象进行二次标记-------->ggggggg 垃圾回收了

finalize()有一个特点就是: JVM始终只调用一次. 无论这个对象被垃圾回收器标记为什么状态, finalize()始终只调用一次. 但是程序员在代码中主动调用的不记录在这之内.

回收方法区

永生代的垃圾收集主要回收两个部分:废弃常量和无用的类。

回收废弃常量:回收废弃常量和回收java堆中的对象非常相似,例如string,一个字符串“abc”已经进入了常量池,当前系统中没有任何一个string对象叫做“abc”,即没有任何地方引用这个字符串。如果发生内存回收,这个“abc”就会被系统清理出常量池。

回收无用的类:需要满足三个条件

  • 该类的所有实例都已经被回收,也就是java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。(通常在对象中保存有,或者一些反射的场景中有)

垃手收集算法

注意:先说明一下新生代、老年代、永生代的概念

堆中分为新生代–一般采用复制算法 和老年代-一般采用标记整理算法、标记清除算法

而方法区才是永生代。

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。 Major GC 是清理永久代。Full GC 是清理整个堆空间—包括年轻代和永久代。

标记-清除算法

分为标记和清除两个阶段,首先标记出所有需要回收的对象。在标记完成后统一回收所有被标记的对象,标记判定:栈中才有可达性分析、方法区则又分成两类废弃常量和无用类。

不足之处:标记和清除的效率不高,还有就是空间问题,标记清楚后会产生大量不连续的内存碎片,如果太多会导致程序运行期间分配大对象的时候,无法找到足够的连续的内存空间份额耦,而触发一次垃圾回收动作。

复制算法

将可用的容量划分为2,每次只使用其中的一块,当一块内存用完的时候,就将还存活着的对象复制到另外一块上。复制的时候由于是按顺序内存分配,所有不会产生不连续的内存空间。

不足之处:1、内存缩小了一半,可用空间少了一半啊。2、当对象的存活率高的时候,ggg了太多的复制操作,效率低。

实际上新生代的垃圾收集器内部的实现方式就是复制算法,但是它们进行了改进,是将内存分为一块较大的Eden和两块较小的Survivor。Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。每次使用的话只使用eden加一块Survivor。当回收的时候,将Eden和Survivor from中的存活对象复制到另一块Survivor to空间上,最后清除掉Eden和刚刚使用过的Survivor from,然后后面使用的就是存放着活着的对象的Survivor to(改名为Survivor from)和Eden,此时Survivor to中存活着的对象age+1,Survival to区会把一些存活得足够旧的对象移至年老代。(这部分与对象由新生代转换为老年代有关,后面会说到)。

虚拟机中Eden和Survivor的大小占比是8:1,所以每次新生代中可用的内存就是90%,至于为什么Survivor只占1,那是因为新生代中的对象大多数都不会存活太久,用完就可以清理的,所以理论是1/10的内存空间即可存放存活的新生代对象。

但是凡事也是有例外的:

当存活的对象太多了,Survivto to的空间不够用的时候,这时候就需要依赖其他内存(老年代)进行分配担保。过程就是Survivto from 和 Eden中生存下来的对象太多了,Survivto to无法存放这么多的对象,此时这部分的对象通过分配担保机制进入老年代。分配担保机制后面再细说。

下图是别的博客处拿的图:根据来比较好理解我上述的话。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yN3b52io-1589450484533)(/Users/awakeyoyoyo/Library/Application Support/typora-user-images/image-20200514134150795.png)]

标记-整理算法

标记过程和标记清除算法一样,但是后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动没然后清理掉端边界以外的内存。

分代收集算法

其实就是将堆分为新生代和老年代,不同的年代特点采取最合适的收集算法,新生代由于对象存活率低,采用复制算法,老年代由于存活率高采用标志清除或者标志整理算法。 这些取决于垃圾收集器的实现。

HotSpot算法实现
枚举根节点:

由于可达性分析需要从GC Roots节点中寻找引用链。这是就需要用到枚举根节点了,把所有GC Roots枚举出来。

GC Roots中一般为全局性引用(常量、类静态属性)和执行上下文(栈帧中的本地变量表),枚举出这个GC Roots我们需要考虑到这个分析过程所产生结果的准确性枚举效率,也就是我们此时要讲的保证“一致性”快照和提高枚举效率。

如何提高枚举效率:OOPMap

但执行系统停顿下来后,不需要一个不漏的检查完所有执行上下文和全局引用的位置,使用OOPMap的数据结构,来让虚拟机得知哪些地方存放着对象的引用。

一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。使用 OopMap 可以避免全栈扫描,加快枚举根节点的速度。

那么如何保证一致性快照呢:GC停顿
  • GC停顿

    即在整个分析期间整个执行系统,不可以出现分析过程中对象引用关系还在不断变化的过程,否则分析结果的准确性就会无法得到保证。所以程序需要在此时停止下来。

如何保证每次gc停顿的位置都有OOPMap记录下对象的引用位置:安全点、安全区域
  • 安全点

    1、何为安全点

    我们提到要提高枚举效率就要用到OOPMap来存储引用的位置,但是如果为每一条指令的生成对应的OOPMap就会需要大量的额外空间,虚拟机只在特定的位置记录了OOPMap信息,这些位置称为安全点。程序在执行时,并非在所有的地方都能停下来开始GC,只有到达这个“安全点“时才能停顿下来。安全点的选区既不能太少以至于让GC等待时间过长,也不能过于频繁以至于过分增大运行时的负荷。安全点的选定基本上是成语“是否具有让程序长时间执行的特征”为标准来进行选定的,

    2、如何让所有线程停止

    抢先式中断”不需要线程的执行代码去主动配合,在GC发生时,首次会把所有的线程全部中断,如果发现有些线程中断点不是安全点,就恢复该线程直到安全点上停止。

    ”主动式中断“实际上就是线程主动轮询的一个过程,当GC需要中断线程时,不直接对线程进行操作,仅仅简单的设置一个标志,这个轮询标志当然要与安全点相重合。各个线程在执行的时候都会主动去询问这个轮询标志:”我是否到了该中断的点了?“。如果是真,就把线程挂起,现在大部分虚拟机都采用是”主动式中断”方式,因为它相对“抢先式中断”方式避免了一个中断——>启动——>又中断的一个过程。

  • 安全区域

    设置“安全点”虽然保证了大部分线程停顿,但总有例外,假如某些线程正在阻塞活着睡眠。请求中断时,JVM不可能等该线程睡醒之后到达安全点之后才能进行可达性分析过程,而此时如果该线程睡醒了恰巧GC又在进行可达性分析或者是回收,那该线程又该何去何从呢?所以JVM又在安全点的基础上加了一个双重保险——安全区域。

    安全区域是指在一段代码片中,引用关系不会发生改变,实际上就是一个安全点的拓展。当线程执行到安全区域时,首先标识自己已进入安全区域,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为“安全区域”状态的线程了,该线程只能乖乖的等待根节点枚举或者整个GC过程完成之后才能继续执行。当然如果没进安全区域的线程,还是要等待他进行安全点的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值