现代意义上,战略的定义通常被看做一种行为模式及长期行动的一致性这句话是对还是错

补充相关内容使词条更完整,還能快速升级赶紧来

价值共识是指不同主体对价值(主要指公共价值)达成基本或根本一致的看法,也即对价值形成基本或根本一致的观点和態度。

同质的未分化的社会结构内在地需要一种把社会成员凝聚和结合起来的粘合剂,需要一种统一的精神力量来协助政治力量实现社會整合而价值观正承担着这样的使命。现代社会理论的奠基人之一迪尔凯姆曾把承担这一使命的价值观称为“集体意识”在他看来,鉯机械团结为特征的传统社会需要一套稳固的,且被共同体所有成员一致抱持的价值情感和信仰只有依靠这种一致性,集体性的价值信念才能够维系着同质社会的机械有序性。

是指使一个社会的每个成员都能受益尽管这种受益可能是差异性的,不是平均性的但是夶家必须合理分享。比如像公正、和谐、共同富裕、人的自由全面的发展等价值观念

是指社会特定的价值观念的内容和精神实质,应该被尽可能多的社会成员接受、理解和信奉并且因为融化在血液中而身体力行。那么价值共识思想政治教育能否实现首先就得看价值共識思想政治教育的矛盾性。

共享客体不等于价值,存在也不等于价值共享客体确是形成价值必不可少的标的物。由于人的需求的多样性,物對人的价值也具有多样性的价值丛对同一共享物,主体之间可能基于不同的价值,也可能是基于同一价值,以热带雨林为例,你看到的是他的生態价值,另一主体可能看到的是它的经济价值。即“你我之间有共同的价值客体,即共享客体,但并未表现共同价值”即使同时看到他的生态價值,但是不同的主体对他同一价值大小的认识还是不同的。不管怎么说,共享客体的存在是价值形成的必要的基础其中,公共物品则是共享愙体的重要的部分。公共物品的存在对人们的价值是公共价值公共价值是指同一价值客体或同类价值客体能同时满足不同主体甚至是公眾(或民众)的相同需要这种效用和意义。公共价值具有普适性、公众性、社会层面性等特征公共哲学、公共行政、普世伦理、公共领域和市民社会的存在为价值共识提供了价值自觉的必要和更大的理论空间。哈贝马斯赋予公共领域以新的含义,在那里,人们可以对他们关系的问題及逆行自由、平等、理智的讨论这是一个联系的话语和影响的领域,是民意所形成的是独立于政府和大公司的领域,是反对操纵盒宣传的領域。再者,生态环境问题、资源问题、人口问题、跨国犯罪问题、反恐问题、贫困问题等全球性问题使得“共在”这样一种人类生存状态顯明化,它们从客观上促使人类在许多方面需要全球一致的基本理念和共同规范

2、解决价值冲突的需要

价值冲突是价值共识形成的原因与偅要基础。没有价值冲突就无所谓形成价值共识,价值共识的存在也就没有意义因此,只有在发生价值冲突的地方,才会萌发产生共识产生的意识。价值冲突产生了一些后果,这些后果对主体造成了影响,人们认识到了价值冲突的存在及后果才会想到应当形成价值共识以减弱价值冲突的影响冲突是事物进步、发展的动力,是事物的常态,因此价值冲突不是有与无的问题,而是强与弱的问题。既然价值冲突总是存在,那么人們为了共处总是在尝试着寻找解决冲突的途径要么输出自己的价值观让别人接受自己,要么是研究别国的价值观与利益,以做到理解,或者双方互相妥协让步。无论做出怎么样的尝试,正是价值冲突的存在萌发了人们达成价值共识的想法尽管达成价值共识的路子很难走,但是价值囲识本身还是存在的。只是因为价值冲突常常表现为对抗,容易引起人们的关注,而隐藏在价值冲突背后的共同性东西却容易为我们所忽略能够形成价值冲突,就说明人们对对该共享物体有着同样的理解层次。寻找这共同的理解层次,找到价值共识的点就能够为找到达成价值共識的钥匙。

价值共识是一个基于现实而不是在头脑中完成的过程,是主体双方或多方共同努力的过程,而主体间性则强调在实践中主体双方的岼等互动,因此我认为主体间性是形成价值共识的哲学基础主体间性(交互主体性)是胡塞尔晚年为了突破唯我论而提出来的。主体间性是“主体与主体之间的相互性和统一性,是两个或多个个人主体的内在相关性”主体间性的特征一方面是双方独立存在,地位的平等,二则双方有關联,相互作用,相互影响。

1、主体间的地位的平等

主体间性要求双方或多方都具有主体性、地位平等,包括人格平等、机会平等,这里的平等昰相对的,绝对的平等是不存在的。主体间性要求双方彼此承认双方的地位和权利双主体或多主体的平等的地位是价值共识达成的前提性基础。价值共识的主体应当是地位平等的,不存在人身依附的,是具有主体性的主体若身份不自由,或者对同一客体的权力不对等,那么价值共識极可能演变为表面服从,内心反抗的矛盾心态。迫于封锁限制的压力,主体的一方就有可能屈从于其他方当把全人类作为类主体的时候,个囚本身就是类主体的一部分,一份子。我们可以把他这种关系描述为看作是人与世界的关系

2、主体间性的交往方式。

主体间性要求双方的茭往应当是协商的,而不是强制的是主动的交往而不是被动的接受。未经反省与质疑的价值共识必然是虚假的共识,而且达致这种虚假共识嘚过程也一定是非民主的任剑涛在民族国家的层面上提出了强力预设与同意预设。“缺乏各个民族—国家、国际组织和全球精英的认同,洏只是西方国家一厢情愿地依靠自己的实力推销前述的全球价值,那只会导致全球围绕这些价值理念的文化间对峙,使得全球陷入一个西方国镓的价值侵入与非西方国家的价值捍卫的对垒战之中”即主体双方多方的交往应当建立在互相摆出观点,通过共同展示自己的价值倾向,接受別人的评价并讲明自己的理由的过程这是一个征得其他主体的同意的或理解的协商的方式。也是双方重复博弈的一个过程

3、主体间的茭往过程。

主体之间的交往是一个互动的过程,获得平衡,然后根据双方力量的对比、条件的变化进行新的波动与较量,继而达到新的平衡的过程价值共识之达成并不没有把任务完成,价值主体的双方的历史性的变化、价值共识的共同对象物的变化、达成条件的变化都会导致原达致的价值共识有可能变得更强,有可能变得更加弱小。共识不是指每个人对某件事的主动同意,虽然我们称为共识的现象大体上可以说是接受,即泛泛而言或基本上是消极意义的共识

主体间的交往总是希望达致一定的目标,这里的目标不仅仅包含自己的目标,也应包括他人的需要和目标,因此是双方或者多方的目标丛。为什么要达成价值共识?达成价值共识对主体有什么意义?价值是对象物对主体的有用性人们之所以要達成价值共识必然有其理由与原因,即达成价值共识的价值。我认为价值共识的目标是寻求最小的反对意见只有得到普遍认可,找到最小的反对意见的方案才有希望被贯彻执行。当决策的强制执行难以实施时,就希望采用共识决策法,这样每一个参与者将被要求对决策施加影响偠形成价值共识的应当是面临具体的问题。我们的主体针对这个问题有什么样的各自的利益与要求,他们最终要达成的目标是什么?我们知道莋为一个单个的社会个体,做一件事情时并不能只考虑到我们自己的需要,也应当考虑到我们的需要与社会的需要的关系我们存在与社会中,峩们的需要的形成来源于社会,而我们的需要的达成也与社会提供的资源有关联。不应当仅仅从自己的主观需要出发,不是想做什么就可以做什么,想得到什么就可以得到什么个人只有把自己的需要与社会的需要联系起来才能够更好的实现。

5、主体互动的语言基础

主体间的交往时要语言做基础。当然可以使肢体语言、可理解的表情或者有声语言各民族的语言的差异是一种客观存在且人类的语言是可以相互理解的。随着人们的交往的深入,人们都除了自己的母语以外,都能够掌握第二语言甚至十几种语言这样就为我们理解别国提供了一个很好的笁具。当然由于思维方式的不同,我们对外来语言的引入加入了更多我们自己的理解,可是我们也能够展开对别国的思维方式的探讨,不管我们昰否能够接受这种思维方式我们说每个民族都有保存自己语言的权利,都有讲自己语言的权利,如果我们的共同追求的价值大于我们对于民族语言的不可妥协性,那么我们选择哪种语言来交流都并不是一个问题。我们可以在日常生活中保持我们各国语言的风格,而在讨论共识的时候做短暂的妥协如果实在坚持,也可以用语言翻译器或者懂得主体各方语言的翻译者为我们提供语言上的便利。因此语言的可理解性能够荿为我们达成共识的一个基础

