Java运行时数据区及垃圾回收机制

定义:Java运行时数据区有,jvm栈本地方法栈程序计数器,非堆:(方法区, 字符串常量池)

按照线程共享与否及主要存放数据类型可以分为:

线程私有:

jvm栈: 方法参数,方法返回值,局部变量,操作数栈

线程共享

堆:对象,数组

方法区:类(类的结构信息如:方法数据名称,构造方法,普通方法字节码),静态变量,运行时常量池

还是可以用到在介绍jvm中各种常量池时用到的图

在这里插入图片描述
作用:之所以划分Java运行数据区,是因为在jvm中各种数据,因其数据的共享性,数据对应在内存中的存活时长等都各不相同,所以需要划分不同区域来对数据进行存储。
而且不同区域的内存特性并不一致,比如程序计数器(也就是寄存器)的内存不大,但访问速度极快,所以不同大小的数据类型,应该存放在合适的内存区域。

需要注意的是,在jdk1.8之前jvm的方法区是通过永久代来实现的,在JDK1.8,取消了永久代,而是直接使用元数据区(metaspace)作为方法区. metaspace使用的是本地内存(Native memory)

Q:为什么要用元数据区替换永久代?
A:使用永久代来实现方法区,指定了MaxPermSize之后,就决定了永久代的上限。但是在使用过程中,我们并不是能很准确的知道永久代上限应该设置为多少。而默认情况下,元数据区因为使用的是本地内存,只要本地内存足够,就不会出现"OOM PermGen space"这样的异常信息了。

但是为了限制方法区的无线膨胀,JVM同样提供了参数来对其进行限定。

-XX:MetaspaceSize, metaspace的初始空间分配额,以byte为单位,达到了该值,就会触发垃圾收集器进行类型卸载。同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。

-XX:MaxMetaspaceSize,设置元数据区的上限。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

堆中内存分配原则

对象的内存分配,就是在堆上进行内存分配。对象主要分配在年轻代上,少数情况下也可能直接分配在老年代中,分配的规则不是固定的,其细节取决于当前使用的是哪一种垃圾收集器,还和jvm中的内存设置相关

下面介绍几条最普遍的内存分配原则
1.对象优先分配在Eden区
大多数情况下,对象在年轻代的Eden中分配,当Eden区没有足够的空间进行分配时,虚拟机进发起一次Minor GC,如果GC期间jvm发现已有的对象全部无法放入Survivor空间,会通过分配担保机制提前转移至老年代中
2.大对象直接进入老年代分配
所谓的大对象是指需要大量连续的内存空间的对象,比较典型就是数组,很长的字符串。
3.长期存活的对象将进入老年代
虚拟机将为每个对象定义一个对象年龄计数器,如果对象出生在Eden,并经历过第一次Minor GC后,且Survivor中有空间可以容纳这个对象,将被转移到Survivor空间中,并且对象年龄设置为1,对象每在Survivor中熬过一次Minor GC,
年龄就增加一岁,当它的年龄增加到一定程度时(默认15岁),就会被晋升到老年代。
在进行Minor GC时,先将eden区存活对象复制到survivor0区,然后清空eden区,当这个survivor0区也满了时,则将eden区和survivor0区存活对象复制到survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,
然后交换survivor0区和survivor1区的角色(即下次垃圾回收时会扫描Eden区和survivor1区),即保持survivor0区为空,如此往复。特别地,当survivor1区也不足以存放eden区和survivor0区的存活对象时,就将存活对象直接存放到老年代。
如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。

Java垃圾收集机制

定义:众所周知,java之所以不是特别占内存,主要是因为其有一套自动的内存回收机制。而这个机制主要就是java中对jvm堆中的数据进行内存回收.

根据上面介绍的java运行时数据区,我们可以知道

堆被细分为:
年轻代:Eden,survivor 0,survivor 1; 老年代

在进行垃圾回收前,我们首先需要定义,什么样的数据可以作为垃圾进行回收。

简单来说,垃圾收集器回自动回收内存中那么已不再存活的对象。判断一个对象是否还存活主要有以下两种方法:

引用计数法:
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。在任何时刻,如果一个对象的计数器值为0,就是不可能再使用的。
主流的Jvm并没有选用引用计数法来管理内存,最主要的原因是它很难解决对象之间的相互循环引用的问题。

