2025-02-21 11:26:53
前面一篇文章讲的是面试前的准备,我从简历、技术知识、算法题和工作项目四个方面一一分享了该如何做准备,以及其中的经验和技巧。今天我们就来聊聊面试中的技巧。
面试过程很短,对一个人的认识和了解也是很有限的。如果你的技能一般的话,那么就需要加强你的形象和谈吐了。总之,你不能内在和外在都不要吧,最好是内在和外在都很好。
形象方面,最好还是穿工作便装,休闲的也没事,但是要让人感到干净、整洁。不要有异味,不要邋遢——头不梳、胡子不刮、衣服也皱巴巴的,还是要修修边幅的。因为有 HR 的人会来面你的,HR 一般都是女孩子,所以不要吓到她们。
另外,保持微笑,表现得热情、开朗和幽默是非常重要的。每个人都喜欢和开朗风趣积极向上的人相处。经常微笑,表现出自己的热情,适当开开玩笑,自嘲一下,会让人觉得你很容易亲近。交谈时千万不要像挤牙膏一样,别人问你一句,你答一句,要把完整的前因后果讲完。别人问你个事,你就多分享一些这个事中的酸甜苦辣,把故事讲得生动有趣点儿,能逗笑 HR 妹子最好(但不要撩)。
说话的时候,要看着对方,一方面这是对对方的尊重和礼貌,另一方面,这也是一种自信。就算没有面好,也不要低着头,又不是做错了什么事。有什么事说不清楚的,不要犹豫,该画图画图。对于比较复杂的面试官听不懂的问题,要变换不同的方式来描述。
面试官问的问题,你要给出充足的细节,千万不要让面试官不断地追问,那样就被动了。你问我解决过的最难的问题是什么,我就把这个问题的来龙去脉和其中的各种细节给你滔滔不绝地讲个遍。当然,也要讲得清楚干净有条理,不要东拉西扯的,也不要云山雾罩的。这些表达和谈吐还是要多练!
最好的训练就写作,你写得多了,能把复杂的问题描述清楚了,自然也会体现在语言组织能力上了。
面试中有一些问题很难,但是不要放弃,要不断尝试。很多时候,面试官并不期待你能在很短的时候内解出一道难题,他只是想看一下你遇到难题时的态度和思维方式。如果你能证明给面试官看,你解决问题的方向和方法是正确的,就算是没有找到答案,也是很不错的。因为只要方向走对了,剩下的就是时间问题了。
如果实在解不出来,或是被问了不懂的知识性问题,那么就直接说不懂就好了。记下来,回去多看多练,下次记住了就好。
另外,对于没有答上来的问题,有的人会在面试后请教一下面试官。但是我觉得更好的方式是,问面试官要个他的邮箱或微信,回去后,努力搞懂,举一反三,然后写个东西再发回去。这样做是有可能让你起死回生的哦。多少可以暗示对方:“你看,我有不懂的,但是我能下工夫很快就搞懂了,你看我的学习能力还不错哦。你就不再考虑一下了吗?”
应聘的时候,你有可能会被问到几个尖锐的问题,这时你要小心做答。一般来说,你会遇到这几个常见的比较尖锐的问题。
你为什么要离开现在的公司?这种问题一般都是来问你的离职动机的,招聘方有理由相信,你是怎样离开前东家的,就会怎样离开我。另外,从这个问题上,招聘方想了解你真实的动机,对工作的想法和个人的喜好。一般来说,永远不要说之前公司的坏话,最标准的外交词令是:“我离开现有公司的原因是我太喜欢你们公司了”。
这样的回答,对于招聘方来说毫无破绽可言,而如果你开始抱怨你现在的公司了,很可能会引出很多问题把你问到最后都抬不起头来。当然,你也可以说前公司的问题,比如:自己心爱的项目被公司废弃了、公司转型了、公司业绩下滑了、在现有的公司没有成长空间了…… 这些都还是可以说的。
说一下你的缺点?并给出几个例子。这个问题也是很难很难回答的。但是,我想说,人无完人,是个人总是会有缺点的,但是有的缺点也有点不好意思说。所以,这个问题是比较难的。这个问题不能说套话,说套话会显得特别假。这个问题还是要说实话,也不能说一些不痛不痒的小毛病,这样会让他觉得你避重就轻。
只要你认识到任何性格的人都有问题,那么这个问题你就好回答了。比如,对我来说,我个是比较着急的人,急性子,而且是个做事的人。所以,我最大的问题就是在推进一些事的时候,会忽略别人的感受。当压力变大的时候,我甚至会说出一些别人难以接受的话(俗话说的情商为零)。这个没什么不好意思承认的,我这么多年来也在改进自己。
总之,我想说的是,我们每个人都应审视一下自己,思考一下自己光明面的后面。而回答这个问题的最佳方法,就是想想附着在正面事件上的阴暗面,那就是你的答案。比如,我对事情的要求太高了,跟我在一起工作人的压力太大。我太内向了,所以别人和我沟通起来有点费劲。我太过关心团队了,所以,有时候会忽略了项目成本和时间进度……
最后还要补一句,我知道我的缺点,我也在努力改正,我正在通过什么样的方式改正。这非常关键,因为这基本上是面试官最喜欢看到的答案了,就是你不仅能正视自己的缺点,而且还能不断地改正。
另外,与这个问题相对应的是,说一下你的优点。这个问题是比较坑的,你的优点是需要用证据来说明的。比如,我通常的回答是,我的优点就是学习能力强,因为我掌握的技术面很广,而且,我什么样的技术都学,比如最新的 Cloud Native 技术。作为后端人员我还学前端方面的技术如 React.js 和 Vue.js,这一切都来源于我扎实的基础知识……
回答这个问题的时候,一般都会反衬出你的价值观,HR 就是想了解你的价值观。比如,我比较踏实,我想把技术一直做到老。再比如,我有韧性,我受过哪些挫折、失败、不公、无奈和无助,我没有当逃兵……
你为什么换工作换得这么勤?很多公司的 HR 都会对应聘者频繁换工作持比较负面的评价。频繁换工作在职业生涯中真不是一件好事,因为用人方会觉得要么是你太不踏实了,要么是你太不行了。所以,工作不要换得太频繁。但是如果换得太频繁了,我给你一个建议,在简历里面写上离职原因。
另外,在面试时被问到这个问题时,你需要给出合理的解释,以消除用人方的疑惑。怎么给出合理的解释呢?一方面,你还是需要诚恳一点儿,另一方面,你在解释时需要承认频繁换工作也不是自己想的,自己何尝不想在一份工作上干得时间长一点儿。
无奈,要么是公司有变化,要么就是自己没选好。一方面表达自己也厌倦了频繁换工作这种事,另一方面,你要把这个话题引到另外一个方向上——什么样的工作自己可以干很久?自己所期望的工作内容和工作环境是什么样的?这样就转而去谈你所向往的工作内容和环境了,并再表达一下在这样的工作环境下,是可以很长时间做下去的,并愿意和公司一起发展。
但是,先不要说得太理想了,不然,用人方也会觉得自己是做不到的。正确的说法是,自己并不担心公司有各种各样的问题,只要有一起扛事的队友一起拼搏,这才是最关键的。
你在一家公司呆了接近 10 年为什么没有做到管理层?你又是怎么保持竞争力的?一般来说,不想做管理的程序员也挺多的,在技术的方向上勤勤恳恳深耕细作,会是一个非常难得的优秀工程师。专注于技术,不分心,不断地在技术上的深度和广度上钻研,这就是保持竞争力最好的方式。所以,其实这个问题挺好回答的。
但另一个更难的问题是:你工作满 5 年了,为什么还不是一个高级程序员?对于国外的顶尖公司来说,如果你有 5 年的工作经验,但还不能胜任高级程序员(Amazon 的 SDE2)的职位,那么你这个人就基本会被 pass 掉了,包括在职的员工也是一样的。于是,对于工作年限超过 5 年的程序员,如果你还不能证明你可以独当一面,你的能力能够驾驭复杂难题,那么国外的顶尖公司都不会问你这个问题的。
国内的公司可能会问你这个问题,对此,我个人认为比较好的回答是要分几方面来谈。一方面,过去因为什么原因耽误了些时间(环境因素、客观条件因素),另一方面,要表示同样也有主观因素,不然显得有点找借口的感觉,不诚恳。
接下来,要表明自己心里面也比较慌(表明自己不用别人提醒可以自己意识到自己的问题),所以,近一年来一直在学习,罗列一下学过哪些东西,最好还有学习目标和学习计划(表明自己除了有意识外,还有行动)。当然,厉害的面试官会不断地追问你一些细节,以此来确定你没有说假话,对此,你要有充足的准备。
你为什么换了一个方向?你觉得你有什么优势? 这个问题其实并不难回答,实话实说就好了。但是不要让招聘方感受到你浮燥的内心,或是朝三暮四的性格,更不要让人感觉到你像 “ 小猫钓鱼 ” 那样一边不行又来搞另一边。
我觉得回答这个问题有两种方式:一种是非常自信的回答——” 我从来没有改变我的方向,因为种种原因,我没能得到我想要的方向,虽然现在很残酷,但是我一直都没有放弃我的方向,我一直都在努力学习……” 如果你要这么回答了,你就要真的是这样的,在新的方向有所研究和建树,不然会被识破的。
另一种回答则常规一点,首先说明一下,自己的兴趣爱好,为什么这个方向要比之前的那个方向更适合自己。可以用几个例子来说明,但其中要有一些细节,比如,自己试过这个新方向 ,发现干得比原来那边更好,更容易出成绩,自己的兴奋点更大,所以觉得新方向更适合自己。然后,承认换一个方向短期内并没有优势,但是,因为自己的某某特质,比如,学习能力强、勤奋、聪明等特质,未来一定是可以胜任的。
但是,你要用证据证明你的学习能力强,你比一般人勤奋,比一般人聪明。不然如果对方追问下去,会让你破绽百出的。总之,回答这样的问题,需要一定的证据作为补充,而且还要伴随着以降职降薪为代价。所以,一般来说,选定方向最好不要再变了,如果一定要变的话,你也要有必胜的信心和先下后上的心态,而且这些信心和心态要让招聘方看到。
对于技术的热情或初心体现在你生活和工作中的哪里?这个问题其实是想了解一下你的性格,以及对生活和工作的态度。这个问题会伴随着很多细节上的追问。所以,你要小心回答,而且是要带感情的,但一定要是真实的。
一般来说,热情和初心不是停留在嘴上的,而是要表现在行动上的,你需要给出几个曾经发生过的示例。这些示例可以是:你死磕某个事解决某个难题不认输的精神;你坚持做某件事,无论风吹雨打,无论有没有激励;你在某个逆境中依然没有放弃依然努力的态度;在面对压力时,你勇于承担责任的精神;你严谨细心、精益求精的做事风格;面对诱惑能沉得住气,不浮躁……
总结一下,对技术的热情或初心,需要表现在这么几个特质上:执着、坚持、坚韧、不服输、担当、不妥协、不浮燥……我说一句,我相信每个人或多或少都会有这些特质,这是你的亮点,要小心呵护。不然,你跟一条咸鱼就没什么两样了。
你觉得你比男性程序员有什么优势?这种问题一看就带有性别歧视。我的建议是,首先从更高的维度教育一下对方,放出观点,性别不能算优势,人与人的不同和差距是体现在工作技能和态度上的。然后,把回答转向到自己的工作技能和工作态度上来,随后从诸如想象力、品味、沟通能力、严谨细心、承受压力等方面说明自己的长处。
当然,能问得出这样问题的公司一定不是好公司,千万不要去了。所以,可以放心地怼回去。需要注意的是,职场中的怼人是要用数据和事实打脸的。
比如:世界上第一个程序员就是女的叫 Ada,她不仅预言了通用计算机的可能,还发明了世界上第一个计算机程序。世界上第一台通用计算机 ENIAC 的编译和部署工作是由 6 位女程序员组成的团队完成的。把阿波罗送到月球的程序员也是女的,叫 Margaret Hamilton。微软 Halo 游戏引擎的主程也是女的,还是中国香港人,叫余国荔……另外,在中国的运动比赛上,女性运动员比男性运动员的成绩要好……
在各个公司,我看到更多的男性除了在使蛮力和搬砖上比女性要强,也没什么其他长项。如果认为写程序是劳动密集型的工种,当然是男性比女性好用。对了,你们这里是劳动密集型的公司吗?最后,我认为,就对女性尊重方面还是国外公司做得好。所以,建议女程序员还是要去国外公司工作。
最后,我想说一下,回答尖锐问题你会有两种方法,一般是比较官方的,像外交或是政治词令,另一种是比较诚恳的、真实的。虽然两者都可以,但是我觉得后者更好一些。因为那是能打动人的。对于一些不礼貌的问题,我觉得你要站在更高的维度教育他们,这样才会显得他们的 low。
一般来说,面试结束的时候,都会问你有没有什么问题。不要放弃这个机会。
如果你面得比较好,这个时候可以问几个尖锐的问题,这样有利于后面谈 offer 和岗位(抓住机会反转被动为主动)。比如,我就问过国外某一线公司的面试官下面两组问题:
你们公司有多少一线开发经理还在写代码?你们的一线经理都没有时间来写代码了,不知道细节怎么做好管理?另外是不是说明你们公司有大量的内耗?
任何公司都有好的有不好的,你能不能分享一下你最喜欢这个公司的地方和最不喜欢的地方?
基本上来说,面试官都会被我问住,然后开始语塞。能让说英语母语的老外在我这个英文一般的人面前说不清话,我还是很满足的。哈哈哈。当然,也不一定是非要像我这么尖锐地问问题,你也可以设计几个柔和一点儿的问题。总之,问这样问题的目的是,暗示一下对方,我来不来还不一定呢,也别想压低我的 offer,你们公司也不是什么都好,要想让我来,得再加点……(嘿嘿嘿)
如果你面得一般,这个时候你也可以问些加分的问题。比如:目前贵公司或是贵团队最需要解决什么样的问题?我能帮贵公司做些什么?能不能给我一些资料我先了解一下,这样我后面如果能进来,就能上手更快一些了。因为你面得一般的话,面试官会比较犹豫和纠结,此时你需要让面试官不要犹豫,所以,你可以表现得更加热情和主动一点。你看,竟然一副通过面试明天就要上班的 “ 无耻嘴脸 ” 也会为你加点分的……(哈哈哈)
如果你面得很不行,基本挂掉了。这个时候,也要问问题。但最好问一下面试官对你的评价,并且让他指出你的不足和需要改进的地方。面试本来就是一次经历和一次学习,你也可以把其当作是一种受教育的过程。所以,不要放过自己可以成长的机会。通过面试官给你的评价,你日后就知道自己需要努力的地方和方向了。这是多好的一件事儿啊。
总结一下今天的内容。我认为,形象和谈吐对于面试成功与否非常重要。着装方面一定要大方得体,干净整洁;谈吐方面一定要自信从容,能够清楚准确地表达自己的观点和想法。随后是如何面对一些答不上来的问题,如何回答尖锐问题,以及在面试结束之后,如何提问,为自己争取福利或者机会。
来源:《左耳听风专栏:程序员面试攻略》
2025-02-18 10:57:48
学习了《程序员练级攻略》以后,我觉得你应该来学习一下“如何面试”了。在我的职业生涯中,我应聘过很多公司,小公司、中型公司、大公司、国内的公司、国外的公司都有。我有成功获得 offer 的经历,也有过不少失败的经历。
我从 2007 年做管理以来,面试过至少 1000 多人次的工程师。这十多年来,我发现有一些事情没什么变化,我们一代又一代的年轻人在应聘时的表现和我 20 年前没什么两样,连简历都没什么改进,更不要说程序员在表达能力方面的长进了。如果只看面试表现的话,感觉世界停止了 20 年似的。
我一直在想,为什么应聘、与人沟通、赚钱等这些重要的软技能,学校里不教呢?这么重要的技能居然要你自己去学,不得不说是教育上的一种失败。另外,关于如何应聘的事,估计你也看过一些文章了,我这里只分享一些我的实实在在的经验和相关的技巧。一定你和看过的不太一样。相信一定能帮得到你!
应聘是需要准备的,下面这些点你需要好好地准备一下。如果你没有准备的话,那么被 pass 掉的概率会非常大。
首先你要准备的是简历。简历很重要,这是别人了解你的第一个地方,所以简历要好好写。当然,我们知道真正的好简历是要用自己的经历去写的,比如,有人的简历就是一句话:我发明了 Unix。
当然,并不是所有的人都有这样的经历,但这依然告诉我们,自己的经历才是简历最大的亮点。所以,你要去那些能让你的简历有更多含金量的公司工作,要做那些能让你的简历更闪亮的工作。这是写简历的最佳实践——用自己的经历聊,而不是用文字写。
但从另一方面来说,简历这个文本也是要好好写的,况且,我们不是每个人都会有很耀眼的经历,所以,还是要好好写简历。基本上来说,简历上的信息不要写太多,信息太多相当于没有信息,不要单纯地罗列,要突出自己的长处和技能。一般来说,简历需要包括以下几项内容。
自我简介。这个自我简介是用最简单的话来说明自己的情况,不超过 200 字。比如:10+ 年的软件开发经验(说明你的主业),4+ 年的团队 leader 经验(说明你的领导力),擅长高可用高性能的分布式架构(说明你的专业和专攻),多年互联网和金融行业背景(说明你的行业背景),任职于 XXX 公司的 XX 职位(说明你的职业),负责 XXX 平台或系统(说明你的业务场景)……
个人信息。这里有几点需要注意。
基本信息。电子邮箱建议用 Gmail,千万不要用 QQ 邮箱,要让人感觉职业化一些。
个人网站。如果你有个人主页、博客、GitHub 或是 Stack Overflow,请一定附上,这是加分项。如果个人主页或博客有独立域名,那更好,这会给人一种你爱动手做事的感觉。页面也要干净有美感,这样会让人感觉你有品味。
网站内容。一般来说这些项都会被面试官点看浏览,所以,里面的内容你需要小心组织和呈现,千万不要造假。另外,除了技术上的一些知识总结(不要太初级,要有深度的、原理型的、刨根问底型的文章),你也可以秀一秀自己的技术价值观(比如,对代码整洁的追求,对一些技术热点事件的看法),这会让你更容易获得面试官的好感。面试官的好感很重要。
作品展。如果你有一些作品展现,会更好。当然,对于前端程序员来说,这是比较容易的。而对于后端程序员来说,这会比较难一些,只能展示一下自己的 GitHub 了。如果你有一些比较不错的证书或奖项(如微软的认证、Oracle 的认证),也可以展示一下。
个人技能。个人信息下面你应该罗列几条个人的技能。这些内容要能很明显地让对方了解你掌握的技术和熟悉的领域。
技术技能栈。其中包括你擅长和会用的编程语言(如 Java、Go、Python 等),编程框架或一些重要的库(如 Spring Boot、Netty、React.js、gRPC 等),熟悉的一些技术软件(如 Redis、Kafka、Docker 等),设计或架构(如面向对象设计、分布式系统架构、异步编程、高性能调优等)。
技术领域。前端、算法、机器学习、分布式、底层、数据库等。
业务领域。一方面是行业领域,如金融、电商、电信等,另一方面是业务领域,如 CRM、支付、物流、商品等。
经验和软技能。带过多少人的团队、有多少年的项目管理经验、学习能力如何、执行力怎么样、设计过什么样的系统。(不要太多,几句话就好)
其实和用人单位发布的招聘信息中的职位技能需求很相似。有时候我都在想,明明用人单位的职位需求里写成那样,为什么应聘人还不依葫芦画瓢呢?所以,对应于你的简历,如果能和职位需求看齐有相类似的描述,这样可以快速地让人觉得你和要应聘的职位很匹配。
工作经历和教育经历
列一下你的工作经历。每份工作完成的主要项目(不要列一大堆项目,挑重要的),主要突出项目的难度、规模、挑战、职责,以及获得的认可和荣誉。
工作经历和教育经历,主要是对上述的个人技能的印证。不要东拉西扯,要紧紧地围绕着你的技能、特长和亮点来展开。
一般来说,你简历中的内容最好控制在两页 A4 纸以内,最好有中英文版,简历不要是 Word 版的,最好是 PDF 版,然后简历的格式和风格请参考 LinkedIn 上的(在 微软的 Office 模板网站 上也能找到一些不错的简历模板)。简历的内容不要太多,内容太多,重点就不明显了。写简历的目的是呈现自己的特长、亮点和特点。只要你能呈现出 2-3 个亮点和特长,就可以吸引到人了。
简历只是一块敲门砖。一些热门的公司和项目能够吸引到很多很多人的简历,所以,你要在众多的简历中脱颖而出。除了自己的经历和能力有亮点外,你还需要有吸引用人单位的方法。
有很多公司都是 HR 先来筛一遍简历,HR 其实并不懂技术,她们只会看你的过往经历、能力是否和职位描述上的匹配。如果简历上的经历和技术亮点不足的话,那么你可以在简历的版式和形式的制作上花些心思,以及在简历的自我描述中加上一些“虚”的东西。
比如“工作态度积极,不分份内和份外的事,只要对公司和个人有利,都会努力做好;勤奋踏实,热爱学习,喜欢做一个全栈工程师;善于发现问题,并解决问题……”表示我虽然现在的经历和技能不足以打动你,但是我的态度端正,潜力巨大,你不能错过……
一般来说,你的简历上写什么,面试官就会问什么,所以,不要打自己的脸,精通就是精通,熟悉就是熟悉,了解就是了解。然后对于你列出来的这些技术,你一定要把其最基本的技术细节给掌握了。面试官一般也会逐步加大问题的难度和深度,看看你到底在哪个层次上。所以,你还是需要系统地看看书,才能应对面试官的问题。比如:
你写上了 Java,那么 Java 的基本语法都要了解,并发编程、NIO、JVM 等,你多少要有点儿了解,Spring、Netty 这些框架也要了解。
你写上了 Go,那么至少得把官网上的 Effective Go 给看了。
你写上了 Redis,那么除了 Redis 的数据结构,Redis 的性能优化、高可用配置、分布式锁什么的,你多少也要把官网上的那几篇文章读一读。
你写上了面向对象,那么怎么着也得把《设计模式》中的 23 个模式了解一下。
你写上了分布式架构,那么 CAP 理论、微服务架构、弹力设计、Spring Cloud、Cloud Native 这些架构就要做到心里有数。
你写上网络编程,那么 TCP/IP 的三次握手,四次挥手,两端的状态变化你得知道吧,Socket 编程的那几个系统调用,还有 select、poll、epoll 这些异步 IO 多路复用的东西,你得知道。
总之,无论你在简历里写什么技术,这些技术的基础知识你都得学一下。本质上来说,这跟考试一样啊。你想想你是怎样准备期末考试的,是不是得把教科书上所有章节中的关键知识点都过一下?你不见得要记住所有的知识点,但是 80% 以上的关键知识点,你多少得知道吧。
国外的公司一般还会面算法题,他们用算法题来过滤掉那些非计算机专业出身的人。国内的一些公司也一样,尤其是一些校招面试,也有很多算法题。所以,算法是很重要的,是你需要努力学习和准备的。
LeetCode 是一个不错的地方。如果你能完成其中 50% 的题,那么你基本上可以想面哪里就面哪里了。这里,你要知道,一些面试官也是新手,他们也是从网上找一些算法题来考你。所以,你不用太害怕算法题,都是有套路的。比如:
如果是数据排序方面的题,那基本上是和二分查找有关系的。
如果是在一个无序数组上的搜索或者统计,基本上来说需要动用 O(1) 时间复杂度的 hash 数据结构。
在一堆无序的数据中找 top n 的算法,基本上来说,就是使用最大堆或是最小堆的数据结构。
如果是穷举答案相关的题(如八皇后、二叉树等),基本上来说,需要使用深度优先、广度优先或是回溯等递归的思路。
动态规划要重点准备一下,这样的题很多,如最大和子数组、买卖股票、背包问题、爬楼梯、改字符……这里有一个 Top 20 的动态规划题的列表 。
一些经典的数据结构算法也要看一下,比如,二叉树、链表和数组上的经典算法,LRU 算法,Trie 树,字符串子串匹配,回文等,这些常见的题都是经常会被考到的。
基本上来说,算法题主要是考察应聘者是否是计算机专业出身的,对于基本的数据结构和算法有没有相应的认识。你做得多了,就是能感觉得到其中的套路和方法的。所以,本质来说,还是要多练多做。
无论什么公司的面试,都会让你说一个你做过的项目,或是你过去解决过的一个难题。但我很好奇怪,这种必问的题,为什么很多应聘者都没有好好准备一下。
一般来说,会有下面这样的几个经典的面试问题。
说一个你做过的最自豪的项目,或是最近做过的一个项目。
说一个你解决过的最难的技术问题,或是最有技术含量的问题。
说一个你最痛苦的项目,或最艰难的项目。
说一个犯过的最大的技术错误,或是引发的技术故障。
对于上面这四个问题:第一个问题,主要是想看看你过去工作中做过的最高级的事是什么,还有你的兴趣点和兴奋点是什么;第二和第三个问题,主要是想看看你解决难题的能力,以及面对压力和困难时的心态;第四个问题,主要是想了解一下你面对错误时的态度,还要了解你是否会对错误有所总结和改进。
这些问题都会伴随着对各种细节的不停追问,因为这样的问题太容易造假了。所以,面试官会不停地追问细节,就像审问一样。因为一个谎言需要用更多的谎言来掩盖,如果没有经过高强度和专业的训练的话,最好不要撒谎。因此对于业余的不是做特工或是间谍的人来说,谎言是经不起追问的。
怎样准备这样的题,我这里有几个提示。
要有框架。讲故事要学会使用 STAR 。Situation - 在什么样的环境和背景下,Task - 你要干什么样的事,Action - 你采取了什么样的行动和努力,Result - 最终得到了什么样的效果。这是整个语言组织的框架,不要冗长啰嗦。
要有细节。没有细节的故事听起来就很假,所以,其中要有很多细节。因为是技术方面的,所以,一定要有很多技术细节。
要有感情。讲这些故事一定要带感情。要让面试官感受到你的热情、骄傲、坚韧和顽强。一定要是真实的,只有真实的事才会有真实的感情。
要有思考。只有细节和故事还不够,还要有自己的思考和得失总结,以及后续的改进。
要做到上述,是不容易的。一般来说,你也是需要训练的。首先,你要形成及时总结的习惯,对自己的日常工作和经历做总结,否则难免会有“书到用时方恨少”的感觉。另外,你还需要训练自己的语言组织能力。最后,你还要有对这些事件的思考,这需要和其他人进行讨论和总结。
对此,如果你想有一个比较好的面试回答效果,这不是你能临时准备出来的,工夫都是花在平时的。而训练这方面能力的最好方式就是在工作中写文档 ,在工作之余写博客。只有写得多了,写得好了,你这样的能力才能训练出来。
总结一下今天的内容。面试前的准备该怎样做,对面试成功与否至关重要。在这篇文章中,我分享了自己总结一些经验和相关技巧。首先是怎样写简历,我认为,简历上的信息不要写太多,信息太多相当于没有信息,不要单纯地罗列,要突出自己的长处和技能。
然后是技术知识的准备,我强调,无论你在简历里写什么技术,这些技术的基础知识你都得学一下。即便不能记住所有的知识点,但是 80% 以上的关键知识点,你多少得知道吧。随后是算法题的准备,我推荐了 LeetCode,并给出了好几种经典算法题的解题套路。
最后是工作项目的准备,给出了几种经典的面试问题及应答思路,并分享了该如何做准备。我认为,想有一个比较好的面试回答效果,是临时准备不出来的,要将工夫花在平时。
下篇文章中,介绍的是面试中的技巧,比如,答不出来时该怎么办、如何回答尖锐问题、如何抓住最后提问的机会等,很有实践指导意义。敬请期待。
来源:《左耳听风专栏:程序员面试攻略》
2025-02-14 10:28:30
面试和笔试的要点其实差不多,基础知识和实战经验都是最重要的关注点(当然,面试时的态度和眼缘也很重要)。
实际面试时,因为时间有限,不可能所有问题都问一遍,一般是根据简历上涉及的内容,抽一部分话题来聊一聊。看看面试者的经验、态度,以及面对一层层深入问题时的处理思路。借此了解面试者的技术水平,对深度、广度,以及思考和解决问题的能力。
常见的面试套路是什么呢?
XXX 是什么?
实现原理是什么?
为什么这样实现?
如果让你实现你会怎么做?
分析下你的实现有什么优缺点?
有哪些需要改进的地方?
下面总结一些比较常见的面试题,供大家参考。针对这些问题,大家可以给自己打一个分。
0 分:不清楚相关知识。
30 分:有一点印象,知道一些名词。
60 分:知道一些概念以及含义,了解功能和常见用途。
80 分:能在参考答案的基础上进行补充。
100 分:发现参考答案的问题。
下面我们来看看 JVM 相关面试问题。
JVM 全称是 Java Virtual Machine,中文称为 Java 虚拟机。
JVM 是 Java 程序运行的底层平台,与 Java 支持库一起构成了 Java 程序的执行环境。
分为 JVM 规范和 JVM 实现两个部分。简单来说,Java 虚拟机就是指能执行标准 Java 字节码的虚拟计算机。
现在的 JDK、JRE 和 JVM 一般是整套出现的。
JDK = JRE + 开发调试诊断工具
JRE = JVM + Java 标准库
常见的 JDK 厂商包括:
Oracle 公司,包括 Hotspot 虚拟机、GraalVM,分为 OpenJDK 和 OracleJDK 两种版本
IBM 公司,J9 虚拟机,用在 IBM 的产品套件中
Azul Systems 公司,高性能的 Zing 和开源的 Zulu
阿里巴巴,Dragonwell 是阿里开发的 OpenJDK 定制版
亚马逊,Corretto OpenJDK
Red Hat 公司的 OpenJDK
Adopt OpenJDK
此外,还有一些开源和试验性质的 JVM 实现,比如 Go.JVM
各种版本的 JDK 一般来说都会符合 Java 虚拟机规范。 两者的区别一般来说包括:
两种 JDK 提供的工具套件略有差别,比如 jmc 等有版权的工具。
某些协议或配置不一样,比如美国限制出口的加密算法。
其他细微差别,比如 JRE 中某些私有的 API 不一样。
有一说一,选择哪个版本需要考虑研发团队的具体情况:比如机器的操作系统、团队成员的掌握情况、兼顾遗留项目等等。
当前 Java 最受欢迎的长期维护版本是 Java 8 和 Java 11。
Java 8 是经典 LTS 版本,性能优秀,系统稳定,良好支持各种 CPU 架构和操作系统平台。
Java 11 是新的长期支持版,性能更强,支持更多新特性,而且经过几年的维护已经很稳定。
有的企业在开发环境使用 OracleJDK,在生产环境使用 OpenJDK。也有的企业恰好相反,在开发环境使用 OpenJDK,在生产环境使用 OracleJDK。也有的公司使用同样的打包版本。开发和部署时只要进行过测试就没问题。一般来说,测试环境、预上线环境的 JDK 配置需要和生产环境一致。
Java 中的字节码,是值 Java 源代码编译后的中间代码格式,一般称为字节码文件。
字节码文件中,一般包含以下部分:
版本号信息
静态常量池(符号常量)
类相关的信息
字段相关的信息
方法相关的信息
调试相关的信息
可以说,大部分信息都是通过常量池中的符号常量来表述的。
常量是指不变的量,字母 ‘K’ 或者数字 1024 在 UTF-8 编码中对应到对应的二进制格式都是不变的。同样地,字符串在 Java 中的二进制表示也是不变的, 比如 “KK”。
在 Java 中需要注意的是,final 关键字修饰的字段和变量,表示最终变量,只能赋值 1 次,不允许再次修改,由编译器和执行引擎共同保证。
在 Java 中,常量池包括两层含义:
静态常量池,class 文件中的一个部分,里面保存的是类相关的各种符号常量。
运行时常量池,其内容主要由静态常量池解析得到,但也可以由程序添加。
根据 JVM 规范,标准的 JVM 运行时数据区包括以下部分:
程序计数器
Java 虚拟机栈
堆内存
方法区
运行时常量池
本地方法栈
具体的 JVM 实现可根据实际情况进行优化或者合并,满足规范的要求即可。
堆内存是指由程序代码自由分配的内存,与栈内存作区分。
在 Java 中,堆内存主要用于分配对象的存储空间,只要拿到对象引用,所有线程都可以访问堆内存。
以 Hotspot 为例,堆内存(HEAP)主要由 GC 模块进行分配和管理,可分为以下部分:
新生代
存活区
老年代
其中,新生代和存活区一般称为年轻代。
除堆内存之外,JVM 的内存池还包括非堆(NON_HEAP),对应于 JVM 规范中的方法区,常量池等部分:
MetaSpace
CodeCache
Compressed Class Space
内存溢出(OOM)是指可用内存不足。
程序运行需要使用的内存超出最大可用值,如果不进行处理就会影响到其他进程,所以现在操作系统的处理办法是:只要超出立即报错,比如抛出“内存溢出错误”。
就像杯子装不下,满了要溢出来一样,比如一个杯子只有 500ml 的容量,却倒进去 600ml,于是水就溢出造成破坏。
内存泄漏(Memory Leak)是指本来无用的对象却继续占用内存,没有再恰当的时机释放占用的内存。
不使用的内存,却没有被释放,称为“内存泄漏”。也就是该释放的没释放,该回收的没回收。
比较典型的场景是:每一个请求进来,或者每一次操作处理,都分配了内存,却有一部分不能回收(或未释放),那么随着处理的请求越来越多,内存泄漏也就越来越严重。
在 Java 中一般是指无用的对象却因为错误的引用关系,不能被 GC 回收清理。
如果存在严重的内存泄漏问题,随着时间的推移,则必然会引起内存溢出。
内存泄漏一般是资源管理问题和程序 Bug,内存溢出则是内存空间不足和内存泄漏的最终结果。
public class MyOrder{ private long orderId; private long userId; private byte state; private long createMillis; }
一般来说,MyOrder 类的每个对象会占用 40 个字节。
计算方式为:
对象头占用 12 字节。
每个 long 类型的字段占用 8 字节,3 个 long 字段占用 24 字节。
byte 字段占用 1 个字节。
以上合计 37 字节,加上以 8 字节对齐,则实际占用 40 个字节。
对象头中一般包含两个部分:
标记字,占用一个机器字,也就是 8 字节。
类型指针,占用一个机器字,也就是 8 个字节。
如果堆内存小于 32GB,JVM 默认会开启指针压缩,则只占用 4 个字节。
所以前面的计算中,对象头占用 12 字节。如果是数组,对象头中还会多出一个部分:
数组长度,int 值,占用 4 字节。
截止目前(2020 年 3 月),JVM 可配置参数已经达到 1000 多个,其中 GC 和内存配置相关的 JVM 参数就有 600 多个。但在绝大部分业务场景下,常用的 JVM 配置参数也就 10 来个。
例如:
# JVM 启动参数不换行 # 设置堆内存 -Xmx4g -Xms4g # 指定 GC 算法 -XX:+UseG1GC -XX:MaxGCPauseMillis=50 # 指定 GC 并行线程数 -XX:ParallelGCThreads=4 # 打印 GC 日志 -XX:+PrintGCDetails -XX:+PrintGCDateStamps # 指定 GC 日志文件 -Xloggc:gc.log # 指定 Meta 区的最大值 -XX:MaxMetaspaceSize=2g # 设置单个线程栈的大小 -Xss1m # 指定堆内存溢出时自动进行 Dump -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/
此外,还有一些常用的属性配置:
# 指定默认的连接超时时间 -Dsun.net.client.defaultConnectTimeout=2000 -Dsun.net.client.defaultReadTimeout=2000 # 指定时区 -Duser.timezone=GMT+08 # 设置默认的文件编码为 UTF-8 -Dfile.encoding=UTF-8 # 指定随机数熵源(Entropy Source) -Djava.security.egd=file:/dev/./urandom
需要根据系统的配置来确定,要给操作系统和 JVM 本身留下一定的剩余空间。推荐配置系统或容器里可用内存的 70~80% 最好。
比如说系统有 8G 物理内存,系统自己可能会用掉一点,大概还有 7.5G 可以用,那么建议配置 -Xmx6g
。
说明:7.5G*0.8=6G,如果知道系统里有明确使用堆外内存的地方,还需要进一步降低这个值。
JVM 总内存 = 栈 + 堆 + 非堆 + 堆外 + Native
一般来说,JDK 8 及以下版本通过以下参数来开启 GC 日志:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
如果是在 JDK 9 及以上的版本,则格式略有不同:
-Xlog:gc*=info:file=gc.log:time:filecount=0
java -XX:+UseG1GC -Xms4g -Xmx4g -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps Hello
Java 8 版本的 Hotspot JVM,默认情况下使用的是并行垃圾收集器(Parallel GC)。其他厂商提供的 JDK 8 基本上也默认使用并行垃圾收集器。
Java 9 之后,官方 JDK 默认使用的垃圾收集器是 G1。
常见的垃圾收集器包括:
串行垃圾收集器:-XX:+UseSerialGC
并行垃圾收集器:-XX:+UseParallelGC
CMS 垃圾收集器:-XX:+UseConcMarkSweepGC
G1 垃圾收集器:-XX:+UseG1GC
就是只有单个 worker 线程来执行 GC 工作。
并行垃圾收集,是指使用多个 GC worker 线程并行地执行垃圾收集,能充分利用多核 CPU 的能力,缩短垃圾收集的暂停时间。
除了单线程的 GC,其他的垃圾收集器,比如 PS、CMS、G1 等新的垃圾收集器都使用了多个线程来并行执行 GC 工作。
并发垃圾收集器,是指在应用程序在正常执行时,有一部分 GC 任务,由 GC 线程在应用线程一起并发执行。 例如 CMS/G1 的各种并发阶段。
首先,G1 的堆内存不再单纯划分为年轻代和老年代,而是划分为多个(通常是 2048 个)可以存放对象的小块堆区域(smaller heap regions)。
每个小块,可能一会被定义成 Eden 区,一会被指定为 Survivor 区或者 Old 区。
这样划分之后,使得 G1 不必每次都去回收整个堆空间,而是以增量的方式来进行处理:每次只处理一部分内存块,称为此次 GC 的回收集(collection set)。
下一次 GC 时在本次的基础上,再选定一定的区域来进行回收。增量式垃圾收集的好处是大大降低了单次 GC 暂停的时间。
年轻代是分来垃圾收集算法中的一个概念,相对于老年代而言,年轻代一般包括:
新生代,Eden 区。
存活区,执行年轻代 GC 时,用存活区来保存活下来的对象。存活区也是年轻代的一部分,但一般有 2 个存活区,所以可以来回倒腾。
因为 GC 过程中,有一部分操作需要等所有应用线程都到达安全点,暂停之后才能执行,这时候就叫做 GC 停顿,或者叫做 GC 暂停。
这两者一般可以认为就是同一个意思。
缺乏经验的话,针对当前问题,往往需要使用不同的工具来收集信息,例如:
收集不同的指标(CPU、内存、磁盘 IO、网络等等)
分析应用日志
分析 GC 日志
获取线程转储并分析
获取堆转储来进行分析
一般根据 APM 监控来排查应用系统本身的问题,有时候也可以使用 Chrome 浏览器等工具来排查外部原因,比如网络问题。
可量化的 3 个性能指标:
系统容量:比如硬件配置,设计容量;
吞吐量:最直观的指标是 TPS;
响应时间:也就是系统延迟,包括服务端延时和网络延迟。
这些指标。可以具体拓展到单机并发、总体并发、数据量、用户数、预算成本等等。
这个问题请根据实际情况回答,比如 Linux 命令,或者 JDK 提供的工具等。
可以使用 ps -ef
和 jps -v
等等。
比如:free -m
、free -h
、top
命令等等。
一般先使用 jps 命令,再使用 jstack -l
。
一般使用 jmap 工具来获取堆内存快照。
根据实际情况来看,获取内存快照可能会让系统暂停或阻塞一段时间,根据内存量决定。
使用 jmap 时,如果指定 live 参数,则会触发一次 Full GC,需要注意。
示例:
jmap -dump:format=b,file=3826.hprof 3826
JVM 有一个内置的分析器叫做 HPROF,堆内存转储文件的格式,最早就是这款工具定义的。
一般使用 Eclipse MAT 工具,或者 jhat 工具来处理。
上网搜索是比较笨的办法,但也是一种办法。
另外就是,各种 JDK 工具都支持 -h
选项来查看帮助信息,只要用得比较熟练,即使忘记了也很容易根据提示进行操作。
比如 GC 问题、内存泄漏问题、或者其他疑难杂症等等。然后可能还有一些后续的问题。例如:
你遇到过的印象最深的 JVM 问题是什么?
这个问题是怎么分析和解决的?
这个过程中有哪些值得分享的经验?
此问题为开放性问题,请根据自身情况进行回答。
来源:《JVM 核心技术 32 讲》
2025-02-10 09:18:19
通过前面的学习,我们发现 GC 日志量很大,人工分析太消耗精力了。由于各种 GC 算法的复杂性,它们的日志格式互相之间不太兼容。
有没有什么工具来减少我们的重复劳动呢? 这种轮子肯定是有现成的。比如 GCEasy、GCViwer 等等。
这一节我们就开始介绍一些能让我们事半功倍的工具。
GCEasy 工具由 Tier1app 公司 开发和支持,这家公司主要提供3款分析工具:
GCEasy,访问地址:https://gceasy.io/,是一款在线的 GC 日志分析工具,支持各种版本的 GC 日志格式。
FastThread,官网地址:https://fastthread.io/,线程分析工具,后面我们专门有一节课程会进行介绍。
HeapHero,官网地址:https://heaphero.io/,顾名思义,这是一款 Heap Dump 分析工具。
其中 GCEasy 可用来分析定位GC和内存性能问题,支持以下三种模式:
官方网站在线分析(免费),我们主要介绍这种方式
API 接口调用(付费计划)
本地安装(企业付费)
作为一款商业产品,分析能力和结果报告自然是棒棒的。
可以分析 GC 日志和 JStat 日志
支持上传文件的方式(免费)
支持粘贴日志文本的方式(免费)
支持下载结果报告 *(付费方案)
支持分享链接(免费】
支持 API 调用的方式 *(付费方案)
企业版支持本地安装 *(企业付费)
付费方案可以免费试用:就是说结果现在也是可以试用下载的
我们这里依然使用前面演示的示例代码,稍微修改一下,让其执行 30 秒左右。
假设程序启动参数为:
-XX:+UseParallelGC -Xms512m -Xmx512m -Xloggc:gc.demo.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
然后我们就得到了一个 GC 日志文件 gc.demo.log。
打开页面 https://gceasy.io/,选择上传文件或者粘贴文本:
我们也可以使用 API 调用方式,官方给出的示例如下:
curl -X POST --data-binary @./my-app-gc.log https://api.gceasy.io/analyzeGC?apiKey={API_KEY_SENT_IN_EMAIL} --header "Content-Type:text"
有 API 支持,就可以通过编程的方式,或者自动化脚本的方式来使用这个工具。
当然,有上传 API,肯定也有下载 API。本文不进行详细的介绍,有兴趣可以看官方文档。
下面我们介绍一款很好用的开源分析工具:GCViwer。
GCViewer 项目的 GitHub 主页是:
然后我们在 Github 项目的 releases 页面 中,找到并下载最新的版本,例如:gcviewer-1.36.jar。
Mac 系统可以直接下载封装好的应用:gcviewer-1.36-dist-mac.zip。下载,解压,安装之后首次打开可能会报安全警告,这时候可能需要到安全设置里面去勾选允许,例如:
先获取 GC 日志文件,方法同上面的 GCEasy 一样。
可以通过命令行的方式启动 GCViewer 工具来进行分析:
java -jar gcviewer_1.3.4.jar
新版本支持用 java 命令直接启动。老版本可能需要在后面加上 GC 日志文件的路径。工具启动之后,大致会看到类似下面的图形界面:
然后在图形界面中点击对应的按钮打开日志文件即可。现在的版本支持单个 GC 日志文件,多个 GC 日志文件,以及网络 URL。
当然,如果不想使用图形界面,或者没法使用图形界面的情况下,也可以在后面加上程序参数,直接将分析结果输出到文件。
例如执行以下命令:
java -jar gcviewer-1.36.jar /xxxx/gc.demo.log summary.csv chart.png
这会将信息汇总到当前目录下的 summary.csv 文件之中,并自动将图形信息保存为 chart.png 文件。
在图形界面中打开某个 GC 日志文件。
上图中,Chart 区域是对 GC 事件的图形化展示。包括各个内存池的大小和 GC 事件。其中有 2 个可视化指标:蓝色线条表示堆内存的使用情况,黑色的 Bar 则表示 GC 暂停时间的长短。每个颜色表示什么信息可以参考 View 菜单。
从前面的图中可以看到,程序启动很短的时间后,堆内存几乎全部被消耗,不能顺利分配新对象,并引发频繁的 Full GC 事件. 这说明程序可能存在内存泄露,或者启动时指定的内存空间不足。
从图中还可以看到 GC 暂停的频率和持续时间。然后发现 GC 几乎不间断地运行。
右边也有三个选项卡可以展示不同的汇总信息:
“Summary(摘要)” 中比较有用的是:
“Throughput”(吞吐量百分比),吞吐量显示了有效工作的时间比例,剩下的部分就是 GC 的消耗
“Number of GC pauses”(GC 暂停的次数)
“Number of full GC pauses”(Full GC 暂停的次数)
以上示例中的吞吐量为 13.03%。这意味着有 86.97% 的 CPU 时间用在了 GC 上面。很明显系统所面临的情况很糟糕——宝贵的 CPU 时间没有用于执行实际工作,而是在试图清理垃圾。原因也很简单,我们只给程序分配了 512MB 堆内存。
下一个有意思的地方是“Pause”(暂停)选项卡:
其中“Pause”展示了 GC 暂停的总时间,平均值,最小值和最大值,并且将 total 与 minor/major 暂停分开统计。如果要优化程序的延迟指标,这些统计可以很快判断出暂停时间是否过长。
另外,我们可以得出明确的信息:累计暂停时间为 26.89 秒,GC 暂停的总次数为 599 次,这在 30 秒的总运行时间里那不是一般的高。
更详细的 GC 暂停汇总信息,请查看主界面中的“Event details”选项卡:
从“Event details”标签中,可以看到日志中所有重要的GC事件汇总:普通 GC 的停顿次数和 Full GC 停顿次数,以及并发GC 执行数等等。
此示例中,可以看到一个明显的地方:Full GC 暂停严重影响了吞吐量和延迟,依据是 569 次 Full GC,暂停了 26.58 秒(一共执行 30 秒)。
可以看到,GCViewer 能用图形界面快速展现异常的 GC 行为。一般来说,图像化信息能迅速揭示以下症状:
低吞吐量。当应用的吞吐量下降到不能容忍的地步时,用于真正的业务处理的有效时间就大量减少。具体有多大的“容忍度”(tolerable)取决于具体场景。按照经验,低于 90% 的有效时间就值得警惕了,可能需要好好优化下 GC。
单次 GC 的暂停时间过长。只要有一次 GC 停顿时间过长,就会影响程序的延迟指标。例如,延迟需求规定必须在 1000ms 以内完成交易,那就不能容忍任何一次GC暂停超过 1000 毫秒。
堆内存使用率过高。如果老年代空间在 Full GC 之后仍然接近全满,程序性能就会大幅降低,可能是资源不足或者内存泄漏。这种症状会对吞吐量产生严重影响。
真是业界的福音——图形化展示的 GC 日志信息绝对是我们重磅推荐的。不用去阅读和分析冗长而又复杂的 GC 日志,通过图形界面,可以很容易得到同样的信息。不过,虽然图形界面以对用户友好的方式展示了重要信息,但是有时候部分细节也可能需要从日志文件去寻找。
来源:《JVM 核心技术 32 讲》
2025-02-07 10:12:34
G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
G1 相关的调优参数,可以参考:
https://www.oracle.com/technical-resources/articles/java/g1gc.html
G1 使用示例:
# 请注意命令行启动时没有换行 java -XX:+UseG1GC -Xms512m -Xmx512m -Xloggc:gc.demo.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps demo.jvm0204.GCLogAnalysis
运行之后,我们看看 G1 的日志长什么样:
Java HotSpot(TM) 64-Bit Server VM (25.162-b12) ...... Memory: 4k page,physical 16777216k(709304k free) CommandLine flags: -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC 2019-12-23T01:45:40.605-0800: 0.181: [GC pause (G1 Evacuation Pause) (young),0.0038577 secs] [Parallel Time: 3.1 ms,GC Workers: 8] ...... 此处省略多行 [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.2 ms] [Other: 0.6 ms] ...... 此处省略多行 [Eden: 25.0M(25.0M)->0.0B(25.0M) Survivors: 0.0B->4096.0K Heap: 28.2M(512.0M)->9162.7K(512.0M)] [Times: user=0.01 sys=0.01,real=0.00 secs] 2019-12-23T01:45:40.881-0800: 0.456: [GC pause (G1 Evacuation Pause) (young) (to-space exhausted),0.0147955 secs] [Parallel Time: 12.3 ms,GC Workers: 8] ...... 此处省略多行 [Eden: 298.0M(298.0M)->0.0B(63.0M) Survivors: 9216.0K->26.0M Heap: 434.1M(512.0M)->344.2M(512.0M)] [Times: user=0.02 sys=0.05,real=0.02 secs] 2019-12-23T01:45:41.563-0800: 1.139: [GC pause (G1 Evacuation Pause) (mixed),0.0042371 secs] [Parallel Time: 3.7 ms,GC Workers: 8] ...... 此处省略多行 [Eden: 20.0M(20.0M)->0.0B(34.0M) Survivors: 5120.0K->4096.0K Heap: 393.7M(512.0M)->358.5M(512.0M)] [Times: user=0.02 sys=0.00,real=0.00 secs] 2019-12-23T01:45:41.568-0800: 1.144: [GC pause (G1 Humongous Allocation) (young) (initial-mark),0.0012116 secs] [Parallel Time: 0.7 ms,GC Workers: 8] ...... 此处省略多行 [Other: 0.4 ms] [Humongous Register: 0.1 ms] [Humongous Reclaim: 0.0 ms] [Eden: 2048.0K(34.0M)->0.0B(33.0M) Survivors: 4096.0K->1024.0K Heap: 359.5M(512.0M)->359.0M(512.0M)] [Times: user=0.01 sys=0.00,real=0.00 secs] 2019-12-23T01:45:41.569-0800: 1.145: [GC concurrent-root-region-scan-start] 2019-12-23T01:45:41.569-0800: 1.145: [GC concurrent-root-region-scan-end,0.0000360 secs] 2019-12-23T01:45:41.569-0800: 1.145: [GC concurrent-mark-start] 2019-12-23T01:45:41.571-0800: 1.146: [GC concurrent-mark-end,0.0015209 secs] 2019-12-23T01:45:41.571-0800: 1.146: [GC remark 2019-12-23T01:45:41.571-0800: 1.147: [Finalize Marking,0.0002456 secs] 2019-12-23T01:45:41.571-0800: 1.147: [GC ref-proc,0.0000504 secs] 2019-12-23T01:45:41.571-0800: 1.147: [Unloading,0.0007297 secs], 0.0021658 secs] [Times: user=0.01 sys=0.00,real=0.00 secs] 2019-12-23T01:45:41.573-0800: 1.149: [GC cleanup 366M->366M(512M),0.0006795 secs] [Times: user=0.00 sys=0.00,real=0.00 secs] Heap garbage-first heap total 524288K,used 381470K [...... region size 1024K,12 young (12288K),1 survivors (1024K) Metaspace used 3331K,capacity 4494K,committed 4864K,reserved 1056768K class space used 364K,capacity 386K,committed 512K,reserved 1048576K
以上是摘录的一部分 GC 日志信息。实际运行我们的示例程序1秒钟,可能会生成上千行的 GC 日志。
当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。如果还没有存活区,则任意选择一部分空闲的内存块作为存活区。
拷贝的过程称为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理。
转移暂停的日志信息很长,为简单起见,我们去除了一些不重要的信息。在并发阶段之后我们会进行详细的讲解。此外,由于日志记录很多,所以并行阶段和“其他”阶段的日志将拆分为多个部分来进行讲解。
我们从 GC 日志中抽取部分关键信息:
2019-12-23T01:45:40.605-0800: 0.181: [GC pause (G1 Evacuation Pause) (young),0.0038577 secs] [Parallel Time: 3.1 ms,GC Workers: 8] ...... worker 线程的详情,下面单独讲解 [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.2 ms] [Other: 0.6 ms] ...... 其他琐碎任务,下面单独讲解 [Eden: 25.0M(25.0M)->0.0B(25.0M) Survivors: 0.0B->4096.0K Heap: 28.2M(512.0M)->9162.7K(512.0M)] [Times: user=0.01 sys=0.01,real=0.00 secs]
大家一起来分析:
[GC pause (G1 Evacuation Pause) (young),0.0038577 secs]
:G1 转移暂停,纯年轻代模式;只清理年轻代空间。这次暂停在 JVM 启动之后 181ms 开始,持续的系统时间为 0.0038577 秒,也就是 3.8ms。
[Parallel Time: 3.1 ms,GC Workers: 8]
:表明后面的活动由 8 个 Worker 线程并行执行,消耗时间为 3.1 毫秒(real time);worker
是一种模式,类似于一个老板指挥多个工人干活。
…...
:为阅读方便,省略了部分内容,可以参考前面的日志,下面紧接着也会讲解。
[Code Root Fixup: 0.0 ms]
:释放用于管理并行活动的内部数据,一般都接近于零。这个过程是串行执行的。
[Code Root Purge: 0.0 ms]
:清理其他部分数据,也是非常快的,如非必要基本上等于零。也是串行执行的过程。
[Other: 0.6 ms]
:其他活动消耗的时间,其中大部分是并行执行的。
…
:请参考后文。
[Eden: 25.0M(25.0M)->0.0B(25.0M)
:暂停之前和暂停之后,Eden 区的使用量/总容量。
Survivors: 0.0B->4096.0K
:GC 暂停前后,存活区的使用量。Heap: 28.2M(512.0M)->9162.7K(512.0M)]
:暂停前后,整个堆内存的使用量与总容量。
[Times: user=0.01 sys=0.01,real=0.00 secs]
:GC 事件的持续时间。
说明:系统时间(wall clock time/elapsed time),是指一段程序从运行到终止,系统时钟走过的时间。一般系统时间都要比 CPU 时间略微长一点。
最繁重的 GC 任务由多个专用的 worker 线程来执行,下面的日志描述了它们的行为:
[Parallel Time: 3.1 ms,GC Workers: 8] [GC Worker Start (ms): Min: 180.6,Avg: 180.6,Max: 180.7,Diff: 0.1] [Ext Root Scanning (ms): Min: 0.1,Avg: 0.3,Max: 0.6,Diff: 0.4,Sum: 2.1] [Update RS (ms): Min: 0.0,Avg: 0.0,Max: 0.0,Diff: 0.0,Sum: 0.0] [Processed Buffers: Min: 0,Avg: 0.0,Max: 0,Diff: 0,Sum: 0] [Scan RS (ms): Min: 0.0,Avg: 0.0,Max: 0.0,Diff: 0.0,Sum: 0.0] [Code Root Scanning (ms): Min: 0.0,Avg: 0.0,Max: 0.1,Diff: 0.1,Sum: 0.1] [Object Copy (ms): Min: 2.2,Avg: 2.5,Max: 2.7,Diff: 0.4,Sum: 19.8] [Termination (ms): Min: 0.0,Avg: 0.2,Max: 0.4,Diff: 0.4,Sum: 1.5] [Termination Attempts: Min: 1,Avg: 1.0,Max: 1,Diff: 0,Sum: 8] [GC Worker Other (ms): Min: 0.0,Avg: 0.0,Max: 0.0,Diff: 0.0,Sum: 0.1] [GC Worker Total (ms): Min: 2.9,Avg: 3.0,Max: 3.0,Diff: 0.1,Sum: 23.7] [GC Worker End (ms): Min: 183.6,Avg: 183.6,Max: 183.6,Diff: 0.0]
Worker 线程的日志信息解读:
[Parallel Time: 3.1 ms,GC Workers: 8]
:前面介绍过,这表明下列活动由 8 个线程并行执行,消耗的时间为 3.1 毫秒(real time)。
GC Worker Start (ms)
:GC 的 worker 线程开始启动时,相对于 pause 开始时间的毫秒间隔。如果 Min 和 Max 差别很大,则表明本机其他进程所使用的线程数量过多,挤占了 GC 的可用 CPU 时间。
Ext Root Scanning (ms)
:用了多长时间来扫描堆外内存(non-heap)的 GC ROOT,如 classloaders、JNI 引用、JVM 系统 ROOT 等。后面显示了运行时间,“Sum”指的是 CPU 时间。
Update RS
、Processed Buffers
、Scan RS
这三部分也是类似的,RS
是 Remembered Set
的缩写,可以参考前面章节。
Code Root Scanning (ms)
:扫描实际代码中的 root 用了多长时间:例如线程栈中的局部变量。
Object Copy (ms)
:用了多长时间来拷贝回收集中的存活对象。
Termination (ms)
:GC 的 worker 线程用了多长时间来确保自身可以安全地停止,在这段时间内什么也不做,完成后 GC 线程就终止运行了,所以叫终止等待时间。
Termination Attempts
:GC 的 worker 线程尝试多少次 try 和 teminate。如果 worker 发现还有一些任务没处理完,则这一次尝试就是失败的,暂时还不能终止。
GC Worker Other (ms)
:其他的小任务, 因为时间很短,在 GC 日志将他们归结在一起。
GC Worker Total (ms)
:GC 的 worker 线程工作时间总计。
[GC Worker End (ms)
:GC 的 worker 线程完成作业时刻,相对于此次 GC 暂停开始时间的毫秒数。通常来说这部分数字应该大致相等,否则就说明有太多的线程被挂起,很可能是因为“坏邻居效应(noisy neighbor)”所导致的。
此外,在转移暂停期间,还有一些琐碎的小任务。
[Other: 0.6 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.3 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.1 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms]
其他琐碎任务,这里只介绍其中的一部分:
[Other: 0.6 ms]
:其他活动消耗的时间,其中很多是并行执行的。
Choose CSet
:选择 CSet 消耗的时间,CSet 是 Collection Set 的缩写。
[Ref Proc: 0.3 ms]
:处理非强引用(non-strong)的时间,进行清理或者决定是否需要清理。
[Ref Enq: 0.0 ms]
:用来将剩下的 non-strong 引用排列到合适的 ReferenceQueue 中。
Humongous Register
、Humongous Reclaim
大对象相关的部分,后面进行介绍。
[Free CSet: 0.0 ms]
:将回收集中被释放的小堆归还所消耗的时间,以便他们能用来分配新的对象。
此次 Young GC 对应的示意图如下所示:
当堆内存的总体使用比例达到一定数值时,就会触发并发标记。这个默认比例是 45%,但也可以通过 JVM 参数 InitiatingHeapOccupancyPercent 来设置。和 CMS 一样,G1 的并发标记也是由多个阶段组成,其中一些阶段是完全并发的,还有一些阶段则会暂停应用线程。
可以在 Evacuation Pause 日志中的第一行看到(initial-mark)暂停,类似这样:
2019-12-23T01:45:41.568-0800: 1.144: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0012116 secs]
当然,这里引发 GC 的原因是大对象分配,也可能是其他原因,例如:to-space exhausted,或者默认 GC 原因等等。
此阶段标记所有从“根区域”可达的存活对象。
根区域包括:非空的区域,以及在标记过程中不得不收集的区域。
对应的日志:
2019-12-23T01:45:41.569-0800: 1.145: [GC concurrent-root-region-scan-start] 2019-12-23T01:45:41.569-0800: 1.145: [GC concurrent-root-region-scan-end,0.0000360 secs]
对应的日志:
2019-12-23T01:45:41.569-0800: 1.145: [GC concurrent-mark-start] 2019-12-23T01:45:41.571-0800: 1.146: [GC concurrent-mark-end,0.0015209 secs]
对应的日志:
2019-12-23T01:45:41.571-0800: 1.146: [GC remark 2019-12-23T01:45:41.571-0800: 1.147: [Finalize Marking,0.0002456 secs] 2019-12-23T01:45:41.571-0800: 1.147: [GC ref-proc,0.0000504 secs] 2019-12-23T01:45:41.571-0800: 1.147: [Unloading,0.0007297 secs],0.0021658 secs] [Times: user=0.01 sys=0.00,real=0.00 secs]
最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升 GC 的效率。此阶段也为下一次标记执行必需的所有整理工作(house-keeping activities)——维护并发标记的内部状态。
要提醒的是,所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的:例如空堆区的回收,还有大部分的存活率计算,此阶段也需要一个短暂的 STW 暂停,才能不受应用线程的影响并完成作业。
这种 STW 停顿的对应的日志如下:
2019-12-23T01:45:41.573-0800: 1.149: [GC cleanup 366M->366M(512M),0.0006795 secs] [Times: user=0.00 sys=0.00,real=0.00 secs]
如果发现某些小堆块中只包含垃圾,则日志格式可能会有点不同,如:
2019-12-23T21:26:42.411-0800: 0.689: [GC cleanup 247M->242M(512M),0.0005349 secs] [Times: user=0.00 sys=0.00,real=0.00 secs] 2019-12-23T21:26:42.412-0800: 0.689: [GC concurrent-cleanup-start] 2019-12-23T21:26:42.412-0800: 0.689: [GC concurrent-cleanup-end,0.0000134 secs]
如果你在执行示例程序之后没有看到对应的 GC 日志,可以多跑几遍试试。毕竟 GC 和内存分配属于运行时动态的,每次运行都可能有些不同。
我们在示例程序中生成的数组大小和缓存哪个对象都是用的随机数,每次运行结果都不一样。
请思考一下我们学过的 Java 随机数 API,有什么办法让每次生成的随机数结果都一致呢?
如有不了解的同学,请搜索:随机数种子。
标记周期一般只在碰到 region 中一个存活对象都没有的时候,才会顺手处理一把,大多数情况下都不释放内存。
示意图如下所示:
并发标记完成之后,G1 将执行一次混合收集(mixed collection),不只清理年轻代,还将一部分老年代区域也加入到 collection set 中。
混合模式的转移暂停(Evacuation Pause)不一定紧跟并发标记阶段。
在并发标记与混合转移暂停之间,很可能会存在多次 Young 模式的转移暂停。
“混合模式”就是指这次 GC 事件混合着处理年轻代和老年代的 region。这也是 G1 等增量垃圾收集器的特色。
而 ZGC 等最新的垃圾收集器则不使用分代算法。当然,以后可能还是会实现分代的,毕竟分代之后性能还会有提升。
混合模式下的日志,和纯年轻代模式相比,可以发现一些有趣的地方:
2019-12-23T21:26:42.383-0800: 0.661: [GC pause (G1 Evacuation Pause) (mixed),0.0029192 secs] [Parallel Time: 2.2 ms,GC Workers: 8] ...... [Update RS (ms): Min: 0.1,Avg: 0.2,Max: 0.3,Diff: 0.2,Sum: 1.4] [Processed Buffers: Min: 0,Avg: 1.8,Max: 3,Diff: 3,Sum: 14] [Scan RS (ms): Min: 0.0,Avg: 0.0,Max: 0.0,Diff: 0.0,Sum: 0.1] ...... [Clear CT: 0.4 ms] [Other: 0.4 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.1 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.1 ms] [Free CSet: 0.1 ms] [Eden: 21.0M(21.0M)->0.0B(21.0M) Survivors: 4096.0K->4096.0K Heap: 337.7M(512.0M)->274.3M(512.0M)] [Times: user=0.01 sys=0.00,real=0.00 secs]
简单解读(部分概念和名称,可以参考 G1 章节):
[Update RS (ms)
:因为 Remembered Sets 是并发处理的,必须确保在实际的垃圾收集之前,缓冲区中的 card 得到处理。如果 card 数量很多,则 GC 并发线程的负载可能就会很高。可能的原因是修改的字段过多,或者 CPU 资源受限。
Processed Buffers
:各个 worker 线程处理了多少个本地缓冲区(local buffer)。
Scan RS (ms)
:用了多长时间扫描来自 RSet 的引用。
[Clear CT: 0.4 ms]
:清理 card table 中 cards 的时间。清理工作只是简单地删除“脏”状态,此状态用来标识一个字段是否被更新的,供 Remembered Sets 使用。
[Redirty Cards: 0.1 ms]
:将 card table 中适当的位置标记为 dirty 所花费的时间。“适当的位置”是由 GC 本身执行的堆内存改变所决定的,例如引用排队等。
G1 是一款自适应的增量垃圾收集器。一般来说,只有在内存严重不足的情况下才会发生 Full GC。比如堆空间不足或者 to-space 空间不足。
在前面的示例程序基础上,增加缓存对象的数量,即可模拟 Full GC。
示例日志如下:
2020-03-02T18:44:17.814-0800: 2.826: [Full GC (Allocation Failure) 403M->401M(512M),0.0046647 secs] [Eden: 0.0B(25.0M)->0.0B(25.0M) Survivors: 0.0B->0.0B Heap: 403.6M(512.0M)->401.5M(512.0M)], [Metaspace: 2789K->2789K(1056768K)] [Times: user=0.00 sys=0.00,real=0.00 secs]
因为我们的堆内存空间很小,存活对象的数量也不多,所以这里看到的 Full GC 暂停时间很短。
此次 Full GC 的示意图如下所示:
在堆内存较大的情况下(8G+),如果 G1 发生了 Full GC,暂停时间可能会退化,达到几十秒甚至更多。如下面这张图片所示:
从其中的 OldGen 部分可以看到,118 次 Full GC 消耗了 31 分钟,平均每次达到 20 秒,按图像比例可粗略得知,吞吐率不足 30%。
这张图片所表示的场景是在压测 Flink 按时间窗口进行聚合计算时发生的,主要原因是对象太多,堆内存空间不足而导致的,修改对象类型为原生数据类型之后问题得到缓解,加大堆内存空间,满足批处理/流计算的需求之后 GC 问题不再复现。
发生持续时间很长的 Full GC 暂停时,就需要我们进行排查和分析,确定是否需要修改 GC 配置,或者增加内存,还是需要修改程序的业务逻辑。关于 G1 的调优,我们在后面的调优部分再进行介绍。
来源:《JVM 核心技术 32 讲》