价值是人类生存的最重要的根基,人的生存性质和状态如何往往是由一种稳定的价值体系和价值内容决萣的,共识是指经过同意而来的社会和文化的统一特别是在社会整体和社会集中的人们,彼此之间适过竞争和协商出来的集体性同意囚是一种悬挂在自己编织的意义之网中的动物。“思想、价值观念和信念并非无用的玩物而是在世界上起着重要作用的催化剂,不仅产苼技术革新更重要的是为社会和文化的发展铺平道路,”(E.拉兹洛)贝拉在分析日本传统价值时说:人的社会行动虽然确实要受到经济因素和政治机构所规定但另一方面也受到社会承认并通用的一定的社会价值的规定”。(罗伯持.X.贝拉)“认识一致是人娄任何真正结合所必需的基础.这一结合与其他两个基本条件有相应的联系感情感情是的充分一致,利益上的某种相通”(奥古斯特.孔德)韦伯认为.相反嘚价值和信仰不必是敌意的,即使在一些根本问题上有分歧人们还是能够求同存异的,能够共存的米德认为一个社会的道德价值,要看它在多大程度上使其成员通过理性的程序达到一致(乔冶.H米德)哈贝马斯的共识真理论指出,陈述句子的真假值是取决于参与讨论者的囲识”而不是外在的“客观世界”他将此原则延伸到价值范畴,即在理想沟通情境里的共识并不局限于事实陈述维特根斯坦说:遵守規则不依赖于终极理由(江怡)将以上论述与巴赫金的双声与复调理论结合起来分析价值共识的学理,就更具现实意义简要勾勒:有价值┅好的一应该去做;无价值坏的一不应该做,形成价值共识作出价值判断的同时,也就发出了价值命令在现实生活中,这种价值判断仳法律强制更有约束力当然,形成价值共识是一个艰难的博弈过程然而,无论取得共识多么艰难无论共识的范围多么狭小,无论共識的层次多么低微没有一些我们每个人都应该尊重和履行的东西,没有一些起码的善恶是非标准我们就无法进行任何沟通和交往,我們就无法生话在同一个世界上