可达性分析法:
可达性分析法通过一系列称为GC Roots的对象作为起点,从这些节点开始向下搜索能与GC Root存在直接或间接引用的对象,这个搜索走过的路径称为引用链。当一个对象到所有GC Root之间没有任何引用相链接时,
则证明这个对象是不可达的,不可达的对象会被判定为可回收的对象。在这里插入图片描述
以下这些对象可以作为GC Root:
1.jvm栈中的对象
2.方法区中静态属性引用的对象
3.本地方法栈中JNI(即Native)引用的对象

finalize

并不是被标记为不可达的对象,就一定会被回收。要真正的宣告一个对象的死亡,至少要经过两次标记过程:如果对象在进行可达性分析后发现没有与任何一个GC Root相关联,那么它会第一次被标记
并且进行一次筛选,筛选的条件就是此对象是否有必要执行finalize()方法,当对象没有override finalize()时,或者finalize()已经被调用过,jvm将这两种情况视为没必要执行finalize()方法
如果这个对象被判断为有必要执行finalize()时,那么这个对象会被加入到F-Queue的队列中,并且稍后由jvm自动建立的,低优先级的Finalizer线程去执行这个队列中的各对象的finalize()方法。这里所说的虚拟机执行
finalize()并不意味着每一个对象的finalize()一定会被执行完,如果一个对象中的finalize()执行缓慢,或者发生了死循环,很可能会导致F-Queue队列中的其他对象处于永久等待,甚至整个内存回收机制崩溃。
finalize()方法是对象最后一次逃脱被回收的命运的机会,稍后GC会将F-Queue中的对象进行第二次小规模标记,如果对象要在finalize中拯救自己,只需要在方法中重新与GC Root引用链中的任何一个对象建立起链接即可
,比如把自己this引用赋值给某个类变量,或成员变量,那么在第二次标记时,这个对象将被移出即将回收的集合,如果对象这个时候还没有逃脱,那么它基本就真正的被回收了。

垃圾回收方法

弄清楚了哪些是要被回收的对象,那么接下来梳理一下jvm中的几种垃圾回收方法。以下所说的都是堆中的垃圾回收

首先,垃圾回收类型可分为:

年轻代垃圾回收:Minor GC,包括Eden和survivor 0,survivor 1 ;因为年轻代的对象大多数都具备朝生夕灭的特点,所以Minor GC进行的非常频繁,一般回收速度也比较快

老年代垃圾回收:Major GC/Full GC,出现Major GC经常伴随着至少一次Minor GC,即年轻代,老年代都进行垃圾回收(并非绝对,在Parallel Scavenge收集器中的收集策略就有直接进行Major GC).Major GC的速度一般会比
Minor GC的速度要慢上十倍。

垃圾回收的方法主要有:

1.标记-清除
最基础的收集算法是标记-清除法(Mark Sweep),算法分为标记和清除两个阶段。首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。此算法主要有两个不足:一是效率不高,在标记和清除两个阶段的效率
都不高;另外一个空间问题,标记-清除后会产生大量的不连续的碎片空间,空间碎片太多可能会导致以后程序在运行过程中需要分配较大的对象时,无法找到足够且连续的内存空间。
在这里插入图片描述
2.标记-整理
为了解决标记-清除法中造成的空间碎片问题,标记-整理法应运而生。标记-整理法也是由两个步骤组成,标记和整理;在完成标记之后,并不会直接清除掉被标记的对象。而是让所有的对象都向一端移动
,然后将边界以外的内存全部清理掉。
在这里插入图片描述
在这里插入图片描述

3.标记-复制
为了解决效率问题,出现了复制算法,它将可用内存划分为大小相等的两块,每次只用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块未使用的内存上。然后把当前已使用的内存空间一次性清理掉
。这样使得每次都是对整个半区进行回收,也就不会有空间碎片的问题存在,只需要移动堆顶指针,按顺序为存活的对象分配内存空间即可,实现简单,运行高效。但是代价是可用内存需要预留一半的可用空间,
相当于可用内存缩小为原来的一半,代价较大。
现在的商业虚拟机都采用标记-复制算法来回收新生代。一般来说,Eden和survivor 0,survivor 1的比例是8:1:1.而整个老年代与年轻代的比例是2:1;每次都使用Eden和survivor中的一块,当进行垃圾回收时,
将Eden和survivor上还存活的对象一次性的复制到另一块survivor空间上,最后清理掉Eden和刚才用的的survivor空间

