- 博主简介:想进大厂的打工人
- 博主主页:@xyk:
- 所属专栏: JavaEE初阶
上篇文章我们讲了java运行时内存的各个区域。
传送门:【JavaEE】JVM的组成及类加载过程_xyk:的博客-CSDN博客
对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此我们本篇所讲的有关内存分配和回收关注的为Java堆与方法区这两个区域。
目录
文章目录
一、垃圾回收机制—GC
1.1 引用计数器算法(java没有采用这种算法)
1.2 可达性分析
二、垃圾回收算法
2.1 标记清除算法
2.2 复制算法
2.3 标记整理算法
2.4 复合策略”分带回收“
三、垃圾收集器
3.1 CMS收集器(老年代收集器,并发GC)
3.2 G1收集器(唯一一款全区域的垃圾回收器)
一、垃圾回收机制—GC
在JVM中存在一个垃圾回收机制,GC,帮助程序猿自动释放内存的,能够有效的减少内存泄漏的出现频率。主要是针对 堆上的对象 来进行释放~
GC也就是以 对象 为单位进行释放的(说是释放内存,其实是释放对象)
GC中主要分成两个阶段:
1.找,谁是垃圾
2.释放,用什么算法
Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法
死亡对象的判断算法:
1.1 引用计数器算法(java没有采用这种算法)
引用计算器判断对象是否存活的算法是这样的:给每一个对象设置一个引用计数器,每当有一个地方引用这个对象的时候,计数器就加1,与之相反,每当引用失效的时候就减1。当计数器为0,则认为没有对象了,就是垃圾了~
优点:实现简单、性能高。
缺点:增减处理频繁消耗cpu计算、计数器占用很多位浪费空间(每个对象都需要分配一个计数器)、最重要的缺点是无法解决循环引用的问题。
什么是循环引用?
存在两个对象,同时互相引用指向对象
此时,如果a和b都销毁了,这个时候,两个对象的引用计数给自减1,但是这两个对象的引用计数不是0,不能作为垃圾,无法回收,这俩个对象也无法使用了~陷入了一个逻辑上的循环
1.2 可达性分析
把对象之间的引用关系,理解成了一个树形结构,从一些特殊的起点出发,进行遍历,只要能遍历访问到的对象,就是”可达“,再把”不可达的“当作垃圾回收即可
通过 root 这个引用,就可以访问到整个树的任意结点,那么哪些对象可以作为gcroot?
- 栈上的局部变量(每个栈的每个局部变量,都是起点)
- 常量池中的引用的对象
- 方法区中,静态成员引用的对象
可达性分析,克服了引用计数的两个缺点~
但是也有自己的问题:
1.消耗更多的时间,因此某个对象成了垃圾,也不一定第一时间发现,每次扫描的过程,都是需要消耗时间的
2.在进行可达性分析的时候,要顺藤摸瓜,一旦这个过程中,代码的对象引用关系发生变化了,就麻烦了
因为,为了更准确的完成这个”摸瓜“的过程,需要让其他的业务线程 暂停工作!!(STW问题
stop the world,为了保证内存的一致性,必须先暂停程序的执行)
二、垃圾回收算法
上面我们可以通过可达性分析找到垃圾,那么我们应该用什么回收算法来进行回收操作呢?
2.1 标记清除算法
标记-清除算法是最基础的算法,像它的名字一样算法分为“标记”和“清除”两个阶段,首先需要标记出所需要回收的对象,标记完成后统一收集被标记的对象。
优点: 实现简单。
缺点: 产生不连续的内存碎片;“标记”和“清除”的执行效率都不高。如果要申请空间比较大,会失败。
2.2 复制算法
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。
优点: 执行效率高。
缺点: 空间利用率低, 因为复制算法每次只能使用一半的内存。
2.3 标记整理算法
类似于 顺序表删除中间元素,有一个搬运过程~
优点: 解决了内存碎片问题,比复制算法空间利用率高。
缺点: 因为有局部对象移动,相对效率不高。
2.4 复合策略”分带回收“
因此,实际上 JVM 的实现思路,是结合了上述几种思想方法,针对不同的情况,使用不同的策略.
给对象设定了”年龄“这样的概念,描述了这个对象存在多久了,如果一个对象刚诞生,认为是0岁,每次经过一轮扫描,没被标记成垃圾,这个时候对象就涨一岁,通过年龄来区分这个对象的 存活时间
1.新创建的对象,放到伊甸区,当垃圾回收扫描伊甸区之后,绝大部分对象都会在第一轮 GC 中就被干掉~~ 大部分对象是活不过一岁的,朝生夕死
2.如果伊甸区的对象,熬过第一轮 GC ,就会通过复制算法,拷贝到幸存区,幸存区分成两半大小,一次只使用其中的一半~~ 垃圾回收扫描幸存区的对象,也是发现垃圾就淘汰,不是垃圾的,通过复制算法,复制到幸存区的另外一半
3.当这个对象在幸存区,熬过若干轮 GC 之后,年龄增长到一定程度了,就会通过复制算法拷贝到老年代
4.进入老年代的对象,年龄都很大了,再消亡的概率比前面新生代中的对象小,针对老年代 GC 的扫描频次就会降低很多,如果老年代发现某个对象是垃圾了,使用标记整理的方式清除
5.特殊情况,如果对象非常大,直接进入老年代(大对象进行复制算法,成本比较高,而且大对象也不会很多)
三、垃圾收集器
3.1 CMS收集器(老年代收集器,并发GC)
特性:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前
很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速
度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:
初始标记(CMS initial mark)
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The
World”。
并发标记(CMS concurrent mark)
并发标记阶段就是进行GC Roots Tracing的过程。
重新标记(CMS remark)
重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分
对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的
时间短,仍然需要“Stop The World”。
并发清除(CMS concurrent sweep)
并发清除阶段会清除对象
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
缺点:
- CMS收集器对CPU资源非常敏感
- CMS收集器无法处理浮动垃圾
- CMS收集器会产生大量空间碎片
3.2 G1收集器(唯一一款全区域的垃圾回收器)
G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的
region块,然后并行的对其进行垃圾回收
G1垃圾回收器回收region(区域)的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的。
一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象
1.年轻代垃圾收集
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域
2.老年代垃收集
1.初始标记(Initial Mark)阶段 - 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。
2.并发标记(Concurrent Mark)阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,就是如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率
很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean
up阶段。
3.最终标记(CMS中的Remark阶段) - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不
同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标
记可达对象。4.筛选回收(Clean up/Copy)阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有一个
Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个
阶段也是和minor gc一同发生的,如下图所示
G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。 如果你的应用追求低停顿,G1可以作为选择;如果你的应用追求吞吐量,G1并不带来特别明显的好处。