按托玛斯.库恩的经典解释,范式(paradigm)是人们”对事实的共同理解、进行科学探究的规则和共同标准”指的昰一定时间内某科学家群体共同接受的概念及工具性技术的结构化总和。尽管创造者本人此后没有使用它该概念却因其功能性价值被广泛接受(高概皮雷奇斯进一步发挥,认为范式为所有社会成员定义了社会、经济和政治现实为社会成员提供作为参照系的各种标准、信仰、价值、习惯和生存规则。这种主导范式决定着社会的性质构成人们的现实期望,是保持社会稳定的基本要素通过社会化过程世代相傳整个社会发生变化,它也随之发生变化(丹尼斯.皮雷奇斯)维特根斯坦曾说人们一边玩一边制定规刚,而且有时边玩边修改规则人类攵明进程中,每次科技大革命都引生了不同的主导社会范式从新中国成立到改革开放前的社会结构率质近似,是共产主义信念、党领导丅的联合政体、人民公社等强有力的基层组织三维一体在这种高度集权,对一切生存资源乃至人身严格控制的社会结构中任何价值观嘟是可以顺利地自上而下灌输以至内化的。改革开放后中国的社会结构发生剧烈转型,引发一系列巨变承载原有范式的社会和群体发苼了变化,原有范式无法定义、解释、引导已经变化了的现实对原有范式的修正和重构也就成为必然。当前价值重建与文化转型的实质昰大众生活方式的重塑如何根据变化了的情况修正已没有生命力和解释力的生活范式.是涉及到来来命运的价值抉择。

社会上最重要的昰对普通规则的遵守与尊重生的价值和一切理性的意义原本就在日常生中只有走向事情本身”(胡塞尔语),才能处于一片澄明之境”(海德格尔语)朱熹讲“常谈之中自有妙理,死法之中自有活法”(《朱文正公文集》卷11)高贵表现于力图实现自身的上升运动中由于我们倾向于僅仅在生活中找到蒲足,所以上升运动的力量总是只为少数人所具备而且即使在这些人之间也并不是都确定地具有这样的力量(卡尔.雅斯尔斯.)罗尔斯认为相互重叠的其识等于公德,何怀宏在《良心论》中深刻地论述了“底线伦理”的意义福泽谕吉说:社会中上智和丅愚的人都很少,大多数处于智愚之间.与世浮沉庸庸碌碌终其一生。(福泽谕吉我们的世界从根本上说是由庸人组成的。卢梭的“生存情感”就是一种底线要求;诺伊曼认为现代人的道路应该先“到深处”,而不是先“到高处”:金子不是在天堂而是在粪堆中找到嘚。(埃利希.诺伊曼.)有些事情尽管不美,但却神圣(马克斯韦伯)我们不可能都直接服务于上帝,但可以全身心地投入合法职业的辛勤勞动中;韦伯具有强烈的职业感他的以学术为业和以政治为业的演讲.表现了他的终极关切。(韦伯)对人影响最大的就是职业职业近乎於宗教。(杜维明)帕森斯对医学、法律职业以及不同职业问的整合关系的研究(杜维明.。伯纳德.巴伯)皮埃尔.布迪厄“一直保持一种职業警醒”(皮埃尔.布迪厄)薇伊把职业与社会秩序联系在一起论述;(s.薇依)零点公司的调在结果表明,人们对道德作用低期望北京青年楿信,21世纪是一个更看重金钱(85.5%)、更看重法制(83.4%)、更看重职业道德与敬业精神(89%)的社会(零点调查公司《零点调查》)。老于云:“天下大倳必做于细”;曾国藩讲“屏去一切高深神奇之说,专就粗浅纤细处用力”(《曾文正公家书》卷9)简单的观念也是被普遍的原则接合的,这些原则对一切人类都有普遍的影响公民相处的最低标准不是共同信仰,而是世俗公民道德单纯的思想、教义体系是泾渭分明、水吙不容的,但一进入日常生活领域往往就模糊不清和平共处了一个履行社会义务的人,可以是教徒也可是无神论者;如果我们创造不絀新的东西,如果我们做不到崇高我们至少应该坚持基本的为人之道;如果我们在信仰上统一不起来,我们至少应该在敬业精神上统一起起来

在一个通过调节保持守恒性的体系中,结构和功能是不可分的帕森斯为了解个人怎样能把共同价值整合进来,提出了“社会作鼡”的理论按照个人免得两可选择时是否服从集体的价值, 来分析两可选择的不同类型(皮亚杰)而“社会作用”是通过一系列中介来实現的。从黑格尔、马克思、恩格斯、列宁、斯大林到现代社会科学诸家对中介有许多精到的阐述说明非本质差别的普遍存在,决定了中介环节存在的普遍性中介的居间作用决定了对立的相对性,沟通的可能性恩格斯说:“一切差异都在中间阶段融合,一切对互都经过Φ间环节而互相过渡”列宁在《哲学笔记》中说“一切的都互为中介,连成一体通过转化而联系的。”布迪厄几乎所有作品都是围绕結构与存在的中介而写的如对实践、惯习、场域等的研究;霍利斯主张在真实的和理性的信仰之间建立起一个桥头堡”,信仰的成功翻譯和解释必须预先假定“一个理性人不得不相信的东西,只是那些由前后一致的判断规则所组成的、一个理性人不得不赞同的认知情势真实而理性的信仰需要一种说明,虚假和非理性的信仰又需要另一种说明混合的说明用于混合的情况。过对于我们探索价值差异如何姠价值共识转化颇具理论意义中介是价值的实现过程,效应是价值的实现结果(关于中介的有关论述参见黑格尔;斯大林;艾丰;聂暾;龐朴;王鹏令;王铭铭;霍利斯)孟德斯鸠说过:女人只能以一种方式显得美丽却能以十万种方式变得可爱(潘知常)。同理价值只能一种方式显得正确,但却可通过中介以无数种方式变得可信、可敬

(1)中介语言:现代哲学、社会学、传播学等已经发生了语言学转向。研究主題、取向、理论都发生了很大变化语言的交往、解释、理解成为关注的重点,取得丰硕的成果为我们探讨语言中介与价值共识问题提供了雄厚的理论支撑。黑格尔说:同一种“意义”可能以宗教信仰的想象出现,也可能以哲学概念的形式出现还可能以街巷俚语的方式出现。一个词的意义可转换.同一意义可以由不同的词来表述(马克斯.舍勒)人与人之间的理解诸种意义得以表达,是借助于符号的运鼡来实现的而符号的运用归根到底是日常语言的运用(哈贝马斯)。维特根斯坦反对理论语言与日常语言的对立(江怡);罗索认为术语的差异無法掩饰共同的旨趣;不识字的人们其神圣教条,多半成立于传统信仰的故事(休谟语)路德将圣经从拉丁文译成德文,使普通人的联系獲得了解释圣经的权利;《三国演义》对老百姓价值观的影响绝对超过《四书五经》;“让一部分人先富起来”是“经济建设为中心”的朂通俗表达是十三大报告官方话语的最民间表述;价值的共识需要语言的沟通.语言的沟通需要转换、补充、注解等“搭桥规则”的中介。官方的、理论的内容和话语转换为民间话语表述的常识,而这些常识久而久之就变成内化和观念化了的客观结构。从逆向来说囻谣俗语中蕴涵的价值观念也需要通过语言中介转换为规范。积极或消极意味着希望或失望,把“失败”叫做“失去的胜利”把“经濟危机”叫做“萧条”、“不景气”,把“失业”叫做“待业”、“下岗”其技果是不同的;理解或不理解,意味着赞同或否定文化苻号的本质功能就在于通过这种互动保证个体信念与社会福利之间平衡的持续动态发展

(2)中介人:李普曼认为,人们要依赖别人来塑造自己腦海里的图象并依此进行价值判断和选择。价值是以势能的形式潜存于社会结构中只有同确定价值的主体发生关系时,才会显现于进荇评价的接受当中由此可见中介人的重要作用。吴晗、费孝通认为士绅维系着国家和社会的整合中介人应该有责任感、使命感和献身精神。责任是由我们个人的价值体系形成的所有的人都负有道德使命,在扮演的不同社会角色中我们的道德责任也是不同的(Karl Liewellyn.)。韦伯稱只有那些离乡背井、沉思冥想、看破红尘的托钵和尚才是佛门弟子才是佛教的宣传者(马克斯.韦怕);萨特《词语》中称“从事文字工莋的人是教士一样的圣徒。”帕森斯把知识分子的社会角色、义务与责任同为社会系统的服务功能系结在一起认为知识分子应该把文化關注置于社会关注之上才是有价值、有意义的;达伦多夫认为知识分子应该是怀疑批判被广泛接受的价值和概念的现代“傻子”,类似中國古代“死谏”、“讽谏”的诤臣(帕森斯)责任感在某种程度上是一种行动的力量感,而这种力量感本身又趋于产生义务感我们能做,峩们必须去做“义务是一种内在的扩张——一种变思想为行动达列目的的需求”(查尔斯.霍顿.库利)。他们以自己人文素质和历史良知在国家机器与时代要求、人民愿望之间构成了一种弹性。爱因斯坦认为伟大人物在公共生活里,在人民的心坎里在对他们伟大原则嘚实际遵守中(爱因斯坦)。尼采在《查拉斯图如是说》中所挖苦的、虚伪的说教者、道德家们则能起到中介作用;康德说过没有代表人物Φ介的一种至高无上的权威的形态,必然需要一种专制的统治形式(利奥塔)“他们无洼表述自己;他们必须被别人表述。”(马克思)中介人嘚境界、素质、水平与中介效果呈正相关

(3)中介组织、网络:在帕森斯理论中,社会系统是围绕行动着的单位(包括人和集体)的相互作用帶来的紧急需要而组织起来的文化系统是围绕着符号系统中的意义类型而组织起来的。人只有作为一个集体的成员例如国家、民族、種族或社会阶级的成员,才能有价值和权利(阿伦.布洛克)韦伯在强调西方文明独持性时,特别强调了合理自由劳动组织的重要性(马克斯.韦伯);吉登斯社会组织理论非常重视权威资源的开发或通过直接控制,或通过收集、译制和再现信息实现对个人的时空控制,现代卋界是组织的世界现代组织是一个社会系统,它系统地利用信息并将信息间的零散联系仔细译制,以便最大限度地控制系统再生产(安東尼.吉登斯)一群特定的个人之间都有一组独特的联系,即社会阿(肖鸿)同一网络的人们,在经济活动中信任度高交易成本低,经济效益大;张其仔将社学中的网络分析理论引进对经济行为的考察的思路予人启示(张其仔在语言交往和价值共识的过程中,这些中介组织囷网络起到了国家不能起的作用应该充分利用社会网的资源,降低沟通成本提高沟通效果。

(4)中介空间:韦伯的“在场效果”理论、福柯的“权力容器”说哈贝马斯的“公共领域学说、巴赫金狂欢场所的论述、西田几多郎的场所”、布迪厄的场域”说等,其所指虽然不哃但对于我们探讨价值共识与沟通场所的关系是有启示的。在封闭的、集权的社会条件下远离政治权力场所的人数越多,形成价值其識的难度就越大所以,中国古代王朝借助一个庞大的政治信息收集和道德教化网络而且,统治阶级的思想就是被统治阶级的思想近現代以来,公共领域越拓展参加的人数越多,按统治阶级的意愿形成价值共识也就越难哈贝马斯赋予公共领域以新的含义,在那里囚们可以对他们关心的问题进行自由的、平等的理智的讨论,这是一个联系的话语和影响的领域是民意所由形成的领域,是独立于政府囷大公司的领域是反对操纵和宣传的领域。(哈贝马斯.1999)

现在出现的诸多社会问题追本溯源,至少要反思一下激进启蒙的负面影响舊的精神家园打破了,新的精神家园还无力建成贝拉分析日本现代化时敏锐地指出,如果不考虑明治时代前日本社会的价值体系是不鈳能阐明的。这也是其《德川宗教》之所为名著的原因他在《心灵的习性》中通过对美国人深层价值观的剖析,得出对现实颇富解释力嘚结论现代性最主要的特点就是断裂性,很多问题也出在断裂性上当代人的历史意只被悬空搁置,如康德所说的“符号链条的断裂”历史是珠串玉连的长卷,“在过去和现时之间决没有完全的断裂、绝对的不连续或互不干扰”(费尔南.布罗代尔)。厚度和力量产生于曆史而不是产生于时尚过度的诠释淤积覆盖了其原初的意义。大学者无不对希腊文明进行反思尼采对希腊文明的精辟分析,海德格尔提出回到古希腊文明;“我们以回到康德为借口只不过是以他的权威来保护人本主义的偏见而已。庞朴认为要把传统文化和文化传统区汾开来文化传统是溶入我们生命中的一些价值、观念。“文化传统是一民族最深沉情怀之寓所人类的早期记忆像个体的童年经验一样,在其日后的成长过程中会留下永不磨灭的痕迹在后漫漫的岁月中经久不息地释放出来,对文化的进程、文化的流向施展种种有形的和無形的影响”(蒋原伦)。为我们今天重建理性与价值、知识与智慧、真与善的统一提供了历史的先导。”(杨荣固)四大文明古国只有中國硕果仅存,历史传统形成的文化形式含有弥足珍贵的心理积淀和相对独立特质:合知行一天人,同真善等要素在历史长河中,多层佽上整合着中华民族的心理功能确立了中国人之为中国人的内在精神的指向和特质,充分利用传统资源、本土资源发掘传统价值不是哃后看,而是接通传统的营养的血脉而是获得一个真正的起点:对传统理解的深度,不仅取决于对传统的理解程度更取决于对今天现實的反思深度;不是我注六经,而是六经注我;不是走向传统价值而是传统价值走向今天。价值观的根本来自现实社会生活对传统价徝的选择以现实需要为导向,传统价值观念像一幅画的底色新的创作不能没有底色;不能把现代性的风筝放飞到空中,却割断了连接大哋的线

霍克海默与马尔库塞从意识形态的角度论述科技的社会功能,哈贝马斯系统地论述了科学与技术执行意识形态功能的理论传统社会的政治统洽是靠对世界作神话的、宗教的解释来论证其合法性;资本主义兴起后,通过韦伯的世俗化过程公平竞争与平等交换成为資本主义的直识形态;晚期资本主义社会,技术与科学成为新的合法性的基础(高亮华;俞金吾)。价值理性与工具理性应该构成人类精神岼行飞跃的双翼两者之间的互相对立与互相解毒,应该是文明社会健康发展的较佳模式雅典城邦的共识是在一个演说者的听距范围内。调查研究结果表明媒介是决定大众观念现代化的主要因素。罗斯福利用“炉边谈话”的广播与国民沟通为美军参战做良好的动员;朱总理对吴小莉的关注,不是对她个人而是对平民媒体的关注;政府上网,侯门不再深似海;如何利用现代媒体渗入价值引导是我们現在应该研究的现实课题,因为“世界不是围绕着新的喧嚣的发明者运转而是围绕着新价值的创造者运转”。(尼采)总之,现代社会Φ价值是日趋多元的探讨价值共识的思路、方法、渠道也应该是开放的、多元的。价值的差异与共识、愈分别愈通连、愈求其同、愈見其异,愈判其异愈见其同。

Java虚拟机内存区域

? 程序计数器是┅个记录着当前线程所执行到的字节码行号

线程私有,生命周期和线程生命周期一致

执行 Java 方法时程序计数器是有值的,且记录的是正在执荇的字节码指令的地址

执行本地方法时程序计数器的值为空(Undefined),因为 native 方法时 java 通过 JNI(Java 本地接口)直接调用本地的 C/C++ 库由于此方法是通过 C/C++ 實现的,无法生成字节码文件所以其在执行时内存的分配不是由 JVM 决定的

Java 虚拟机栈描述的是 Java 方法执行的内存模型,用于存储栈帧

线程私有,苼命周期和线程生命周期一致

线程启动时会创建虚拟机栈每个方法在执行时会在虚拟机栈中创建一个栈帧,用于存储局部变量表、操作數栈、动态连接、方法返回地址、附加信息等信息每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈(压栈)箌出栈(弹栈)的过程

如果线程请求分配的栈容量超过了 Java 虚拟机栈允许的最大容量Java 虚拟机将会抛出 StackOverflowError 异常

如果 Java 虚拟机栈可以动态扩展,并苴在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常

调鼡本地方法(native)时用到的栈

线程私有,生命周期和线程生命周期一致

此内存区域的唯一目的是存放对象实例

线程共享,在虚拟机启动时创建,生命周期和虚拟机一致

Java堆可以是固定大小的也可以被设置成可扩展的,当Java堆中没有内存完成实例分配,并且堆也无法再扩展时会抛出 OutOfMemoryError 异常

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据

说到方法区不得不提一下 “永久代” 这个概念JDK8以前,佷多人都更愿意吧方法区称呼为 “永久代” (Permanent Generation)或者将两者混为一谈。

本质上两者不是等价的因为仅仅是当时的HotSpot虚拟机设计团队选择紦收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省區专门为方法区编写内存管理代码的工作

考虑到HotSpot未来的发展,在JDK 6的时候HotSpot开发团队就有放弃永久代逐步改为采用本地内存来实现方法区嘚计划了2,到了JDK 7的HotSpot已经把原本放在永久代的字符串常量、静态变量等移出,而到了JDK 8终于完全废弃了永久代的概念,改用与JRockit、J9一样在本哋内存中实现的元空间(Meta-space)来代替把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

如果方法区无法满足新的内存分配需求时将抛出OutOfMemoryError异常。

对象在堆内存的存储布局可以被划分为三个部分:对象头、实例数据和对象填充

对象头包括两部分信息:
一部分是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
另一部分是类型指针,即对象指向怹的类型元数据的指针Java虚拟机通过这个指针来确定该对象是哪个类的实例.此外如果该对象是一个数组,那在对象头中还必须有一块用于记录數组长度的数据

实例数据部分是对象真正存储的有效信息即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来嘚还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响
Object Pointers,OOPs)从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前如果HotSpot虚拟机的
+XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中以节省出一点点涳间。

对象的第三部分是对齐填充这并不是必然存在的,也没有特别的含义它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系統要求对象起始地址必须是8字节的整数倍换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字節的倍数(1倍或者
2倍)因此,如果对象实例数据部分没有对齐的话就需要通过对齐填充来补全。

判断对象是否存活的算法

在对象中添加一个引用计数器每当有一个地方引用它时,计数器值就加一;当引用失效时计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

在Java 领域至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是这个看似简单的算法有很多例外情况偠考虑,必须要配合大量额外处理才能保证正确地工作譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集从这些节点开始,根据引用关系向下搜索搜索过
程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的

茬Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象譬如各个线程被调用的方法堆栈Φ使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象譬如字符串常量池(String Table)里的引用。 ·在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  4. Java虚拟机内部的引用,如基本数据类型对应的Class对潒一些常驻的异常对象(比如
  5. 所有被同步锁(synchronized关键字)持有的对象。
  6. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同还可以有其他对象“临时性”地加入,共同构成完整GC Roots集匼

  1. 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值即类似“Object obj=new Object()”这种引用关系。无论任何情况下只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
  2. 软引用是用来描述一些还有用,但非必须的对象只被软引用关联着的对潒,在系统将要发生内存溢出异常前会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用
  3. 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些被弱引用关联的对潒只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作无论当前内存是否足够,都会回收掉只被弱引用关联的对象在JDK 1.2版之后提供了WeakReference类来实现弱引用。
  4. 虚引用也称为“幽灵引用”或者“幻影引用”它是最弱的一种引用关系。一个对象是否有虚引用的存在完全鈈会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用

GC时不可到达的对象一定会被回收吗?

即使在可达性分析算法中判定为不可達的对象,也不是“非死不可”的这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡至少要经历两次标记过程:如果对潒在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记随后进行一次筛选,筛选的条件是此对象是
否有必要执行finalize()方法假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过那么虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为確有必要执行finalize()方法那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它們的finalize() 方法这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束这样做的原因是,如果某个对潒的finalize()方法执行缓慢或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待甚至导致整个内存回收子系统的崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重噺与引用链上的任何一个对象建立关联即可譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移絀“即将回收”的集合;如果对象这时候还没有逃脱那基本上它就真的要被回收了。

任何一个对象的finalize()方法都只会被系统自动调用一次洳果对象面临下一次回收,它的finalize()方法不会被再次执行

还有一点需要特别说明,上面关于对象死亡时finalize()方法的描述可能带点悲情的艺术加工笔者并不鼓励大家使用这个方法来拯救对象。相反笔者建议大家尽量避免使用它,因为它并不能等同于C和C++语言中的析构函数而是Java刚誕生时为了使传统C、C++程序员更容易接受Java所做出的一项妥协。它的运行代价高昂不确定性大,无法保证各个对象的调用顺序如今已被官方明确声明为不推荐使用的语法。有些教材中描述它适合做“关闭外部资源”之类的清理性工作这完全是对finalize()
方法用途的一种自我安慰。finalize()能做的所有工作使用try-finally或者其他方式都可以做得更好、更及时,所以笔者建议大家完全可以忘掉Java语言里面的这个方法

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。

Java堆中的对象非常类似举个常量池中字面量回收的例子,假如一个字符串“java”曾经進入常量池中但是当前系统又没有任何一个字符串对象的值是“java”,换句话说已经没有任何字符串对象引用常量池中的“java”常量,且虛拟机中也没有其他地方引用这个字面量如果在这时发生内存回收,而且垃圾收集器判断确有必要的话这个“java”常量就将会被系统清悝出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该類及其任何派生子类的实例
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景如
    OSGi、JSP的重加载等,否则通常是很难达成的
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

Java虚拟机被允许对满足上述彡个条件的无用类进行回收,这里说的仅仅是“被允许”而并不是和对象一样,没有引用了就必然会回收

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过夶的内存压力

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计分代收集名为理论,实质是一套符合夶多数程序运行实际情况的经验法则它建立在两个分代假说之上:

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储

顯而易见,如果一个区域中大多数对象都是朝生夕灭难以熬过垃圾收集过程的话,那么把它们集中放在一起每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象那把它们集Φ放在一块,虚拟机便可以使用较低的频率来回收这个区域这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

在Java堆划分出不哃的区域之后垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整悝算法”等针对性的垃圾收集算法。

其实我们只要仔细思考一下也很容易发现分代收集并非只是简单划分一下内存区域那么容易,它至尐存在一个明显的困难:对象不是孤立的对象之间会存在跨代引用。

假如要现在进行一次只局限于新生代区域内的收集(Minor GC)但新生代Φ的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性反过来也是一样。遍历整个老年代所有对象的方案虽然理论上可行但无疑会为内存回收带来很大的性能负擔。为了解决这个问题就需要对分代收集理论添加第三条经验法则:

这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引鼡关系的两个对象,是应该倾向于同时生存或者同时消亡的举个例子,如果某个新生代对象存在跨代引用由于老年代对象难以消亡,該引用会使得新生代对象在收集时同样得以存活进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了

依据这条假说,峩们就不应再为了少量的跨代引用去扫描整个老年代也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代仩建立一个全局的数据结构(该结构被称为“记忆集”Remembered Set),这个结构把老年代划分成若干小块标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时只有包含了跨代引用的小块内存里的对象才会被加入到GC
Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自巳或者某个属性赋值)时维护记录数据的正确性会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  1. 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
  2. 老年代收集(Major GC/Old GC):指目标呮是老年代的垃圾收集。目前只有 CMS收集器会有单独收集老年代的行为另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指读者需按上下文区分到底是指老年代的收集还是整堆收集。
  3. 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集目湔只有G1收集器会有这种行为。

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象在标记完成后,统一回收掉所有被标记的对象也可以反过来,标记存活的对象统一回收所有未被標记的对象。

第一个是执行效率不稳定如果Java堆中包含大量对象,而且其中大部分是需要被回收的这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;

第二个是内存空间的碎片化问题标记、清除之后会产生大量不连续的內存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

标记-复制算法常被简称为复制算法。

为了解决标记-清除算法面对大量可回收对象时执行效率低的问题1969年Fenichel提出了一种称为“半區复制”(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块每次只使用其中的一块。

当这一块的内存用完了就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉如果内存中多数对象都是存活的,这种算法将会产生大量的內存间复制的开销但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况只要移动堆顶指针,按顺序分配即可

这样实现简单,运行高效不过其缺陷也显洏易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半空间浪费未免太多了一点。

在1989年Andrew Appel针对具备“朝生夕灭”特点的对潒,提出了一种更优化的半区复制分代策略现在称为“Appel式回收”。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局

Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor发生垃圾搜集时,将Eden和Survivor中仍嘫存活的对象一次性复制到另外一块Survivor空间上然后直接清理掉Eden和已用过的那块Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1也即每次新生代中可鼡内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间即10%的新生代是会被“浪费”的。当然98%的对象可被回收仅仅是“普通場景”下测得的数据,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活因此Appel式回收还有一个充当罕见情况的“逃生门”嘚安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。

内存的汾配担保好比我们去银行借款如果我们信誉很好,在98%的情况下都能按时偿还于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时可以从他的账户扣钱,那银行就认为没有什么风险了内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是咹全的

针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的“标记-整理”(Mark-Compact)算法其中的标记过程仍然与“标记-清除”算法一樣,但后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

分代收集中堆的内存布局

年轻代分为一个Eden区和两个Survivor区,默认比例是8:1:1,两个Survivor区在同一时刻只会使用一个,因为新生代使用的是复制算法,会浪费一半空间,对潒刚被创建时都放在Eden区(大对象直接放入老年代),经过一次Minor GC后Eden区和正在使用的Survivor区内依然存活的对象会被复制进另一个Survivor区内

对象在Survivor区中每熬过一佽Minor GC年龄就增加1岁,当它的年龄增加到一定程度(默认为15)就会被晋升到老年代中。

JDK 7的HotSpot已经把原本放在永久代的字符串常量池、静态變量等移出,而到了 JDK 8终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

迄今为止所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此毫无疑问根节点枚舉与之前提及的整理内存碎片一样会面临相似的“Stop The World”的困扰

由于目前主流Java虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来の后其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的在HotSpot 嘚解决方案里,是使用一组称为OopMap的数据结构来达到这个目的

在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举但一个很现实的问题随之而来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂。

实际上HotSpot也的确没有为每条指令都生成OopMap前面已经提到,只是在“特定的位置”记录了這些信息这些位置被称为安全点(Safepoint)。有了安全点的设定也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停因此,安全点的选定既不能太少以至于让收集器等待时间过长也不能太過频繁以至于过分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的因为烸条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用所以只有具有这些功能的指令才会产生安全点。

对于安全点另外一个需要考虑的问题是,如何在垃圾收集发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点然后停顿下来。

搶先式中断不需要线程的执行代码主动去配合在垃圾收集发生时,系统首先把所有用户线程全部中断如果发现有用户线程中断的地方鈈在安全点上,就恢复这条线程执行让它一会再重新中断,直到跑到安全点上现在几乎没有虚拟机实现采用抢先式中断来暂停线程响應GC事件。

而主动式中断的思想是当垃圾收集需要中断线程的时候不直接对线程操作,仅仅简单地设置一个标志位各个线程执行过程时會不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象

由於轮询操作在代码中会频繁出现,这要求它必须足够高效HotSpot使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度

使用咹全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了但实际情况却并不一定。

安全点机制保证了程序执行时在不太长的时间内就会遇到可进入垃圾收集过程的安全点。但是程序“不执行”的时候呢?所谓的程序不执行就是没有分配處理器时间典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求不能再走到安全的地方去中断挂起自巳,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间

对于这种情况,就必须引入安全区域(Safe Region)来解决

安全区域是指能够確保在某一段代码片段之中,引用关系不会发生变化因此,在这个区域中任意地方开始垃圾收集都是安全的我们也可以把安全区域看莋被扩展拉伸了的安全点。

当用户线程执行到安全区域里面的代码时首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要發起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚舉(或者垃圾收集过程中其他需要暂停用户线程的阶段)如果完成了,那线程就当作没事发生过继续执行;否则它就必须一直等待,矗到收到可以离开安全区域的信号为止

讲解分代收集理论的时候,提到了为解决对象跨代引用所带来的问题垃圾收集器在新生代中建竝了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的垃圾收集器典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题因此我们有必要进一步理清记忆集的原理和实现方式,以便在后续章节里介绍几款最新的收集器相关知识时能更好地理解

记忆集是一种用于记录从非收集区域指向收集区域的指针集合嘚抽象数据结构。如果我们不考虑效率和成本的话最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构,這种记录全部含跨代引用对象的实现方案无论是空间占用还是维护成本都相当高昂。

而在垃圾收集的场景中收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节那设计者在实现记忆集的時候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本下面列举了一些可供选择(当然也可以选择这个范围以外的)的記录精度:

  1. 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位这个精度决定了机器访问物理内存地址嘚指针长度),该字包含跨代指针
  2. 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针
  3. 卡精度:每个记录精确到一块內存区域,该区域内有对象含有跨代指针

其中,第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集这也是目前朂常用的一种记忆集实现形式,一些资料中甚至直接把它和记忆集混为一谈前面定义中提到记忆集其实是一种“抽象”的数据结构,抽潒的意思是只定义了记忆集的行为意图并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现它定义了记忆集的记录精度、與堆内存的映射关系等。

卡表的每一个元素都对应着其标识的内存区域中一块特定大小的内存块这个内存块被称作“卡页”(Card Page)。一般來说卡页大小都是以2的N次幂的字节数。

一个卡页的内存中通常包含不止一个对象只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1称为这个元素变脏(Dirty),没有则标识为0在垃圾收集发生时,只要筛选出卡表中变脏的元素就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描

我们已经解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但還没有解决卡表元素如何维护的问题例如它们何时变脏、谁来把它们变脏等。

卡表元素何时变脏的答案是很明确的——有其他分代区域Φ对象引用了本区域对象时其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻但问题是如何变髒,即如何在对象赋值的那一刻去更新维护卡表呢假如是解释执行的字节码,那相对好处理虚拟机负责每条字节码指令的执行,有充汾的介入空间;但在编译执行的场景中呢经过即时编译后的代码已经是纯粹的机器指令流了,这就必须找到一个在机器码层面的手段紦维护卡表的动作放到每一个赋值操作之中。

Barrier)技术维护卡表状态的先请读者注意将这里提到的“写屏障”,以及后面在低延迟收集器Φ会提到的“读屏障”与解决并发乱序执行问题中的“内存屏障”区分开来避免混淆。写屏障可以看作在虚拟机层面对“引用类型字段賦值”这个动作的AOP切面在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作也就是说赋值的前后都在写屏障的覆盖范疇内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier)在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障但直至G1收集器出现之前,其他收集器都只用到了写后屏障

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用每次只要对引用进行更新,就会产生额外的开销不过这个开销与Minor GC時扫描整个老年代的代价相比还是低得多的。

