对垃圾回收进行分析前,咱们先来了解一些基本概念java
引用与指针:git
答案是“用适当的工具作恰如其分的工做”。好比说,某人须要一份证实,原本在文件上盖 上公章的印子就好了,若是把取公章的钥匙交给他,那么他就得到了不应有的权利。(什么状况下,就用什么对策) 6. 为何还要说“只有指针,没有引用是一个重要改变?”? 答案是虽然引用在某些状况下好用,但他也会致使致命错误。以下: ``` char *pc = 0; // 设置指针为空值 char& rc = *pc; // 让引用指向空值 ``` 这是很是有害的,毫无疑问。结果将是不肯定的(编译器能产生一些输出,致使任何事情都有可能发生),应该躲开写出这样代码的人除非他们赞成改正错误。若是你担忧这样的代码会出如今你的软件里,那么你最好彻底避免使用引用,要否则就去让更优秀的程序员去作。 7. 最后上附图,帮助理解
堆(heap)和栈(stack)程序员
程序的栈结构github
]算法
1.返回地址:一个main函数中断执行的执行点. 2.ebp:指向函数活动记录的一个固定位置,ebp又被称为帧指针.固定位置是,这样在函数返回的时候,ebp就能够经过这个恢复到调用前的值。 3.esp始终指向栈顶,所以随着函数的执行,它老是变化的。 4.入栈顺序:先压这次调用函数参数入栈,接着是main函数返回地址,而后是ebp等寄存器。
这里咱们对比了解不一样的 “找到须要标记的对象”的方法编程
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时, 计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。以下图所示:并发
优势:引用计数收集器能够很快地执行,交织在程序的运行之中。这个特性对于程序不能被长时间打断的实时环境颇有利。 缺点:很难处理循环引用,好比图中相互引用的两个对象则没法释放。 应用:Python 和 Swift 采用引用计数方案。
从GC Roots(每种具体实现对GC Roots有不一样的定义)做为起点,向下搜索它们引用的对象,能够生成一棵引用树,树的节点视为可达对象,反之视为不可达。以下图所示:编程语言
本地方法栈则为虚拟机所使用的Native方法服务。
Native方法是指本地方法,当在方法中调用一些不是由java语言写的代码或者在方法中用java语言直接操纵计算机硬件。
JNI:Java Native Interface缩写,容许Java代码和其余语言写的代码进行交互。
上述如图,关于root区域的详细解释参考这里函数
这里咱们介绍几种不一样的 “标记对象”的方法工具
将全部堆上对齐的字都认为是指针,那么有些数据就会被误认为是指针。因而某些实际是数字的假指针,会背误认为指向活跃对象,致使内存泄露(假指针指向的对象多是死对象,但依旧有指针指向——这个假指针指向它)同时咱们不能移动任何内存区域。
若是是静态语言,编译器可以告诉咱们每一个类当中指针的具体位置,而一旦咱们知道对象时哪一个类实例化获得的,就能知道对象中全部指针。这是JVM实现垃圾回收的方式,但这种方式并不适合JS这样的动态语言
标记指针法:这种方法须要在每一个字末位预留一位来标记这个字段是指针仍是数据。这种方法须要编译器支持,但实现简单,并且性能不错。V8采用的是这种方式。
具体实现
堆区域对应了一个标记位图区域,堆中每一个字(不是byte,而是word)都会在标记位区域
中有对应的标记位。每一个机器字(32位或64位)会对应4位的标记位。所以,64位系统中
至关于每一个标记位图的字节对应16个堆中的字节。
虽然是一个堆字节对应4位标记位,但标记位图区域的内存布局并非按4位一组,而是
16个堆字节为一组,将它们的标记位信息打包存储的。每组64位的标记位图从上到下依
次包括:
16位的 特殊位 标记位 16位的 垃圾回收 标记位 16位的 无指针/块边界 的标记位 16位的 已分配 标记位
这样设计使得对一个类型的相应的位进行遍历很容易。
前面提到堆区域和堆地址的标记位图区域是分开存储的,其实它们是以
mheap.arena_start地址为边界,向上是实际使用的堆地址空间,向下则是标记位图区
域。以64位系统为例,计算堆中某个地址的标记位的公式以下:
偏移 = 地址 - mheap.arena_start 标记位地址 = mheap.arena_start - 偏移/16 - 1 移位 = 偏移 % 16 标记位 = *标记位地址 >> 移位
而后就能够经过 (标记位 & 垃圾回收标记位),(标记位 & 分配位),等来测试相应的位。
(也就是说,原本64位是一个字,须要4位标记位。可是,为了与字长相对,16个标记位
放一块儿(恰好一个字长)一块儿表示16个字。而且每类标记位都放在一块儿
AA..AABB...BB)
因为没有类型信息,咱们并不知道这个结构体成员不包含指针,所以咱们只能对结构体
的每一个字节递归地标记下去,这显然会浪费不少时间。
(能不能清除 变成了几率事件)。
初始标记
并发标记
最终标记
筛选回收
初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行
GC Roots Trancing的过程,而从新标记阶段则是为了修正并发标记期间因用户程序继
续运行而致使标记产生变更那一部分对象的标记记录,这个阶段的停顿时间比初始标记稍
长一些,但远比并发标记时间短。
这里咱们介绍几种不一样的垃圾回收算法
标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出全部须要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
优势是简单,容易实现。
缺点是容易产生内存碎片,碎片太多可能会致使后续过程当中须要为大对象分配空间时没法找到足够的空间而提早触发新的一次垃圾收集动做。(由于没有对不一样生命周期的对象采用不一样算法,因此碎片多,内存容易满,gc频率高,耗时,看了后面的方法就明白了)
根据对象存活的生命周期将内存划分为若干个不一样的区域。不一样区域采用不一样算法(复制算法,标记整理算法),这就是分代回收算法。
通常状况下将堆区划分为老年代(Old Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集时只有少许对象须要被回收,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,那么就能够根据不一样代的特色采起最适合的收集算法。
1.新生代回收
新生代使用Scavenge算法进行回收。在Scavenge算法的实现中,主要采用了Cheney算法。
Cheney算法是一种采用复制的方式实现的垃圾回收算法。 它将内存一分为二,每一部分空间称为semispace。在这两个semispace中,一个处于使用状态,另外一个处于闲置状态。 简而言之,就是经过将存活对象在两个semispace空间之间进行复制。 复制过程采用的是BFS(广度优先遍历)的思想,从根对象出发,广度优先遍历全部能到达的对象 优势:时间效率上表现优异(牺牲空间换取时间) 缺点:只能使用堆内存的一半
新生代的空间划分比例为何是比例为8:1:1(不是按照上面算法中说的1:1)?
新建立的对象都是放在Eden空间,这是很频繁的,尤为是大量的局部变量产生的临时对 象,这些对象绝大部分都应该立刻被回收,能存活下来被转移到survivor空间的每每不 多。因此,设置较大的Eden空间和较小的Survivor空间是合理的,大大提升了内存的使 用率,缓解了Copying算法的缺点。 8:1:1就挺好的,固然这个比例是能够调整的,包括上面的新生代和老年代的1:2的 比例也是能够调整的。
Eden空间和两块Survivor空间的工做流程是怎样的?
具体的执行过程是怎样的?
假设有相似以下的引用状况:
+----- A对象 | 根对象----+----- B对象 ------ E对象 | +----- C对象 ----+---- F对象 | +---- G对象 ----- H对象 D对象
在执行Scavenge以前,From区长这幅模样: ``` +---+---+---+---+---+---+---+---+--------+ | A | B | C | D | E | F | G | H | | +---+---+---+---+---+---+---+---+--------+ ``` 那么首先将根对象能到达的ABC对象复制到To区,因而乎To区就变成了这个样子: ``` allocationPtr ↓ +---+---+---+----------------------------+ | A | B | C | | +---+---+---+----------------------------+ ↑ scanPtr ``` 接下来进入循环,扫描scanPtr所指的A对象,发现其没有指针,因而乎scanPtr移动,变成以下这样 ``` allocationPtr ↓ +---+---+---+----------------------------+ | A | B | C | | +---+---+---+----------------------------+ ↑ scanPtr ``` 接下来扫描B对象,发现其有指向E对象的指针,且E对象在From区,那么咱们须要将E对象复制到allocationPtr所指的地方并移动allocationPtr指针: ``` allocationPtr ↓ +---+---+---+---+------------------------+ | A | B | C | E | | +---+---+---+---+------------------------+ ↑ scanPtr ``` 中间过程省略,具体参考[新生代的垃圾回收具体的执行过程][3] From区和To区在复制完成后的结果: ``` //From区 +---+---+---+---+---+---+---+---+--------+ | A | B | C | D | E | F | G | H | | +---+---+---+---+---+---+---+---+--------+ //To区 +---+---+---+---+---+---+---+------------+ | A | B | C | E | F | G | H | | +---+---+---+---+---+---+---+------------+ ```
最终当scanPtr和allocationPtr重合,说明复制结束。
注意:若是指向老生代咱们就没必要考虑它了。(经过写屏障)
对象什么时候晋升?
1.当一个对象通过屡次新生代的清理依旧幸存。 2.若是To空间已经被使用了超过25%(后面还要进来许多新对象,不敢占用太多) 3.大对象 (其实这部分,包括次数,比例等,是视状况设置的。)
2.老生代回收
Mark-Sweep(标记清除)
标记清除分为标记和清除两个阶段。 主要是标记清除只清除死亡对象,而死亡对象在老生代中占用的比例很小,因此效率较高。
Mark-Compact(标记整理)
标记整理正是为了解决标记清除所带来的内存碎片的问题。 大致过程就是 双端队列标记黑(邻接对象已经所有处理),白(待释放垃圾),灰(邻 接对象还没有所有处理)三种对象. 标记算法的核心就是深度优先搜索.
1.触发GC(什么时候发生垃圾回收?)
通常都是内存满了就回收,下面列举几个常见缘由: GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。 GC_CONCURRENT: 当咱们应用程序的堆内存达到必定量,或者能够理解为快要满的时候,系统会自动触发GC操做来释放内存。 GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。 GC_BEFORE_OOM: 表示是在准备抛OOM异常以前进行的最后努力而触发的GC。
2.写屏障(一个老年代的对象须要引用年轻代的对象,该怎么办?)
若是新生代中的一个对象只有一个指向它的指针,而这个指针在老生代中,咱们如何判断 这个新生代的对象是否存活?为了解决这个问题,须要创建一个列表用来记录全部老生代 对象指向新生代对象的状况。每当有老生代对象指向新生代对象的时候,咱们就记录下 来。 当垃圾回收发生在年轻代时,只需对这张表进行搜索以肯定是否须要进行垃圾回收,而不 是检查老年代中的全部对象引用。
3.深度、广度优先搜索(为何新生代用广度搜索,老生代用深度搜索)
深度优先DFS通常采用递归方式实现,处理tracing的时候,可能会致使栈空间溢出,因此通常采用广度优先来实现tracing(递归状况下容易爆栈)。 广度优先的拷贝顺序使得GC后对象的空间局部性(memory locality)变差(相关变量散开了)。 广度优先搜索法通常无回溯操做,即入栈和出栈的操做,因此运行速度比深度优先搜索算法法要快些。 深度优先搜索法占内存少但速度较慢,广度优先搜索算法占内存多但速度较快。 结合深搜和广搜的实现,以及新生代移动数量小,老生代数量大的状况,咱们能够获得了解答。