⑴ 船上的垃圾分类和处理
船上的垃圾分类和处理,国家环保局也有相应的规定和要求,按照要求对垃圾进行分类,也是对于环境保护做出的贡献。
⑵ C#中关于垃圾收集器的问题
该对象已经被析构。
ObjectReliveTest.Program.relived = this; //复活
执行了对象复苏。但是,该对象已经被执行了终止化,使用他可能会导致不可预期的结果。(参考《.net框架程序设计》,p489)
如果被析构,那么,为什么CLR允许无意义的对象调用方法,而不是抛出异常?
既然是“不可预期的结果”,所以必须通过大量实验来进行观察。你可以多测试几次,你会发现某几次clr会抛出nullreference异常。
----------------------------------------------------------
~Reliver()
{
Console.WriteLine("正在析构复活类");
ObjectReliveTest.Program.relived = this; //复活
}
当对象的析构函数(finalize方法)被执行时,该对象的一个引用被放入了一个根中,从而使其又成为应用程序得一个可达对象。对象重新复苏以后,垃圾收集器不会再将其认为是可收集的对象,应用程序也可以自由的使用该对象。但是,该对象已经被执行了终止化,使用他可能会导致不可预期的结果。
换句话说,在对象执行了finalize方法以后,它的成员仍然可能被访问。
因此应该这样回答你这个问题:对象确实已经被析构,但是clr执行的这个析构,不是将对象从内存中抹掉,而是对它进行了特殊的标记,以便于在某一代龄的内存空间不足时流出空间,在此之前,应用程序仍然能够访问这个对象。 从这里也可以看出.net语言和c++的一些区别。
⑶ 5、垃圾回收机制
JVM的垃圾回收机制主要涉及三个方面的问题:
1.JVM有哪些垃圾回收算法?各自有什么优势?
2.CMS垃圾回收器是如何工作的?有哪些阶段?
3.服务卡顿的元兇到底是什么?
Java不用程序来管理内存的回收,但这些内存是如何回收的?
其实,JVM有专门的线程在做这件事情。当内容空间达到一定条件时,会自动触发,这个过程就叫GC,负责GC的组件被称为垃圾回收器。JVM规范没有规定垃圾回收器怎么实现,它只需要保证不要把正在使用的对象回收掉就可以。在现在的服务器环境中,经常被使用的垃圾回收器有CMS和G1,但JVM还有其它几个常见的垃圾回收器。
GC的过程是先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后删除,所以GC只与活跃的对象有关,和堆的大小无关。
接下来学习下分代垃圾回收的内存划分和GC过程,再有就是常见的垃圾回收器。
这篇比较重要,因为几乎所有的垃圾回收器都是在这些基本思想上演化出来的。
GC的第一步就是找出活跃的对象,根据GC Roots遍历所有的可达对象,这个过程就叫作标记。
如上图所示,圆圈代表对象,绿色的代表GC Roots,红色的代表可以追溯到的对象,标记后,有多个灰色的圆圈,代表都是可被回收的对象。
清除阶段就是把未被标记的对象回收掉。
这种方式有一个明显的问题,会产生碎片空间。
比如申请了1k、2k、3k、4k、5k的内存
由于某些原因,2k和4k的内存不再使用,交给垃圾回收器回收。
解决碎片问题,就需要进行内存整理。
有一个思路就是提送一个对等的内存空间,将存活的对象复制过去,然后清除员内存空间。
在程序设计时,一般遇到扩缩容或者碎片整理问题时,复制算法都是非常有效的。比如:HashMap的扩容使用的是同样的思路,Redis的rehash也是如此。
整个过程如下图
这种方式看似完美,解决了碎片问题,但是弊端也非常明显,它浪费了一半的内存空间来做这个事情,如果原本资源就有限,这就是一种无法容忍的浪费。
不用分配一个对等的空间也是可以完成内存的整理工作。
可以把内存想象成一个非常大的数组,根据随机的index删除了一些数据,那么对数组的清理不需要另外一个数组来进行支持的,使用程序就可以。
主要思路是移动所有的存活对象,且按照内存地址顺序依次排列,然后将末端内存地址以后的内存全部收回。
对象的引用关系一般是非常复杂的,从效率上来说,一般整理算法是要低于复制算法的。
JVM的垃圾回收器,都是对以上几种朴素算法的结合使用,简单看一下它们的特点:
效率一般,缺点是回造成内存碎片的问题。
复制算法是所有算法里面效率最高的,缺点是造成一定的空间浪费。
效率比前两者要差,但没有空间浪费,也消除了内存碎片问题。
所以没有最优的算法,只有最合适的算法。
JVM是计算节点,而不是存储节点。最理想的情况就是对象使用完成之后,它的生命周期立马就结束了,而那些被频繁访问的资源,我们希望它能够常驻在内存里。
对象大致可以分为两类:
1.大部分对象的生命周期都很短
2.其他对象则很可能会存活很长时间
现在的垃圾回收器都会在物理上或者逻辑上,把这两类对象进行分区。我们把死的快的对象所占的区域叫年轻代(Young Generation)。把其他活的长的对象所占的区域叫作老年代(Old Generation),老年代在有时候会叫作Tenured Generation。
年轻代使用的垃圾回收算法是复制算法,因为年轻代发生GC后,会有非常少的对象存活,复制这部分对象是非常高效的
年轻代的内部分区
如图所示,年轻代分为:一个伊甸园空间(Eden),两个幸存者空间(Survivor)。
当年轻代中的Eden区分配满的时候,就会触发年轻代的GC(Minor GC),具体过程如下
1.在Eden区执行了第一次GC之后,存活的对象会被移动到其中一个Suvivor分区(from);
2.Eden区再次GC,这是会采用复制算法,将Eden和from区一起清理,存活的对象会被复制到to区;接下来只需要清空from区就可以了
在整个过程中总会有一个Survivor分区是空置的。Eden、from、to的默认比例是8:1:1,所以只会造成10%的空间浪费。
这个比例是由参数-XX:SurvivorRatio进行配置的(默认为8)。
补充下不常提到的TLAB。TLAB全称是Thread Local Allocation Buffer,JVM默认给每个线程开辟一个buffer区域,用来加速对象分配。这个buffer就放在Eden区中。
这个道理和Java语言中的ThreadLocal类似,避免了对公共区的操作,以及一些锁竞争。
老年代一般使用"标记-清除"、"标记-整理"算法。因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,不如采取就地收集的方式。
对象进入老年代的途径分类
如果对象够老,会通过"提升"进入老年代。关于对象老不老,是通过它的年龄来判断的。每发生一次Minor GC,存活下来的对象年龄都会加1,直到达到一定的阀值,就会提升到老年代,
这些对象如果变的不可达,直到老年代发生GC的时候才会被清理掉。
这个阀值可以通过参数 -XX:+MaxTenuringThreshold进行配置,最大值是15,因为它是用4bit存储的(所以把这个值调的很大的文章,是没有什么根据的)。
每次存活的对象,都会放入其中一个幸存区,这个区域默认比例是10%,但无法保证每次存活的对象都小于10%,当Survivor空间不够,就需要依赖其它内存(老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。
超出某个大小的对象直接在老年代分配,通过参数设置-XX:PretenureSizeThreshold进行配置的,默认为0,默认全部在Eden区进行分配。
有的垃圾回收算法,并不要求age必须达到15才能晋升到老年代,它会使用一些动态的计算方法。比如,如果幸存区中相同年龄对象大小的和,大于幸存区的一半,大于或者等于age的对象将会直接进入老年代。
这些动态判定一半不受外部控制
对象的引用关系时一个巨大的网状,有的对象在Eden区,有的可能在老年代,那么这种跨代的引用是如何处理的呢?由于Minor GC是单独发生的,如果一个老年代的对象引用了它,如何确保能够让年轻代的对象存活呢?
对于是、否的判断,我们通常都会用到Bitmap(位图)和布隆过滤器来加快搜索的速度,需要另外再学习下(如果不知道这两个概念的话)
JVM也是用了类似的方法。其实,老年代是被分成众多的卡页(Card Page)的(一般数量是2的次幂)
卡表(Card Table)就是用于标记卡页状态的一个集合,每个卡表对应一个卡页。
如果年轻代有对象分配,而且老年代有对象指向这个新对象,那么这个老年代对象所对应内存的卡页就会被标识为dirty,卡表只需要非常小的存储空间就可以保留这些状态,垃圾回收时,就可以先读这个卡表,进行快速的判断。
接下来学习HotSpot的几个垃圾回收器,每种回收器都有各自的特点。在平常的GC优化时,一定要清楚现在用的是那种垃圾回收器。
下图包含了年轻代和老年代的划分,方便接下来的学习参考
处理GC的只有一条线程,并且在垃圾回收的过程中暂停一切用户线程。
这是最简单的垃圾回收器,虽然简单,但十分高效,通常用在客户端应用上。因为客户端应用不会频繁创建很多对象,用户也不会感觉出明显的卡顿。相反,它使用的资源更少,也更轻量级。
ParNew是Serial的多线程版本,由多条GC线程并行地进行垃圾清理。清理过程依然要停止用户线程。追求低停顿时间,与Serial唯一区别就是使用了多线程进行垃圾回收,在多CPU环境下性能比Serial会有一定程度的提升;但线程切换需要额外的开销,因此在单CPU环境中表现不如Serial。
另一个多线程版本的垃圾回收器。但与ParNew是有区别的
1.Parallel Scavenge:追求CPU吞吐量,能够在较短时间内完成指定任务,适合没有交互的后台计算,弱交互强计算。
2.ParNew:追求降低用户停顿时间,适合交互式应用,强交互弱计算。
与年轻代的Serial垃圾回收器对应,都是单线程版本,同样适合客户端使用。
年轻代Serial,使用复制算法。
老年代的Old Serial,使用标记-整理算法。
Parallel Old回收器是Parallel Scavenge 的老年代版本,追求CPU吞吐量。
CMS(Concurrent Mark Sweep)回收器是以获取最短GC停顿时间为目标的收集器,它在垃圾回收时使得用户线程和GC线程能够并发执行,因此在垃圾回收过程中用户也不会感到明显的卡顿。
长期看来,CMS垃圾回收器,是要被G1等垃圾回收器替换掉的,在Java8之后,使用它将会抛出一个警告!
除了上面几个垃圾回收器,我们还有G1、ZGC等更加高级的垃圾回收器,它们都有专门的配置参数来使其生效。
通过-XX:PrintCommandLineFlags参数,可以查看当前Java版本默认使用的垃圾回收器。在Java13中,默认的回收器就是G1。
以下是一些配置参数:
1.-XX:+UseSerialGC 年轻代和年老代回收器
2.-XX:+UseParNewGC 年轻代使用ParNew,老年代使用Serial Old。
3.-XX:+UseParallelOldGC 年轻代和老年代哦都市用并行回收器。
4.-XX:+UseConcMarkSweepGC 表示年轻代使用ParNew,老年代使用CMS。
5.-XX:+UseG1GC 使用G1垃圾回收器
6.-XX:+UseZGC 使用ZGC垃圾回收器
这些垃圾回收器的关系还是比较复杂的,请看下图
目前Java8还是主流使用版本,从Java8升级到高版本的Java体系是有一定成本的,所以CMS垃圾回收器还会持续一段时间
抛个问题,如果在垃圾回收的时候,又有新的对象进入怎么办?
为了保住程序不乱套,最好的办法就是暂停用户的一切线程,也就是在这段时间,是不能new对象的,只能等待,表象是在JVM上就是短暂的卡顿,什么都干不了,这个现象叫作Stop The World。
标记阶段,大多数是要STW的。如果不暂停用户进程,在标记对象的时候,有可能有其它用户线程会产生一些新的对象和引用,造成混乱。
现在的垃圾回收器,都会尽量去减少这个过程。但即使最先进的ZGC回收器,也会有短暂的STW过程。我们要做的就是在现有基础设施上,尽量减少GC停顿。
举例说明下
某个高并发服务的峰值流量是10万次/秒,后面有10台负载均衡的机器,那么每台机器平均下来需要1w/s。假如某台机器在这段时间内发生了STW,持续了一秒,那么至少需要10ms就可以返回的1万个请求,需要至少等待1秒。
在用户那里的表现就是系统发生了卡顿。如果我们的GC非常的频繁。这种卡顿就会特别的明显,严重影响用户体验。
虽然说Java为我们提供了非常棒的自动内存管理机制,但也不能滥用,因为它是有STW硬伤的。
介绍了堆的具体分区,年轻代和老年代。介绍了多个常用的垃圾回收器,不同的垃圾回收器有不同的特点。各种垃圾回收器都是为了解决头疼的STW问题,让GC时间更短,停顿更短,吞吐量更大。
接触了很多名词,总结如下
1.Mark
2.Sweep
3.Copy
4.Compact
1.Young generation
2.Survivor
3.Eden
4.Old Generation |Tenured Generation
5.GC
--1.Minor GC
--2.Major GC
1.weak generational hypothesis
2.分配担保
3.提升
4.卡片标记
5.STW
⑷ 生活中垃圾的怎么处理
生活垃圾指人们日常家庭生活所产生的废弃物,品种多、结构杂。主要来源于家庭生活的日常起居、饮食的食品残渣、各类器件、物品的包装袋、包装纸、大批的废纸及纺织品、废金属等因此,根据垃圾的来源及基本种类可将其分为三类:
1、食品垃圾:这是食用各种食品所产生的残余废物的总称。其主要特征是生物分解速度快,腐蚀性强,并产生令人厌恶的各种刺鼻气味。食品垃圾的处理要求不高,主要需解决处理速度的问题。
2、普通垃圾:这是人们日常生活废物的总称。包括废弃的木制品、纸制品、塑料、橡胶、纺织品、皮革制品及丢弃的碎玻璃及金属制品和尘土等。普通垃圾是城市中垃圾回收的主要对象。
3、危险垃圾:对人类和动植物的生命具有瞬间的、短期或长期危害的垃圾称为危险垃圾。包括干电池、日光灯管、体温计等各种化学、生物的危险品、易燃、易爆品,含放射性物质的废物。这类垃圾一般不能混入普通垃圾中,应单独清运和处理。
长久以来,垃圾的处理所采承的方法主要有焚烧、填埋、堆肥这三种。详细内容如下:
内容
卫生填埋
焚烧
堆肥
操作安全性
较好,注意防火
好
好
技术可靠性
可靠
可靠
可靠,国内有相当经验
占地
大
小
中等
选址
较困难,要考虑地形、地质条件,防止地表水、地下水污染,一般远离市区,运输距离较远。
易,可靠近市区建设,运输距离较近
较易,仅需避开居民密集区,气味影响半径小于200m,运输距离适中。
适用条件
无机物>60%
含水量<30%
密度>0.5t/d
垃圾低位热值>3300kJ/kg时不需添加辅助燃料。
从无害化角度,垃圾中可生物降解有机物≥10%,从肥效出发应>40%。
最终处置
无
仅残渣需作填埋处理,为初始量的10%。
非堆肥物需作填埋处理,为初始量的20~25%。
产品市场
可回收沼气发电。
能产生热能或电能。
建立稳定的堆肥市场较困难。
建设投资
较低
较高
适中
资源回收
无现场分选回收实例,但有潜在可能。
前处理工序可回收部分原料,但取决于垃圾中可利用物的比例。
同左
内容
卫生填埋
焚烧
堆肥
地表水污染
有可能,但可采取措施减少可能性。
在处理厂区无,在炉灰填埋时,其对地表水污染的可能性比填埋小。
在非堆肥物填埋时与卫生填埋相仿。
地下水污染
有可能,虽可采取防渗措施,但仍然可能发生渗漏。
灰渣中没有有机质等污染物,仅需填埋时采取固化等措施可防止污染。
重金属等可能随堆肥制品污染地下水
大气污染
有,但可用覆盖压实等措施控制
可以控制,但二恶英(Doxlin)等微量剧毒物需采取措施控制。
有轻微气味,污染指标可能性不大。
土壤污染
限于填埋场区。
无
需控制堆肥制品中重金属含量。
电池在我们日常生活中的应用越来越广泛。电池有汞电池(钮扣式电池)、碱电池(普通电池)、锰电池(阵用电池)之分,一般都不同程度地含有对人体有危害的汞和锰,而废电池中95%的物质又均可以回收,尤其是重金属回收价值很高。因此,将废电池乱扔或焚烧,不仅造成严重的环境问题,更是一种资源的浪费。普通干电池是圆筒形的,外筒由锌制成,这一锌筒即为电池的负极;筒中央炭棒为正极;筒内为二氧化锰、氯化铰和氯化锌。下面介绍两种废干电池内物质回收利用的方法:
1、提取氯化铵
将电池里的黑色物质放在水里搅拌溶解并过滤,将部分滤液放在蒸发皿中蒸发,得白色固体,再加热,利用“升华”收集较纯的氯化铵。
2、制取锌粒
将锌筒上的锌片剪成碎片,放在坩埚中强热(锌熔点419摄氏度,熔化后小心地将锌液倒入冷水里,得到锌粒。该方法可用于实验室里制取锌粒)。
过去人们一直认为垃圾是废品,是没用的东西,这是片面的观点。垃圾其实是一种资源,一种符合可持续发展的经济型再生资源,而现有的处理方式中,掩埋无疑不能回收垃圾资源;焚烧还处在一个资源回收较低的水平;只有综合利用,数管其下,才能实现垃圾资源的最大回收。垃圾要做到综合利用,第一步就要进行分类收集。分类收集是指在垃圾的产地,根据对垃圾的处理或回收利用的不同要求,将垃圾分为不同类别进行收集。分类收集一般采取设置不同形状、不同颜色容器或规定特定的垃圾收集器等办法进行。这也必将成为未来垃圾收集的主要方法。根据一些地区的经验,垃圾的分类收集可以以一个居民住宅区为单位。在小区内设置三种颜色不同的垃圾箱。一种颜色的垃圾箱装食品垃圾,一种颜色的垃圾箱装普通垃圾,另一种颜色的垃圾箱装危险垃圾。由于食品垃圾易腐烂的特点,垃圾的清运应较频繁,速度较快,以半天或一天为单位比较适合;普通垃圾和危险垃圾可以根据各自的产生量来制定清运频率,其中危险垃圾产生量较少,建议2~4天清理一次。垃圾分类运至处理场所一般可由专门的部门来进行(也可由条件较完备的小区进行)。这些机构将食品垃圾、普通垃圾及危险垃圾检查后分别送至不同的处理场所进行最后的处理。其中食品垃圾可以经过发酵等方法处理生成燃气与肥料。普通垃圾中,纸张和木材可再生纸品;玻璃、金属可以熔化后制成新产品;塑料、橡胶也可以熔化后投入再生产。除此之外,1吨废玻璃经机械化工艺处理后可生产出2万只啤酒瓶或相当于1个篮球场面积的窗玻璃;废塑料经回收后,可以变成原油,再从中提炼出柴油、汽油;约占城市生活垃圾总量65%~70%的厨余、果皮垃圾则可动用生化技术和专门的机器“吃进”,利用微生物发酵原理,24小时内就可将垃圾就地变成颗粒型或粉状的肥料或饲料,供公共绿地使用或供市民家庭养花。危险垃圾则通过特殊处理而去除其毒害性。这就是发展了综合利用处理垃圾的方法。
通过这次的学习与研究,我们不仅了解了生活垃圾的处理及利用,还可以准确处理个人制造的垃圾,尽量使垃圾变废为宝。我们应该坚信,总有一天,我们将会走出垃圾的“围城”。
附件1:
校园垃圾处理应实行分类回收制度,其具体操作方法如下:
①每一个班在班内建立一个小型垃圾点,将垃圾按纸类、塑料、金属(如易拉罐等)、电池、其他共五类分类收集。由于纸类、塑料数量较多,可以一天清理一次,另外考虑到电池对环境的危害,建议同学们把废旧电池统一回收。
②学校设立几个大型垃圾箱,统一回收各班分类垃圾箱中的垃圾,并在校园内多设置一些小型分类垃圾箱。学校负责与社会回收单位联系,及时送出回收来的垃圾。
③学校应当设立一个评分制度,对垃圾回收工作开展得好的班级进行鼓励。回收的收入也可返回各班使用。
附件2:
学生对校园白色垃圾处理建议:
(1)学校应教育同学们增强环保意识,多宣传白色污染的危害。
(2)同学们不要随意扔垃圾,对随地扔废弃物的人讲讲环保的重要性。
(3)学校统一将垃圾分类、回收,集中处理。
(4)增设垃圾箱,放在白色污染严重的地方(如小卖部门口)。
(5)设计一个环保标志,挂在醒目的地方。
(6)尽量减少用塑料袋包装物品,并杜绝使用一次性发泡饭盒(现在校食堂使用的一次性饭盒就是国家禁止使用的饭盒,但仍在使用)。
(7)呼吁全社会增强环保意识。
(8)学校不要焚烧垃圾。
⑸ 垃圾收集器-CMS、三色标记、记忆集
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
初始标记:
暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快
并发标记:
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
重新标记:
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。
并发清理:
开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
并发重置:
重置本次GC过程中的标记数据。
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:
1.对CPU资源敏感(会和服务抢资源);
2.无法处理浮动垃圾( 在并发标记和并发清理阶段又产生垃圾 ,这种浮动垃圾只能等到下一次gc再清理了);
3.它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是 在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收
CMS的相关核心参数
1.-XX:+UseConcMarkSweepGC:启用cms
2.-XX:ConcGCThreads:并发的GC线程数
3.-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
4.-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
5.-XX:: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
6.-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
7.-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
8.-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
9.-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。这里引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:
黑色:
表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
灰色:
表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
白色:
表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
标记过程:
初始时,所有对象都在 【白色集合】中;
将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
从灰色集合中获取对象:
3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2. 将本对象 挪到 【黑色集合】里面。
重复步骤3,直至【灰色集合】为空时结束。
结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收
多标-浮动垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动 垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分 对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。
漏标-读写屏障
漏标只有 同时满足 以下两个条件时才会发生:
条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。
条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB) 。
增量更新 就是当黑色对象 插入新的指向 白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
原始快照 就是当灰色对象要 删除指向 白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
以上 无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。
写屏障实现原始快照(SATB): 当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来:
写屏障实现增量更新: 当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D 记录下来:
记忆集
当我们进行young gc时,我们的 gc roots除了常见的栈引用、静态变量、常量、锁对象、class对象 这些常见的之外,如果 老年代有对象引用了我们的新生代对象 ,那么老年代的对象也应该加入gc roots的范围中,但是如果每次进行young gc我们都需要扫描一次老年代的话,那我们进行垃圾回收的代价实在是太大了,因此我们引入了一种叫做记忆集的抽象数据结构来记录这种引用关系。
什么是记忆集?
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构。
如果我们不考虑效率和成本问题,我们可以用一个数组存储所有有指针指向新生代的老年代对象。但是如果这样的话我们维护成本就很好,打个比方,假如所有的老年代对象都有指针指向了新生代,那么我们需要维护整个老年代大小的记忆集,毫无疑问这种方法是不可取的。因此我们引入了卡表的数据结构
什么是卡表?
记忆集是我们针对于跨代引用问题提出的思想,而卡表则是针对于该种思想的具体实现。(可以理解为记忆集是结构,卡表是实现类)
在hotspot虚拟机中,卡表是一个字节数组,数组的每一项对应着内存中的某一块连续地址的区域,如果该区域中有引用指向了待回收区域的对象,卡表数组对应的元素将被置为1,没有则置为0;
G1的记忆集
上述的 卡表机制基本上适用于CMS垃圾回收器 ,因为CMS垃圾回收器只需要在young gc时维护老年代对新生代的引用即可,但是G1垃圾回收器不一样,因为G1垃圾回收器是基于分区模型的,所以每一个Region需要知道有哪些region的引用指向了它,并且这些region是不是本次垃圾回收区域的一部分。因此G1垃圾回收器不能简单的只维护一个卡表(卡表只能简单的知道某块内存区域有没有引用收集区域的对象,但是不能知道到底是谁引用了自己),所以在 G1垃圾回收器的记忆集的实现实际上是基于哈希表的 ,key代表的是其他region的起始地址,value是一集合,里面存放了对应区域的卡表的索引,因此G1的region能够通过记忆集知道,当前是哪个region有引用指向了它,并且能知道是哪块区域存在指针指向。
但是大家应该能注意到, 每个region都维护一个记忆集,内存占用量肯定很大,这也就是为什么G1垃圾回收器比传统的其他垃圾回收器要有更高的内存占用 。据统计G1至少要耗费大约10%-20%的Java堆空间来维护收集器的工作。
参考:
https://blog.csdn.net/xc1989xc/article/details/107466313
https://blog.csdn.net/shangshanzixu/article/details/113918994
⑹ G1 GC垃圾收集流程
从 GC 算法的角度,G1 选择的是复合算法,可以简化理解为:
YoungGC 触发时机
在分配一般对象(非巨型对象)时,当所有 eden region 使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次young gc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。因为YoungGC会进行根扫描,所以会 stop the world 。
YoungGC的回收过程如下 :
1.根扫描root scan,跟CMS类似, Stop the world ,扫描GC Roots对象。
2.处理Dirty card,更新RSet.
3.扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
4.拷贝扫描出的存活的对象到survivor2/old区
5.处理引用队列,软引用,弱引用,虚引用
值得高兴的情况是, cleanup 可以释放老年代的整个regions. 但是并不是每次都是这样, 当并发标记成功结束后,G1会预定一个混合的收集来收集年轻代regions的垃圾,也会在收集集合中加入一部分老年代的regions.
一个混合的Evacuation Pause并不总是并发标记阶段结束后立即开始. 有一系列的规则和启发式算法来决定这个. 比如, 可以释放掉老年代的一大部分空间, 那么就没必要做这个了.
因此,就是在并发标记结束和混合Evacuation Pause之间加入很多fully-young的Evacuation Pause.
具体放入收集集合的老年代区的region,以及它们被加入的顺序都基于一系列的规则选择出来的. 这些规则包括:应用设定的软的实时性能指标, 存活统计以及并发标记阶段垃圾回收的效率, 还有一系列可配的JVM 选项. 混合式收集大体上与我们前面看到fully-young相同, 但这次我们讲到新的对象 remembered sets .
remembered sets 用来支持在不同heap regions上的独立收集. 比如当收集region A,B,C, 我们只需要知道从region D和E中是否有引用到它们来决定它们的存活性.因为遍历整个堆会消耗很久的时间并且打破了我们增量收集的意义, 所以在G1中也采用了与在其他算法中采用Card Table来独立收集年轻代区域类似的优化算法, 叫做remember sets.
如下图所示, 每个region都有一个RSet保存从其他region到这个region中对象的引用. 这些对象会被当做额外的GC roots. 注意在并发标记阶段, 老年代被认为是垃圾的对象会被忽略, 即便有外部对象还在引用它们, 因为它们的对象也会被当做垃圾.
接下来发生的与其他收集器类似:多个并行的GC线程会找出哪些是存活的哪些是垃圾. 最后, 所有存活对象会被移动到survivor区(如有必要创建新的).所有的空region会被释放后被用来存放对象. [[图片上传失败.
为了在应用程序运行期间维护RSets, 任何时候对域的更新都会触发一个Post-Write屏障. 如果关联的引用是跨region的, 比如从一个region到另一个region,一个对应的记录也会在目标region的RSet中添加. 将记录(cards)加入到RSet是异步的并应用了很多优化.简单来说它用Write屏障来将脏记录放到本地buffer中, 一个特殊的GC线程会选择这些记录,然后传播信息给其他region的RSet.
Young GC发生的时机大家都知道,那什么时候发生Mixed GC呢?其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。
gc_handbook_zh
第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。
第四阶段Cleanup只是回收了没有 任何存活对象的Region ,所以它并不需要STW
图是来自 oracle 上对 gc 周期的描述,实心圆都表示一次 GC 停顿
除了以上的参数,G1 GC相关的其他主要的参数有:
参数 含义
-XX:G1HeapRegionSize=n 设置Region大小,并非最终值
-XX:MaxGCPauseMillis 设置G1收集过程目标时间,默认值200ms,不是硬性条件
-XX:G1NewSizePercent 新生代最小值,默认值5%
-XX:G1MaxNewSizePercent 新生代最大值,默认值60%
-XX:ParallelGCThreads STW期间,并行GC线程数
-XX:ConcGCThreads=n 并发标记阶段,并行执行的线程数
-XX: 设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous
-XX:G1ReservePercent,通过-XX:G1ReservePercent指定G1为分配担保预留的空间比例,默认10%。也就是老年代会预留10%的空间来给新生代的对象晋升,如果经常发生新生代晋升失败而导致Full GC,那么可以适当调高此阈值。但是调高此值同时也意味着降低了老年代的实际可用空间
-XX:G1HeapWastePercent
通过-XX:G1HeapWastePercent指定触发Mixed GC的堆垃圾占比,默认值5%,也就是在全局标记结束后能够统计出所有Cset内可被回收的垃圾占整对的比例值,如果超过5%,那么就会触发之后的多轮Mixed GC,如果不超过,那么会在之后的某次Young GC中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低Mixed GC的频率
其他参数
因此AlwaysPreTouch,JVM就会先访问所有分配给它的内存,让操作系统把内存真正的分配给JVM.后续JVM就可以顺畅的访问内存了
关于AlwaysPreTouch找了一些资料,这个参数属于比较偏门的优化项
JAVA进程启动的时候,虽然我们可以为 JVM 指定合适的内存大小,但是这些内存操作系统并没有真正的分配给JVM,而是等JVM访问这些内存的时候,才真正分配,这样会造成以下问题:
配置-XX:+AlwaysPreTouch参数可以优化这个问题,不过这个参数也有副作用,它会影响启动时间,那影响到底有多大呢?请接着往下看。
配置这个参数后这么耗时其中一个原因是,这个特性在JDK8版本以前都不是并行处理的,到了JDK9才是并行。可以戳链接Parallelize Memory Pretouch: https://bugs.openjdk.java.net/browse/JDK-815795
配置-XX:+AlwaysPreTouch参数后,JVM进程启动时间慢了几个数量级的根本原因呢?
在没有配置-XX:+AlwaysPreTouch参数即默认情况下,JVM参数-Xms申明的堆只是在虚拟内存中分配,而不是在物理内存中分配:它被以一种内部数据结构的形式记录,从而避免被其他进程使用这些内存。这些内存页直到被访问时,才会在物理内存中分配。当JVM需要内存的时候,操作系统将根据需要分配内存页。
配置-XX:+AlwaysPreTouch参数后,JVM将-Xms指定的堆内存中每个字节都写入’0’,这样的话,除了在虚拟内存中以内部数据结构保留之外,还会在物理内存中分配。并且由于touch这个行为是单线程的,因此它将会让JVM进程启动变慢。所以,要么选择减少接下来对每个缓存页的第一次访问时间,要么选择减少JVM进程启动时间,这是一种trade-off。
在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?
并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。
晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:
巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
参考
分享文档
Java Hotspot G1 GC的一些关键技术- 美团
Plumbr Handbook Java Garbage Collection.pdf 翻译
G1GC 概念与性能调优- oppo
hbase G1
https://www.jianshu.com/p/5d4e319582f7
⑺ 帮我找一些关于垃圾处理的资料
垃圾的处理方法
分类收集 是指在垃圾的产地,根据对垃圾的处理或回收利用的不同要求,将垃圾分为不同类别进行收集。分类收集一般采取设置不同容器、不同颜色的或规定特定的垃圾收集器等办法进行。分类收集处理效果好,经济效益显着,但需要居民的协作。
德国就是采用分类收集的方法来处理生活垃圾的。每个家庭一般有三个桶:一个桶装报纸和各种废纸;一个桶装要送去焚烧处理的垃圾;还有一个桶用来装要收集的各类资源型垃圾,这些垃圾会被送去资源垃圾回收厂进行分类和回收,但不包括玻璃瓶。各种颜色的玻璃瓶洗净、晾干后,放在大门进口处的一个大箱子里,然后再送到社区的玻璃专用回收桶中去。
破碎、压缩 破碎是重要的预处理工序,特别是大型消费废弃物数量的增加,破碎更显得重要。破碎的目的是减少垃圾体积,增加均匀度,便于下一步处理。压缩是减少垃圾体积的最直接的方法,压缩过程对水质和空气不造成污染,投资和操作费用低,压缩后更便于运输、填地,甚至可直接用作建筑材料。城市垃圾经压缩后一般可缩至原体积的1/5,法国还试验采用更高压力,并加入粘合剂,将垃圾压缩到原体积的1/20。
管道运输 这是理想的运输垃圾废物的方法,清洁卫生,还可节约劳动力和车辆,避免了地面交通拥挤的问题,但投资比较高。瑞典的斯德哥尔摩市有10处采用真空管道运输垃圾,垃圾集中站的下面铺设真空管道,可自动地将垃圾运走。西欧正在试验将垃圾制成浆状,通过管道长距离输送更重的废物。
现在采用最多的几种垃圾处理方法是:
卫生填埋 这是广泛采用的处理城市垃圾的方法,其基本操作是铺上一层城市垃圾并压实后,再铺上一层土,然后逐次铺城市垃圾和土,如此形成夹层结构。同时,可有计划地将废矿坑、粘土坑等经过卫生填地,改造成公园、绿地、牧场、农田或作建筑用地。
垃圾焚化 焚化是将城市垃圾在高温下燃烧,使可燃废物转变为二氧化碳和水,焚化后残灰仅为废物原体积的5%以下,从而大大减少了固体废物量,还可以消灭各种病原体,把一些有毒、有害物质转化为无害物质并可回收热能。由于大城市附近缺乏填埋场所,所以多采用焚化法处理垃圾、废物。
综合利用 城市垃圾的回收利用日益受到重视,美国修订了原来的《固体废物处理法》,公布了《资源回收法》,通过法律鼓励和支持回收利用技术的研究。日本、西欧等发达国家也在大力开展这方面的工作,这种方法也叫资源化法,是当前和今后要大力发展的方法。美国科学家认为,扔弃于垃圾堆的各种废纸、废木纤维和木材废料,是一种新型“森林”。过去它们长期被人类忽视,造成了木质资源的巨大浪费,据估计,美国每年有5600万吨废木材和废纸被抛入垃圾堆,这相当于美国1990年国有林采伐量的3倍。近年来,科学家经研究发现,这种“森林”的开发利用前景十分可观,方法多种多样。各种废纸可以回收制浆造纸,也可和其他原料搭配生产各种纸板。回收的废纸和其他纤维原料制成湿纤维浆,然后模压成型,制成一种“空隙板”,可广泛应用于包装、家具、轻型房屋建筑等方面。而对各种木材废料的再回收利用就更丰富多彩了,如废木包装箱可重新回收,用于生产刨花板;对于拆毁建筑所得的板材,可以重新分级再次利用等。
⑻ 网红海上酒店已停用并被拖离海域,原因是什么
据相关报道,漳州当地群众林某在未取得海域使用权的情况下,于东山前楼镇下林尾村南侧海域私自搭一间“网红”海上酒店。因其涉嫌非法占用海域,漳州海警局东山工作站执法人员已对其立案调查,并责令其停止违法行为,酒店已被拖离海域关闭停业,店内没有员工,此前预定所有房费均已退还给客户。而对于网友关心的环保及排污等相关问题,该酒店发出招募首席环保官及环保设备技术顾问的文章,寻找一位“首席环保官”,为“海席”在建设、运营和对周边海域的环境保护上给予专业的评估、指导和监督。希望技术顾问协助开展:(1)针对海上居民生活的小型污水处理方案:从设备、技术端口提供解决方案;(2)针对周边海域的塑料垃圾自动收集方案:水面垃圾收集器、无人船装置等;(3)针对周边渔民的垃圾分类收集处理方案:引入国内外先进海上垃圾处理设备。
⑼ 三色标记法与垃圾回收器(CMS、G1)
JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。
三色标记法将对象的颜色分为了黑、灰、白,三种颜色。
存在问题:
浮动垃圾:并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,此时,此对象不是白色的不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,这种情况对系统的影响不大,留给下一次GC进行处理即可。
对象漏标问题(需要的对象被回收):并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题,而CMS与G1,两种回收器在使用三色标记法时,都采取了一些措施来应对这些问题,CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。
在JVM虚拟机中有两种常见垃圾回收器使用了该算法:
CMS(Concurrent Mark Sweep)
CMS,是非常有名的JVM垃圾回收器,它起到了承上启下的作用,开启了并发回收的篇章。
但是CMS由于许多小问题,现在基本已经被淘汰。
增量更新(Increment Update)
在应对漏标问题时,CMS使用了Increment Update方法来做:
在一个未被标记的对象(白色对象)被重新引用后,==引用它的对象==,若为黑色则要变成灰色,在下次二次标记时让GC线程继续标记它的属性对象。
但是就算时这样,其仍然是存在漏标的问题:
在一个灰色对象正在被一个GC线程回收时,当它已经被标记过的属性指向了一个白色对象(垃圾)
而这个对象的属性对象本身还未全部标记结束,则为灰色不变
而这个GC线程在标记完最后一个属性后,认为已经将所有的属性标记结束了,将这个灰色对象标记为黑色,被重新引用的白色对象,无法被标记
补充,CMS除了这个缺陷外,仍然存在两个个较为致命的缺陷:
解决方案:使用Mark-Sweep-Compact算法,减少垃圾碎片
当JVM认为内存不够了,再使用CMS进行并发清理内存可能会发生OOM的问题,而不得不进行Serial Old GC,Serial Old是单线程垃圾回收,效率低
解决方案:降低触发CMS GC的阈值,让浮动垃圾不那么容易占满老年代
G1(Garbage First)
从G1垃圾回收器开始,G1的物理内存不再分代,而是由一块一块的Region组成;逻辑分代仍然存在。
前置知识 — Card Table(多种垃圾回收器均具备)
由于在进行YoungGC时,我们在进行对一个对象是否被引用的过程,需要扫描整个Old区,所以JVM设计了CardTable,将Old区分为一个一个Card,一个Card有多个对象;如果一个Card中的对象有引用指向Young区,则将其标记为Dirty Card,下次需要进行YoungGC时,只需要去扫描Dirty Card即可。
Card Table 在底层数据结构以 Bit Map实现。
CSet(Collection Set)
SATB(Snapshot At The Beginning)
在应对漏标问题时,CMS使用了SATB方法来做:
因为SATB在重新标记环节只需要去重新扫描那些被推到堆栈中的引用,并配合Rset来判断当前对象是否被引用来进行回收;
并且在最后G1并不会选择回收所有垃圾对象,而是根据Region的垃圾多少来判断与预估回收价值(指回收的垃圾与回收的STW时间的一个预估值),将一个或者多个Region放到CSet中,最后将这些Region中的存活对象压缩并复制到新的Region中,清空原来的Region。
问题:G1会不会进行Full GC?
会,当内存满了的时候就会进行Full GC;且JDK10之前的Full GC,为单线程的,所以使用G1需要避免Full GC的产生。
解决方案:
加大内存;
提高CPU性能,加快GC回收速度,而对象增加速度赶不上回收速度,则Full GC可以避免;
降低进行Mixed GC触发的阈值,让Mixed GC提早发生(默认45%)
G1的第一篇paper(附录1)发表于2004年,在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。为何oracle要极力推荐G1呢,G1有哪些优点?
首先,G1的设计原则就是简单可行的性能调优
开发人员仅仅需要声明以下参数即可:
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。
其次,G1将新生代,老年代的物理空间划分取消了。
这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。
对象分配策略
说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:
对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。
最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
Young GC 阶段:
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
它的GC步骤分2步:
全局并发标记(global concurrent marking)
拷贝存活对象(evacuation)
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?
在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:
初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。
根对象被置为黑色,子对象被置为灰色。
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。
这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题
我们看下面一种情况,当垃圾收集器扫描到下面情况时:
这时候应用程序执行了以下操作:
这样,对象的状态图变成如下情形:
这时候垃圾收集器再标记扫描的时候就会下图成这样:
很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:
在插入的时候记录对象
在删除的时候记录对象
刚好这对应CMS和G1的2种不同实现方式:
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。
至此,混合式GC告一段落了。下一小节我们讲进入调优实践。
MaxGCPauseMillis调优
前面介绍过使用GC的最基本的参数:
前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。
Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。
在理解了这些后,我们再设置最大暂停时间就好办了。 首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。
其他调优参数
避免使用以下参数:
避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。
触发Full GC
在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?
并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。
晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:
巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
由于篇幅有限,G1还有很多调优实践,在此就不一一列出了,大家在平常的实践中可以慢慢探索。最后,期待java 9能正式发布,默认使用G1为垃圾收集器的java性能会不会又提高呢?
G1处理和传统的垃圾收集策略是不同的,关键的因素是它将所有的内存进行了子区域的划分。
总结
G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过**首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。
参考链接:
https://juejin.cn/post/6859931488352370702
https://blog.csdn.net/qq_39276448/article/details/104470796