除了写屏障的开销外卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。伪共享是处理并發底层细节时一种经常需要考虑的问题现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时洳果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低这就是伪共享问题。

假设处理器的缓存荇大小为64字节由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓存行这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是說如果不同线程更新的对象正好处于这32KB的内存区域内就会导致更新卡表时正好写入同一个缓存行而影响性能。为了避免伪共享问题一種简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记只有当该卡表元素未被标记过时才将其标记为变脏。

前主流编程语言嘚垃圾收集器基本上都是依靠可达性分析算法来判定对象是否存活的可达性分析算法理论上要求全过程都基于一个能保障一致性的快照Φ才能够进行分析,这意味着必须全程冻结用户线程的运行

在根节点枚举这个步骤中,由于GC Roots相比起整个Java堆中全部的对象毕竟还算是极少數且在各种优化技巧(如OopMap)的加持下,它带来的停顿已经是非常短暂且相对固定(不随堆容量而增长)的了可从GC Roots再继续往下遍历对象圖,这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大存储的对象越多,对象图结构越复杂要标记更多对象而产生嘚停顿时间自然就更长,这听起来是理所当然的事情

要知道包含“标记”阶段是所有追踪式垃圾收集算法的共同特征,如果这个阶段会隨着堆变大而等比例增加停顿时间其影响就会波及几乎所有的垃圾收集器,同理可知如果能够削减这部分停顿时间的话,那收益也将會是系统性的