在这里插入图片描述
在这里插入图片描述

常见的垃圾收集器

可以按作用的区域分为

年轻代垃圾收集器

1.**Serial(串行)**收集器是最基本的,历史最悠久的收集器,其采用算法是标记-复制法。它是一个单线程收集器,只会是有一个CPU或一条收集线程去完成垃圾收集工作。尤其是Serial收集器在工作时,
需要暂停其他所有工作线程。直至Serial完成收集工作为止。

优点是:简单高效(与其他收集器的单线程相比),没多线程交互的开销

下图展示了Serial 收集器(老年代采用Serial Old收集器)的运行过程:
在这里插入图片描述

2.ParNew 收集器,是Serial的多线程版本,它也是一个年轻代垃圾收集器,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数,收集算法等都完全相同,二者共用了很多代码

优点是:Serial的多线程的版

ParNew收集器的工作过程如下图(老年代采用Serial Old收集器):
在这里插入图片描述
3.Parallel Scavenge 收集器,同样也是一个多线程并行的年轻代收集器。同样使用标记-复制算法。Parallel Scavenge收集器与其他收集器的不同之处在于,此收集器关注的是最大吞吐量,即极可能缩短垃圾收集占
用CPU的时间比例,以此来到达CPU的最大吞吐量=CPU计算处理数据时间/(CPU处理数据时间+垃圾收集时间)。高吞吐量意味着有大量的时间可以用在程序运算任务上,主要适用在不需要太多交互的任务。

除了可以显式的控制吞吐量,Parallel Scavenge收集器还提供了一个参数: -XX:+UserAdaptiveSizePolicy,这是一个开关参数,打开这个参数之后,就无需再手工指定年轻代大小,Eden和Survivor比例,晋升老年代年龄
等细节参数了,jvm会根据系统当前性能监控信息,动态调整这些参数,以达到最大吞吐量,这种方式称为GC的自适应调节策略。
自适应调节策略也是Paraller Scavenge与ParNew的主要区别之一

还有一个特点需要注意,Paraller Scavenge无法配合CMS收集器使用,所以在JDK1.6推出Parallel Old之前,年轻代如果使用Scavenge,那么老年代只有Serial Old收集器能与之配合使用。

老年代收集器

1.Serial Old 收集器,从名字我们不难看出这是Serial的老年代版本。它同样是一个单线程收集器。但是老年代一般不会将空间分为两部分来复制,所以Serial Old在老年代采用的标记-整理 算法

作为CMS收集器的后备预案,在并发收集器发生Concurrent Mode Failure时使用

2.Parallel Old 收集器,是Parallel scavenge的老年代版本,采用标记-整理算法,前面已经提到过,这个收集器是在JDK1.6之后才开始提供的。

既然是Scavenge的老年代版本,那肯定也是延续了其特性,以实现控制吞吐量为主,在吞吐量敏感的场景,可以使用Parallel Scavenge和Parallel Old的组合进行垃圾收集

3.CMS(Concurrent Mark Sweep) 收集器,是一种以最短停顿时间为目标的收集器,它比较契合互联网服务端的java应用特性。这些服务都比较重视服务的相应速度。
从名字也能看出,采用标记-清除算法。其工作流程主要可以分为4个步骤:
1.初始标记(CMS initial mark):这仅仅只是标记一下GC Root能直接关联到的对象,速度很快,需要暂停其他工作线程
2.并发标记(CMS concurrent mark):进行GC Tracing(引用链路由)的过程,在整个过程中耗时最长,无需暂停用户线程,可并发进行
3.重新标记(CMS remark):为了修正并发标记期间因用户程序继续导致标记产生表动的一部分对象的标记记录,这个阶段也需要暂停其他工作线程,且停顿的时间要比初始标识阶段要长一点,但远比并发标记耗时要短
4.并发清除(CMS concurrent sweep):并发回收垃圾,无需暂停工作线程

特点:由于整个过程中耗时最长的并发标记和并发清除都是能和工作线程同步进行的,所以整体来说,其暂停其他工作线程的时间短,又是并发进行垃圾回收的,综合性能高