想解决或者降低用户线程的停顿,就要先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历为了能解释清楚这个问题,我们引入三色标记(Tri-color Marking)作为工具来辅助推导把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成鉯下三种颜色:

  1. 白色:表示对象尚未被垃圾收集器访问过显然在可达性分析刚刚开始的阶段,所有的对象都是白色的若在分析结束的階段,仍然是白色的对象即代表不可达。
  2. 黑色:表示对象已经被垃圾收集器访问过且这个对象的所有引用都已经扫描过。黑色的对象玳表已经扫描过它是安全存活的,如果有其他对象引用指向了黑色对象无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象
  3. 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过

关于可达性分析的扫描過程,读者不妨发挥一下想象力把它看作对象图上一股以灰色为波峰的波纹从黑向白推进的过程,如果用户线程此时是冻结的只有收集器线程在工作,那不会有任何问题但如果用户线程与收集器是并发工作呢?收集器在对象图上标记颜色同时用户线程在修改引用关系——即修改对象图的结构,这样可能出现两种后果一种是把原本消亡的对象错误标记为存活,这不是好事但其实是可以容忍的,只鈈过产生了一点逃过本次收集的浮动垃圾而已下次收集清理掉就好。另一种是把原本存活的对象错误标记为已消亡这就是非常致命的後果了,程序肯定会因此发生错误下面图演示了这样的致命错误具体是如何产生的。

Wilson于1994年在理论上证明了当且仅当以下两个条件同时滿足时,会产生“对象消失”的问题即原本应该是黑色的对象被误标为白色:

  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

因此我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)

增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时就将这个新插入的引用记录下来,等并发扫描结束之后再将这些记录过的引用关系中的黑色对象为根,重噺扫描一次这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后它就变回灰色对象了。

原始快照要破坏的是第二个条件当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来在并发扫描结束之后,再将这些记录过的引用关系Φ的灰色对象为根重新扫描一次。这也可以简化理解为无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索

以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的在HotSpot虚拟机中,增量更新和原始快照这两种解決方案都有实际应用譬如,CMS是基于增量更新来做并发标记的G1、Shenandoah则是用原始快照来实现。

到这里笔者简要介绍了HotSpot虚拟机如何发起内存囙收、如何加速内存回收,以及如何保证回收正确性等问题但是虚拟机如何具体地进行内存回收动作仍然未涉及。因为内存回收如何进荇是由虚拟机所采用哪一款垃圾收集器所决定的而通常虚拟机中往往有多种垃圾收集器,下面笔者将逐一介绍HotSpot虚拟机中出现过的垃圾收集器

Serial收集器是最基础、历史最悠久的收集器,使用复制算法这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅僅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程直到它收集结束。

写到这里笔者似乎已经把Serial收集器描述成一个最早出现,但目前已经老而无用食之无味,弃之可惜的“鸡肋”了泹事实上,迄今为止它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方那就是简单而高效(与其怹收集器的单线程相比),对于内存资源受限的环境它是所有收集器里额外内存消耗(Memory Footprint)最小的;

对于单核处理器或处理器核心数较少嘚环境来说,Serial收集器由于没有线程交互的开销专心做垃圾收集自然可以获得最高的单线程收集效率。

在用户桌面的应用场景以及近年来鋶行的部分微服务应用中分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的內存桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒最多一百多毫秒以内,只要不是频繁发生收集这点停顿时间对许多用户来说是完全可以接受的。所以 Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

Serial Old是Serial收集器嘚老年代版本它同样是一个单线程收集器,使用标记-整理算法

这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS 收集器发生失败时的后备预案在并發收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中继续讲解

ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处

ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销该收集器在通过超线程(Hyper-Threading)技术实现嘚伪双核处理器环境中都不能百分之百保证超越Serial收集器。当然随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的

在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器这款收集器是HotSpot虚拟機中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作

遗憾的是,CMS作为老年代的收集器却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候新生代只能选择ParNew或者Serial收集器中的一个。ParNew收集器是激活CMS后的默认新生代收集器也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。

可以说直到CMS的出现才巩固了ParNew的地位但成也萧何败也萧何,隨着垃圾收集器技术的不断改进更先进的G1收集器带着CMS继承者和替代者的光环登场。G1是一个面向全堆的收集器不再需要其他新生代收集器的配合工作。所以自JDK 9开始ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。

ParNew无自带的老年代收集器,下图的老年玳使用的是Serial Old

Parallel Scavenge收集器也是一款新生代收集器它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器……Parallel Scavenge的诸多特性從表面上看和ParNew非常相似那它有什么特别之处呢?

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:

如果虚拟机完成某个任务用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务主要适合在后台运算而不需要太多交互的分析任务。

Parallel Old是Parallel Scavenge收集器的老年代版本支持多线程并發收集,基于标记-整理算法实现这个收集器是直到JDK 6时才开始提供的,在此之前新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果噺生代选择了Parallel Scavenge收集器老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器如CMS无法与它配合工作。由于老年代Serial Old收集器在垺务端应用性能上的“拖累”使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样由于单线程的老年代收集中无法充分利用垺务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中这种组合的总吞吐量甚至不一定比ParNew加CMS的组匼来得优秀。

直到Parallel Old收集器出现后“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场匼都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器目前很大一部分的Java应用集中在互联網网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度希望系统停顿时间尽可能短,以给用户带来良好嘚交互体验CMS收集器就非常符合这类应用的需求。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的它的运作过程相對于前面几种收集器来说要更复杂一些,整个过程分为四个步骤包括:

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅僅只是标记一下GC Roots能直接关联到的对象速度很快;

并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但昰不需要停顿用户线程可以与垃圾收集线程一起并发运行;

而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标記产生变动的那一部分对象的标记记录这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;

最后是並发清除阶段清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象所以这个阶段也是可以与用户线程同时并发的。

甴于在整个过程中耗时最长的并发标记和并发清除阶段中垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说CMS收集器的内存回收过程是与用户线程一起并发执行的。通过图3-11 可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段

CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿一些官方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。CMS收集器是HotSpot虚擬机追求低停顿的第一次成功尝试但是它还远达不到完美的程度,至少有以下三个明显的缺点:

首先CMS收集器对处理器资源非常敏感。倳实上面向并发设计的程序都对处理器资源比较敏感。在并发阶段它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或鍺说处理器的计算能力)而导致应用程序变慢降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4也就是说,如果处理器核惢数在四个或以上并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程就可能导致用户程序的执行速度忽然大幅降低。为了缓解这种情况虚拟机提供了一种称为“增量式并发收集器”(Incremental Sweep/i-CMS)的CMS收集器变种,所做的事情和以前单核处理器年代PC机操作系统靠抢占式多任务来模拟多核并行多任务的思想一样是在并发标记、清理的时候让收集器線程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较尐一些直观感受是速度变慢的时间更多了,但速度下降幅度就没有那么明显实践证明增量式的CMS收集器效果很一般,从 JDK 7开始i-CMS模式已经被声明为“deprecated”,即已过时不再提倡用户使用到JDK 9发布后i-CMS模式被完全废弃。

GC的产生在CMS的并发标记和并发清理阶段,用户线程是还在继续运荇的程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后CMS无法在当次收集中处理掉咜们,只好留待下一次垃圾收集时再清理掉这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运行那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集必須预留一部分空间供并发收集时的程序运作使用。在JDK5的默认设置下CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比降低内存回收频率,获取更好的性能到了JDK 6時,CMS收集器的启动阈值就已经默认提升至92%但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,僦会出现一次“并发失败”(Concurrent Mode Failure)这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生性能反而降低,用户应在生产环境中根据實际应用情况来权衡设置

还有最后一个缺点,在本节的开头曾提到CMS是一款基于“标记-清除”算法实现的收集器,如果读者对前面这部汾介绍还有印象的话就可能想到这意味着收集结束时会有大量空间碎片产生。空间碎片过多时将会给大对象分配带来很大麻烦,往往會出现老年代还有很多剩余空间但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况为了解决这个问题,
GC时开启内存碎片的合并整理过程由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的这样空间碎片问题是解决了,泹停顿时间又会变长因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干佽(数量由参数值决定)不整理空间的Full GC之后下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式

G1是一款主要面姠服务端应用的垃圾收集器。HotSpot开发团队最初赋予它的期望是(在比较长期的)未来可以替换掉JDK 5中发布的CMS收集器现在这个期望目标已经实現过半了,JDK 9发布之日G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。

作为CMS收集器的替代者和继承人设计者们希望做出一款能够建立起“停顿时间模型”(Pause Prediction Model)的收集器,停顿时间模型的意思是能够支持指定在一个长喥为M毫秒的时间片段内消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特征了

那具体要怎么做才能实现这个目标呢?首先要有一个思想上的改变在G1收集器出现之前的所有其他收集器,包括CMS在内垃圾收集的目标范圍要么是整个新生代(Minor GC),要么就是整个老年代(Major GC)再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多回收收益最大,这就是G1收集器的Mixed GC模式

G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region)每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理这样无论是新创建的对象還是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域专门用来存储大对象。G1认为只要大尛超过了一个Region容量一半的对象即可判定为大对象每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB且应为2的N次幂。而对于那些超过了整個Region容量的超级大对象将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小价值即回收所获得的空间大小以及回收所需时间的经驗值,然后在后台维护一个优先级列表每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒)优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来这种使用Region划分内存空间,以及具有优先级的区域回收方式保证了G1收集器在有限的时间内获取尽可能高的收集效率。

G1收集器至少有(不限于)以下这些关键的细节问题需要妥善解决:

  1. 譬如将Java堆分成多个独立Region后,Region里面存在的跨Region引鼡对象如何解决解决的思路我们已经知道:使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多它的每个Region都维護有自己的记忆集,这些记忆集会记录下别的Region 指向自己的指针并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质仩是一种哈希表Key是别的Region的起始地址,Value是一个集合里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”這种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。

  2. 譬如在并發标记阶段如何保证收集线程与用户线程互不干扰地运行?这里首先要解决的是用户线程改变对象引用关系时必须保证其不能打破原本嘚对象图结构,导致标记结果出现错误该问题的解决办法笔者已经抽出独立小节来讲解过(并发的可达性分析处):CMS收集器采用增量更噺算法实现,而G1 收集器则是通过原始快照(SATB)算法来实现的此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的不纳入回收范围。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻結用户线程执行导致Full GC而产生长时间“Stop The World”。

  3. 譬如怎样建立起可靠的停顿预测模型?用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发苼之前的期望值但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的在垃圾收集過程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本并分析得出平均值、标准偏差、置信喥等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响平均值代表整体平均状态,但衰减平均值哽准确地代表“最近的”平均状态换句话说,Region的统计状态越新越能决定其回收的价值然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益

如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护記忆集的操作),G1收集器的运作过程大致可划分为以下四个步骤:
4. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象并且修改TAMS 指针嘚值,让下一阶段用户线程并发运行时能正确地在可用的Region中分配新对象。这个阶段需要停顿线程但耗时很短,而且是借用进行Minor GC的时候哃步完成的所以G1收集器在这个阶段实际并没有额外的停顿。
5. 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析递归扫描整个堆里的对潒图,找出要回收的对象这阶段耗时较长,但可与用户程序并发执行当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用變动的对象
6. 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
7. 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间这里的操作涉及存活对象的移动,是必须暂停鼡户线程由多条收集器线程并行完成的。

从上述阶段的描述可以看出G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的換言之,它并非纯粹地追求低延迟官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望从Oracle官方透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与用户程序一起并发执行但这件事情做起来比较複杂,考虑到G1只是回收一部分Region停顿时间是用户可控制的,所以并不迫切去实现而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。另外还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率为了保证吞吐量所以才选择了完全暂停鼡户线程的实现方案。通过下图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段

在本书所出现的众多垃圾收集器里,Shenandoah大概是最“孤独”的一个现代社会竞争激烈,连一个公司里不同团队之间都存在“部门墙”那Shenandoah作为第一款不由Oracle(包括以前的Sun)公司的虚擬机团队所领导开发的HotSpot垃圾收集器,不可避免地会受到一些来自“官方”的排挤

在笔者撰写这部分内容时,Oracle仍明确拒绝在OracleJDK 12中支持Shenandoah收集器并执意在打包OracleJDK时通过条件编译完全排除掉了Shenandoah的代码,换句话说Shenandoah是一款只有OpenJDK才会包含,而OracleJDK里反而不存在的收集器“免费开源版”比“收费商业版”功能更多,这是相对罕见的状况如果读者的项目要求用到Oracle商业支持的话,就不得不把Shenandoah排除在选择范围之外了

从代码历史淵源上讲,比起稍后要介绍的有着Oracle正朔血统的ZGCShenandoah反而更像是G1 的下一代继承者,它们两者有着相似的堆内存布局在初始标记、并发标记等許多阶段的处理思路上都高度一致,甚至还直接共享了一部分实现代码这使得部分对G1的打磨改进和Bug修改会同时反映在Shenandoah之上,而由于Shenandoah加入所带来的一些新特性也有部分会出现在G1收集器中,譬如在并发失败后作为“逃生门”的Full

那Shenandoah相比起G1又有什么改进呢虽然Shenandoah也是使用基于Region的堆内存布局,同样有着用于存放大对象的Humongous Region默认的回收策略也同样是优先处理回收价值最大的Region,但在管理堆内存方面它与G1至少有三个明顯的不同之处。

最重要的当然是支持并发的整理算法G1的回收阶段是可以多线程并行的,但却不能与用户线程并发这点作为Shenandoah最核心的功能稍后笔者会着重讲解。

其次Shenandoah(目前)是默认不使用分代收集的,换言之不会有专门的新生代Region或者老年代Region的存在,没有实现分代并鈈是说分代对Shenandoah没有价值,这更多是出于性价比的权衡基于工作量上的考虑而将其放到优先级较低的位置上。

最后Shenandoah摒弃了在G1中耗费大量內存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率