有优点就必然有缺点,CMS有几个不足的地方
1.对CPU资源比较敏感,在并发阶段,它虽然不会导致用户线程提顿,但是因为占用了一部分线程(或者说CPU资源),而导致应用程序变慢,总吞吐量会降低
2.CMS默认启动的回收线程数量是(CPU数量+3)/4,也就是当CPU在4个以上时,并发进行垃圾回收的线程会占用不少于25%的CPU资源,随着CPU数量增加而下降,但是当CPU数量不足4个时,CMS对程序的影响可能就会
变得很大,如果CPU的负载比较大,还需要分一半去执行垃圾回收,这可能会导致用户程序的执行速度忽然降低50%
3.无法处理浮动垃圾,可能会导致Concurrent Mode Failure失败而导致另一次Full GC。由于CMS收集器和用户线程并发运行,因此在收集过程中不断有新的垃圾产生,这些垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好 等待下一次GC时再将其清理掉,这些垃圾就称为浮动垃圾。
4.CMS垃圾收集器不能像其他垃圾收集器一样,等老年代完全被填满之后再进行垃圾回收,需要预留一部分空间共并发收集时使用,可通过参数-XX:CMSIntiatingOccupancyFaction来设置老年代空间达到多少比例时,进行垃圾收集,默认是68%
如果在CMS运行期间,预留的内存无法满足程序需要,就会出现一次ConcurrentMode Failure失败,此时虚拟机将启动预备方案,使用Serial Old收集器重新进行年老代垃圾回收。
5.因为采用的是标记-清除 算法,因此不可避免的会产生大量不连续的内存碎片,如果无法找到一个足够大的连续内存空间来存放新对象时,将会触发Full GC ,CMS提供一个开关参数-XX:+UserCMSCompactAtFullCollection,用于指定在Full GC之后进行内存整理
,内存整理会使得垃圾收集时间变长,CMS提供了另外一个参数-XX:CMSFullGCsBeforeCompaction, 用于设置在执行多少次不压缩的Full GC之后,跟着再来一次内存整理。

G1(Grabage First)收集器

G1收集器是当前垃圾收集器发展的最新成果,其使命是在未来可以替换掉CMS垃圾收集器
G1具备如下特点:
1.G1与CMS一致,同样是一个并发垃圾收集器
2.G1能对整个堆的垃圾进行回收。G1将整个堆空间分为若干个区域(Region),虽然分代概念在G1中任然保留,但是其管理的粒度是以Region为单位
3.从整体上来看G1是基于标记-整理算法,但从局部看(两个Region之间)是基于标记-复制算法。所以无论是标记-整理或者是标记-复制,G1在进行垃圾回收时,不会产生碎片空间。这对后续为新对象分配连续的存储空间比较友好,也不会因为大对象没有连续的存储空间而提前触发Full Gc了
4.G1垃圾收集器的特别之处在于,可预测停顿时间。这是G1相对于CMS的一大优势,两者共同的关注点都是降低用户线程停顿时间,但是G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指明在一个长度为M毫秒内,消耗在GC上的时间不得超过N毫秒。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个堆中进行全域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的大小空间,以及回收这样大小空间的垃圾所需要的时间经验值),以此在后台维护一个优先列表,每次根据允许的收集时间,
优先回收价值最大的Regiion(这也是Garbage First名字的由来),这种使用Region来划分内存空间以及有优先级的区域的回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的回收效率

G1收集器进行垃圾回收大致可以分为以下几个步骤:
1.初始标记:标记直接与GC Root相关联的对象,此阶段需要暂停用户线程,但是耗时很短
2.并发标记:从GC Root开始进行可达性分析(GC Tracing),找到存活的对象。此阶段耗时较长,但是可以与用户线程并发执行。
3.最终标记:为了修正并发标记期间因用户程序运行而导致标记变化的一部分标记记录。此阶段需要暂停用户线程,但也是多线程进行修正。

以上的几个步骤和CMS都非常的相似,下面是G1比较特别的步骤

4.筛选回收:首先对各Region中的回收价值和回收成本进行排序,根据用户所期望的停顿时间来做出回收依据。此阶段也是与用户线程并发进行的,但是因为只是回收一部分区域中的垃圾内存,所以效率比较高
在这里插入图片描述
全文结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值