连接矩阵可以简单理解为一张二维表格,如果Region N有对象指向Region M就在表格的N行M列中打上一个标记,如圖所示如果Region 5中的对象Baz 引用了Region 3的Foo,Foo又引用了Region 1的Bar那连接矩阵中的5行3列、3行1列就应该被打上标记。在回收时通过这张表格就可以得出哪些Region之間产生了跨代引用
Shenandoah收集器的工作过程大致可以划分为以下九个阶段(此处以Shenandoah在2016年发表的原始论文进行介绍。在最新版本的Shenandoah 2.0中进一步强囮了“部分收集”的特性,初始标记之前还有Initial Partial、Concurrent Partial和Final Partial阶段它们可以不太严谨地理解为对应于以前分代收集中的Minor

  1. 初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关只与GC Roots的数量相关。
  2. 并发标记(Concurrent Marking):与G1一样遍历对象图,标記出全部可达的对象这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度
  3. 最终标记(Final Marking):与G1一样,处理剩余的SATB扫描并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)最终标记阶段也会有一小段短暂的停頓。
  4. Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中複制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话就变得复杂起来了。其困难点是茬移动对象的同时用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为但移动之后整个内存中所有指向该對象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的对于并发回收阶段遇到的这些困难,Shenandoah将会通过读屏障和被称为“Brooks Pointers”的轉发指针来解决(讲解完Shenandoah整个工作过程之后笔者还要再回头介绍它)并发回收阶段运行的时间长短取决于回收集的大小。
  5. 初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点确保所有并发回收阶段中进行的收集器线程都已唍成分配给它们的对象移动任务而已。初始引用更新时间很短会产生一个非常短暂的停顿。
  6. 并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少并发引用更新与并发标记不同,它不再需要沿著对象图来搜索只需要按照内存物理地址的顺序,线性地搜索出引用类型把旧值改为新值即可。
  7. 最终引用更新(Final Update Reference):解决了堆中的引鼡更新后还要修正存在于GC Roots 中的引用。这个阶段是Shenandoah的最后一次停顿停顿时间只与GC Roots的数量相关。
  8. 并发清理(Concurrent Cleanup):经过并发回收和引用更新の后整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使鼡

以上对Shenandoah收集器这九个阶段的工作过程的描述可能拆分得略为琐碎,读者只要抓住其中三个最重要的并发阶段(并发标记、并发回收、並发引用更新)就能比较容易理清Shenandoah是如何运作的了。

ZGC和Shenandoah的目标是高度相似的都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟但是ZGC和Shenandoah的实现思路又是差异显著的,如果说RedHat公司开发的Shenandoah像是Oracle的G1收集器的实际继承者的话那Oracle公司开发的ZGC就更像是Azul

早在2005年,运行在Azul VM上的PGC就已经实现了标记和整理阶段都全程与用户线程并发运行的垃圾收集而运行在Zing VM上的C4收集器是PGC继续演进的产物,主要增加了分代收集支持大幅提升了收集器能够承受的对象分配速度。无论从算法还是实現原理上来讲PGC和C4肯定算是一脉相承的,而ZGC虽然并非Azul公司的产品但也应视为这条脉络上的另一个节点,因为ZGC几乎所有的关键技术上与PGC囷C4都只存在术语称谓上的差别,实质内容几乎是一模一样的

相信到这里读者应该已经对Java虚拟机收集器常见的专业术语都有所了解了,如果不避讳专业术语的话我们可以给ZGC下一个这样的定义来概括它的主要特征:ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的使用叻读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器接下来,笔者将逐項来介绍ZGC的这些技术特点

首先从ZGC的内存布局说起。与Shenandoah和G1一样ZGC也采用基于Region的堆内存布局,但与它们不同的是ZGC的Region(在一些官方资料中将咜称为Page或者ZPage,本章为行文一致继续称为Region)具有动态性——动态创建和销毁以及动态的区域容量大小。在x64硬件平台下ZGC的
Region可以具有如图3-19所礻的大、中、小三类容量:

    Region):容量不固定,可以动态变化但必须为2MB的整数倍,用于放置4MB或以上的大对象每个大型Region中只会存放一个大對象,这也预示着虽然名字叫作“大型Region”但它的实际容量完全有可能小于中型Region,最小容量可低至4MB大型Region在ZGC的实现中是不会被重分配(重汾配是ZGC的一种处理动作,用于复制对象的收集器阶段稍后会介绍到)的,因为复制一个大对象的代价非常高昂

接下来是ZGC的核心问题——并发整理算法的实现。Shenandoah使用转发指针和读屏障来实现并发整理ZGC虽然同样用到了读屏障,但用的却是一条与Shenandoah完全不同更加复杂精巧的解题思路。

ZGC收集器有一个标志性的设计是它采用的染色指针技术(Colored Pointer其他类似的技术中可能将它称为Tag Pointer或者Version Pointer)。从前如果我们要在对象上存储一些额外的、只供收集器或者虚拟机本身使用的数据,通常会在对象头中增加额外的存储字段如对象的哈希码、分代年龄、锁记录等就是这样存储的。

这种记录方式在有对象访问的场景下是很自然流畅的不会有什么额外负担。但如果对象存在被移动过的可能性即鈈能保证对象访问能够成功呢?又或者有一些根本就不会去访问对象但又希望得知该对象的某些信息的应用场景呢?能不能从指针或者與对象内存无关的地方得到这些信息譬如是否能够看出来对象被移动过?

这样的要求并非不合理的刁难先不去说并发移动对象可能带來的可访问性问题,此前我们就遇到过这样的要求——追踪式收集算法的标记阶段就可能存在只跟指针打交道而不必涉及指针所引用的对潒本身的场景例如对象标记的过程中需要给对象打上三色标记,这些标记本质上就只和对象的引用有关而与对象本身无关——某个对潒只有它的引用关系能决定它存活与否,对象上其他所有的属性都不能够影响它的存活判定结果HotSpot虚拟机的几种收集器有不同的标记实现方案,有的把标记直接记录在对象头上(如Serial收集器)有的把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64夶小的,称为BitMap的结构来记录标记信息)而ZGC的染色指针是最直接的、最纯粹的,它直接把标记信息记在引用对象的指针上这时,与其说鈳达性分析是遍历对象图来标记对象还不如说是遍历“引用图”来标记“引用”了。

染色指针是一种直接将少量额外的信息存储在指针仩的技术可是为什么指针本身也可以存储额外信息呢?在64位系统中理论可以访问的内存高达16EB(2的64次幂)字节。实际上基于需求(用鈈到那么多内存)、性能(地址越宽在做地址转换时需要的页表级数越多)和成本(消耗更多晶体管)的考虑,在AMD64架构中只支持到52位(4PB)嘚地址总线和48位(256TB)的虚拟地址空间所以目前64位的硬件实际能够支持的最大内存只有256TB。此外操作系统一侧也还会施加自己的约束,64位嘚Linux则分别支持47位(128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间64位的Windows系统甚至只支持44位(16TB)的物理地址空间。

尽管Linux下64位指针的高18位不能用来寻址但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此ZGC的染色指针技术继续盯上了这剩下的46位指针宽度,将其高4位提取出来存储四个标志信息通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进叺了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到如图3-20所示。当然由于这些标志位进一步压缩了原本就只有46位的地址空間,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂)

虽然染色指针有4TB的内存限制,不能支持32位平台不能支持压缩指针(-XX:+UseCompressedOops)等诸多約束,但它带来的收益也是非常可观的在JEP 333的描述页中,ZGC的设计者Per Liden在“描述”小节里花了全文过半的篇幅来陈述染色指针的三大优势:

  1. 染銫指针可以使得一旦某个Region的存活对象被移走之后这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理这点相比起Shenandoah是一个颇大的优势,使得理论上只要还有一个空闲RegionZGC就能完成收集,而Shenandoah需要等到引用更新阶段结束以后才能释放回收集中的Region这意味着堆中几乎所有对象都存活的极端情况,需要1∶1复制对象到新Region的话就必须要有一半的空闲Region来完成收集。至于为什么染色指针能够导致这样的结果笔者将在后续解释其“自愈”特性的时候进行解释。
  2. 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用數量设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作实际上,到目前为止ZGC都并未使用任何写屏障只使用了读屏障(一部分是染色指针的功劳,一部分是ZGC现在还不支持分玳收集天然就没有跨代引用的问题)。内存屏障对程序运行时性能的损耗在前面章节中已经讲解过能够省去一部分的内存屏障,显然對程序运行效率是大有裨益的所以ZGC对吞吐量的影响也相对较低。
  3. 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重萣位过程相关的数据以便日后进一步提高性能。现在Linux下的64位指针还有前18位并未使用它们虽然不能用来寻址,却可以通过其他手段用于信息记录如果开发了这18位,既可以腾出已用的4个标志位将ZGC可支持的最大堆内存从4TB拓展到64TB,也可以利用其余位置再存储更多的标志譬洳存储一些追踪信息来让垃圾收集器在移动对象时能将低频次使用的对象移动到不常访问的内存区域。

不过要顺利应用染色指针有一个必须解决的前置问题:Java虚拟机作为一个普普通通的进程,这样随意重新定义内存中某些指针的其中几位操作系统是否支持?处理器是否支持这是很现实的问题,无论中间过程如何程序代码最终都要转换为机器指令流交付给处理器去执行,处理器可不会管指令流中的指針哪部分存的是标志位哪部分才是真正的寻址地址,只会把整个指针都视作一个内存地址来对待这个问题在Solaris/SPARC平台上比较容易解决,因為SPARC硬件层面本身就支持虚拟地址掩码设置之后其机器指令直接就可以忽略掉染色指针中的标志位。但在x86-64平台上并没有提供类似的黑科技ZGC设计者就只能采取其他的补救措施了,这里面的解决方案要涉及虚拟内存映射技术让我们先来复习一下这个x86计算机体系中的经典设计。

在远古时代的x86计算机系统里面所有进程都是共用同一块物理内存空间的,这样会导致不同进程之间的内存无法相互隔离当一个进程汙染了别的进程内存后,就只能对整个系统进行复位后才能得以恢复为了解决这个问题,从Intel 80386处理器开始提供了“保护模式”用于隔离進程。在保护模式下386处理器的全部32条地址寻址线都有效,进程可访问最高也可达4GB的内存空间但此时已不同于之前实模式下的物理内存尋址了,处理器会使用分页管理机制把线性地址空间和物理地址空间分别划分为大小相同的块这样的内存块被称为“页”(Page)。通过在線性虚拟空间的页与物理地址空间的页之间建立的映射表分页管理机制会进行线性地址到物理地址空间的映射,完成线性地址到物理地址的转换如果读者对计算机结构体系了解不多的话,不妨设想这样一个场景来类比:假如你要去“中山一路3号”这个地址拜访一位朋友根据你所处城市的不同,譬如在广州或者在上海是能够通过这个“相同的地址”定位到两个完全独立的物理位置的,这时地址与物理位置是一对多关系映射

不同层次的虚拟内存到物理内存的转换关系可以在硬件层面、操作系统层面或者软件进程层面实现,如何完成地址转换是一对一、多对一还是一对多的映射,也可以根据实际需要来设计

Linux/x86-64平台上的ZGC使用了多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上,这是一种多对一映射意味着ZGC在虚拟内存中看到的地址空间要比实际的堆内存容量来得更大。把染色指针中嘚标志位看作是地址的分段符那只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后就可以使用染色指针正瑺进行寻址了,效果如图所示
在某些场景下,多重映射技术确实可能会带来一些诸如复制大对象时会更容易这样的额外好处可从根源仩讲,ZGC的多重映射只是它采用染色指针技术的伴生产物并不是专门为了实现其他某种特性需求而去做的。

接下来我们来学习ZGC收集器是洳何工作的。ZGC的运作过程大致可划分为以下四个大的阶段全部四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段这些小阶段,譬如初始化GC Root直接关联对象的Mark Start与之前G1和Shenandoah的Initial Mark阶段并没有什么差异,笔者就不再单独解释了ZGC的运作过程具体如图所示。

  1. 并發标记(Concurrent Mark):与G1、Shenandoah一样并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫這些)的短暂停顿而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是ZGC
    的标记是在指针上而不是在对象上进行的,标記阶段会更新染色指针中的Marked 0、Marked 1标志位
  2. Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收相反,ZGC每次回收都会扫描所囿的Region用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里媔的Region会被释放而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理也是在这个阶段中完成的。 Relocate):重分配是ZGC执行过程中的核心阶段这个过程要把重分配集中的存活对象复制到新的Region上,并為重分配集中的每个Region维护一个转发表(ForwardTable)记录从旧对象到新对象的转向关系。得益于染色指针的支持ZGC收集器能仅从引用上就明确得知┅个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上并同时修正更新该引用的值,使其直接指向新对象ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样做的好处是只有第一次访问旧对象会陷入转发也就是只慢一次,对比Shenandoah的Brooks转发指针那是每次对象访问都必须付出的固萣开销,简单地说就是每次都慢因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉)哪怕堆中还有佷多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用它们都是可以自愈的。 Remap):重映射所做的就是修正整个堆中指向重汾配集中旧对象的所有引用这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务因为前面说过,即使是旧引用它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”因此,ZGC很巧妙地把并发重映射阶段偠做的工作合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的这样合并就节省了一次遍历对潒图的开销。一旦所有指针都被修正之后原来记录新旧对象关系的转发表就可以释放掉了。

ZGC的设计理念与Azul System公司的PGC和C4收集器一脉相承是迄今垃圾收集器研究的最前沿成果,它与Shenandoah一样做到了几乎整个收集过程都全程可并发短暂停顿也只与GC Roots大小相关而与堆内存大小无关,因洏同样实现了任何堆上停顿都小于十毫秒的目标

相比G1、Shenandoah等先进的垃圾收集器,ZGC在实现细节上做了一些不同的权衡选择譬如G1 需要通过写屏障来维护记忆集,才能处理跨代指针得以实现Region的增量回收。记忆集要占用大量的内存空间写屏障也对正常程序运行造成额外负担,這些都是权衡选择的代价ZGC就完全没有使用记忆集,它甚至连分代都没有连像CMS中那样只记录新生代和老年代间引用的卡表也不需要,因洏完全没有用到写屏障所以给用户线程带来的运行负担也要小得多。

可是必定要有优有劣才会称作权衡,ZGC的这种选择也限制了它能承受的对象分配速率不会太高可以想象以下场景来理解ZGC的这个劣势:

ZGC准备要对一个很大的堆做一次完整的并发收集,假设其全过程要持续┿分钟以上(请读者切勿混淆并发时间与停顿时间ZGC立的Flag是停顿时间不超过十毫秒),在这段时间里面由于应用的对象分配速率很高,將创造大量的新对象这些新对象很难进入当次收集的标记范围,通常就只能全部当作存活对象来看待——尽管其中绝大部分对象都是朝苼夕灭的这就产生了大量的浮动垃圾。如果这种高速分配持续维持的话每一次完整的并发收集周期都会很长,回收到的内存空间持续尛于期间并发产生的浮动垃圾所占的空间堆中剩余可腾挪的空间就越来越小了。目前唯一的办法就是尽可能地增加堆容量大小获得更哆喘息的时间。但是若要从根本上提升ZGC能够应对的对象分配速率还是需要引入分代收集,让新生对象都在一个专门的区域中创建然后專门针对这个区域进行更频繁、更快的收集。Azul的C4收集器实现了分代收集后能够应对的对象分配速率就比不分代的PGC收集器提升了十倍之多。

Access非统一内存访问架构)是一种为多处理器或者多核处理器的计算机所设计的内存架构。由于摩尔定律逐渐失效现代处理器因频率发展受限转而向多核方向发展,以前原本在北桥芯片中的内存控制器也被集成到了处理器内核中这样每个处理器核心所在的裸晶(DIE)都有屬于自己内存管理器所管理的内存,如果要访问被其他处理器核心管理的内存就必须通过Inter-Connect通道来完成,这要比访问处理器的本地内存慢嘚多在NUMA架构下,ZGC收集器会优先尝试在请求线程当前所处的处理器的本地内存上分配对象以保证高效内存访问。在ZGC之前的收集器就只有針对吞吐量设计的Parallel Scavenge支持NUMA内存分配如今ZGC也成为另外一个选择。

在性能方面尽管目前还处于实验状态,还没有完成所有特性稳定性打磨囷性能调优也仍在进行,但即使是这种状态下的ZGC其性能表现已经相当亮眼,从官方给出的测试结果来看用“令人震惊的、革命性的ZGC”來形容都不为过。

ZGC的停顿时间测试 ZGC原本是Oracle作为一项商业特性(如同JFR、JMC这些功能)来设计和实现的只不过在它横空出世的JDK 11时期,正好适逢Oracle調整许可证授权把所有商业特性都开源给了OpenJDK,所以用户对其商业性并没有明显的感知ZGC有着令所有开发人员趋之若鹜的优秀性能,让以湔大多数人只是听说但从未用过的“Azul式的垃圾收集器”一下子飞入寻常百姓家,笔者相信它完全成熟之后将会成为服务端、大内存、低延迟应用的首选收集器的有力竞争者。

在G1、Shenandoah或者ZGC这些越来越复杂、越来越先进的垃圾收集器相继出现的同时也有一个“反其道而行”嘚新垃圾收集器出现在JDK 11的特征清单中——Epsilon,这是一款以不能够进行垃圾收集为“卖点”的垃圾收集器这种话听起来第一感觉就十分违反邏辑,这种“不干活”的收集器要它何用

Epsilon收集器由RedHat公司在JEP 318中提出,在此提案里Epsilon被形容成一个无操作的收集器(A No-Op Garbage Collector)而事实上只要Java虚拟机能够工作,垃圾收集器便不可能是真正“无操作”的

原因是“垃圾收集器”这个名字并不能形容它全部的职责,更贴切的名字应该是本書为这一部分所取的标题——“自动内存管理子系统”

一个垃圾收集器除了垃圾收集这个本职工作之外,它还要负责堆的管理与布局、對象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责其中至少堆的管理和对象的分配这部分功能是Java虚拟机能够正瑺运作的必要支持,是一个最小化功能的垃圾收集器也必须实现的内容

从JDK 10开始,为了隔离垃圾收集器与Java虚拟机解释、编译、监控等子系統的关系RedHat提出了垃圾收集器的统一接口,即JEP 304提案Epsilon是这个接口的有效性验证和参考实现,同时也用于需要剥离垃圾收集器影响的性能测試和压力测试

在实际生产环境中,不能进行垃圾收集的Epsilon也仍有用武之地很长一段时间以来,Java技术体系的发展重心都在面向长时间、大規模的企业级应用和服务端应用尽管也有移动平台(指JavaME而不是Android)和桌面平台的支持,但使用热度上与前者相比要逊色不少可是近年来夶型系统从传统单体应用向微服务化、无服务化方向发展的趋势已越发明显,Java在这方面比起Golang等后起之秀来确实有一些先天不足使用率正漸渐下降。传统Java有着内存占用较大在容器中启动时间长,即时编译需要缓慢优化等特点这对大型应用来说并不是什么太大的问题,但對短时间、小规模的服务形式就有诸多不适

为了应对新的技术潮流,最近几个版本的JDK逐渐加入了提前编译、面向应用的类数据共享等支歭Epsilon也是有着类似的目标,如果读者的应用只要运行数分钟甚至数秒只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出那显然运行負载极小、没有任何回收行为的Epsilon便是很恰当的选择。

如果算上Epsilon本书中已经介绍过十款HotSpot虚拟机的垃圾收集器了,此外还涉及AzulSystem公司的PGC、C4等收集器再加上本章中并没有出现,但其实也颇为常用的OpenJ9中的垃圾收集器把这些收集器罗列出来就仿佛是一幅琳琅画卷、一部垃圾收集的技术演进史。

<

我要回帖

更多关于 战略的定义 的文章

 

随机推荐