YuMingzhe's blog

Writing and Teaching is my way of Learning

《当我谈跑步时我谈些什么》读书笔记(下)

第五章 即便那时的我有一条长长的马尾辫子

  • 但凡值得一做的事情,自有值得去做甚至做过头的价值
  • 人总有一日会走下坡路。不管愿意与否,伴随着时间的流逝,肉体总会消亡。一旦肉体消亡,精神也将日暮途穷。此事我心知肚明,却想把那个岔口向后推迟,哪怕只是一丁半点,这就是身为小说家的我设定的目标。

第六章 已经无人敲桌子,无人扔杯子了

  • 我觉得所谓结束,不过是暂时告一段落,并无太大的意义,就同活着一样。并非因为有了结束,过程才具有意义,而是为了便宜地凸显这过程的意义,抑或转弯抹角地比喻其局限性,才在某个地点姑且设置一个结束,相当哲学。
  • 跑过七十五公里,疲劳感突然销声匿迹后,那段意识的空白之中甚至有某种哲学或宗教的妙趣,其中有强迫我内省的东西。
  • 早晨穿上跑鞋准备出去跑步时,我可以感受到它微弱的胎动。在我的周遭和内部,空气的确开始流动。我愿意精心培育这小小的萌芽。为了不漏过一个响动、不错过一个场面、不迷失方向,我向着自己的身体集中精神。
  • 自打时间这东西产生以来(究竟是什么时候啊),它片刻也不曾休息过,一直前行。躲过了夭折一劫的人,作为恩典,都被赋予了实实在在地老去这弥足珍贵的权利。肉体的衰减这种荣誉守候在前方,我们必须接受并习惯它。

第七章 纽约的秋日

  • 据说奔跑时每次脚着地,腿部都要承受三倍于体重的冲击。

第八章 至死都是十八岁

  • 然而人生中,事情的发展不会那么尽遂人意。在我们人生的某个时间点,正希求一个一目了然的结论时,家门口响起的咚咚敲门声,往往来自手拿坏消息的送信人。送信人稍稍用手碰碰帽子,似乎面带抱歉的表情,而他递过来的通知却一点也不会因此而改善。这并非送信人的责任,我们不能责怪他,不能用手揪住他的衣领连推带搡。可怜的送信人不过在忠实地执行上头交代的工作。而将那工作交代下来的,就是我们的老熟人,现实是也。

第九章 至少是跑到了最后

  • 世间游的好的人大有人在,能巧妙地传授游法的人却不多见。
  • 不论到了多大年龄,只要人还活着,对自己就会有新的发现。不论赤身裸体地在镜子前站立多长时间,都不可能映出人的内面来。
  • 在比赛前蘸着唾沫擦拭泳镜,这样内侧就不会起雾。
  • 在肉体上是痛苦的,在精神上,令人沮丧的局面有时也会出现。但“痛苦”对这一运动来说,乃是前提条件般的东西。不伴随着痛苦,还有谁来挑战铁人三项赛和全程马拉松这种费时耗力的运动呢?正因为痛苦,正因为刻意经历这痛苦,我们才能从这个过程中发现自己活着的感觉,至少是发现一部分,才能最终认识到:生存的质量并非成绩、数字和名词之类固定的东西,而是包含于行为中的流动性的东西
  • 对我们至关重要的东西几乎都是肉眼无法看见,然而用心灵可以感受到的。而且真正有价值的东西,往往通过效率甚低的营生方才获得。
  • 勇敢地面对眼前的难题,全力以赴逐一解决。将意识集中于迈出去的每一步,同时还要以尽可能长的眼光去看待问题,尽可能远地去眺望风景。

《当我谈跑步时我谈些什么》读书笔记(上)

前言

我最近在学游泳,没进入泳池前教练一直强调游泳必须要有足够的肺活量才好,才能游地远,游地长。心想自初中到大学,再到工作之余我一直坚持跑步,论肺活量我应该一点也不害怕,可谁知进入水中后才发现强大的水压会将肺活量压缩至原来的 80%,呼吸很是个问题,原来陆地和水中的情况还真是不一样啊。于是我就更加有目的的练习跑步,意在提高肺活量,遂在网上搜索与跑步相关的书,也就有了我与村上春树的《当我谈跑步时我谈些什么》这本书的邂逅。

村上君自 33 岁卖掉自己苦心经营的饭馆转行为职业作家后就开始重拾跑步这项运动。他几乎每天都坚持跑步,忙里抽闲也要跑,小则 10 公里,多则 20、30 公里,并且每年坚持跑一次马拉松。“有效地燃烧自己,也是活着一事的隐喻”是他对跑步这项事业的肯定,同时跑步也在职业、生活等方面给了他很多的启示。本书中作者以从练习跑步时为起点,到跑马拉松等一系列与跑步相关的活动、赛事等事件为时间线,为我们描绘了他在一次次跑步中获得的感悟,这其中不乏作者独到而深邃的哲理,也有作者对生活、生命的体会,我认为这些都是美好和积极向上的,是所有期望追逐幸福,心态上不甘老去的“勇士”想要读到的,遂摘录下来,与大家分享,最后愿岁月在增加了我们的年龄,衰老了我们的容颜时,我们仍能守住那颗追逐的心。

第一章 谁能够笑话米克·贾格尔呢

(注:米克·贾格尔是一名摇滚乐手,年轻时曾口吐豪言壮语:“我如果到了四十五岁还在唱《满足》,还不如死了的好。”,然而,如今他已经过六十了,还是继续在唱《满足》,有些人为了此事笑话他,但作者笑不出来,因为作者认为年轻时的东西无疑有某种滑稽可笑的成分,而根据心境的变化,它们日后未必一文不值)

  • 欧内斯特·海明威说过:持之以恒,不乱节奏。这对长期作业实在至为重要。一旦节奏得以设定,其余的问题便可以迎刃而解。然而要让惯性的轮子以一定的速度准确无误地旋转起来,对待持之以恒,何等小心翼翼也不为过

  • 人生逐渐变得忙碌,日常生活中无法自由地抽出时间来了。并不是说年轻的时候时间要多少有多少,但至少没有如此繁多的琐事。不知何故,琐事这玩意儿似乎随着年龄的增长逐渐增多。 我并非毫无争强好胜之心。但不知何故,跟别人一决雌雄,我自小就不太在乎胜负成败。这种性格在长大成人后也大致未变。无论何事,赢了别人也罢输给别人也罢,都不太计较,倒是更关心能否到达为自己设定的标准。在这层意义上,长跑才是与我的心态完全吻合的体育运动。 跑过一趟全程马拉松便会明白,在比赛中胜过或负于某个特定的人,对跑者来说并不是特别重要。倘若成了夺冠的热门选手,超过眼前的竞争对手便成为重要的课题。然而对参与比赛的普通市民来说,个人的胜负并不是重大话题。对长跑选手而言,在跑完全程时能否感到自豪或类似自豪的东西可能才是最重要的。同样的说法也适用于写作。书的销量、得奖与否、评论的好坏,这些或许能成为成功与否的标志,却不能说是本质问题。写出来的文字是否达到了自己设定的基准,这才至为重要,这才容不得狡辩。别人大概怎么都可以搪塞,自己的心灵却无法蒙混过关。在这层意义上,写小说很像跑全程马拉松,对于创作者而言,其动机安安静静、确确实实地存在于自身内部,不应向外部去寻求形式与标准。 *跑步对我来说,不单是有益的体育锻炼,还是有效的隐喻。我每日一面跑步,或者说一面积累参赛经验,一面将目标的横杆一点点提高,通过超越这高度来提高自己。至少是立志提高自己,并为之日日付出努力。我固然不是了不起的跑步者,而是处于极为平凡的(毋宁说是凡庸的)水准。然而这个问题根本不重要。我超越了昨天的自己,哪怕只是那么一丁点儿,才更为重要。在长跑中,如果说有什么必须战胜的对手,那就是过去的自己

  • 一味跑步,身体没准会变得失衡,不如搭配上其他运动,来塑造一个全面发展的身体,这样不是更好吗?我如此思量。 *河流这东西,除非有过极大的变化,大体看上去相差无几,查尔斯河尤其一如往昔。岁月流逝,学生们的面孔交替更换,我则年龄增长了十岁,恰如那句话所说:往事如烟。尽管如此,河流却仿佛没有丝毫变化,依旧保留着昔日的姿容。涛涛流水向着波士顿湾无声地逝去,浸润了河岸,繁茂了绿色的夏草,养育了水鸟,从石造的古桥下穿过,夏季映照着蓝天白云,冬天则漂浮着冰凌,不急不躁,无休无止,仿佛通过了种种考验、不可动摇的观念一般,只是默默流向大海。
  • 敞开胸怀呼吸清晨那清冽的空气,蹬踏着跑惯了的地面,奔跑时的喜悦重又苏醒过来。脚步声、呼吸声与心脏的鼓动交织一处,营造出独特的交响节奏。 *随着距离的增长,体重竟轻了下来。两个半月减了七磅,腹部一带微微长出来的赘肉也消失了。七磅相当于三公斤多。请想象一下去肉铺买三公斤的肉,拎在手上走回家的情景,大概就能真实地感受到那份重量。想到一度将如许一份重量揣在身上或者,个中滋味颇为复杂
  • 年轻的我要在内心描绘出自己五十多岁的形象,就好比具体的想象死后的世界一样困难。
  • 正是因为有了各种各样的人,这世间方是世间。别人自有别人的价值观和与之相配的活法,我也有自己的价值观和与之相配的活法。这样的差异产生了细微的分歧,数个分歧组合起来,就可能发展成大的误会,让人受到无缘无故的非难。
  • 能在同一道风景中看到不同于他人的景致、感受到不同于他人的东西、选择不同于他人的语句,才能不断写出属于自己的故事来。我就是我,不是别人,这是我的一份重要的资产。心灵所受的伤,便是人为了这种自立性不得不支付给世界的代价
  • 在某种程度上,我也许是主动地追求孤绝。这种孤绝之感会像不时从瓶中溢出的酸一般,在不知不觉中腐蚀人的心灵,将之溶化。这是一把锋利的双刃剑,保护人的心灵,也细微却不间歇地损伤心灵的内壁。这种危险,我们大概有所体味,心知肚明。 *跑长于平日的距离,让肉体更多地消耗一些,好重新认识自己是个能力有限的软弱人类——从最深处物理性地认识这一点。而且跑的距离长于平日,便是强化了自己的肉体,哪怕只是一点点。发怒的话,就将那份怒气冲着自己发好了。感到懊恼的话,就用那份懊恼来磨炼自己好了。能够默默吞咽下去的东西,就一星不剩地吞咽进体内。
  • 穿上跑鞋,在脸上和颈部抹足防晒霜,调节好手表,来到路边,然后开始跑步。脸颊承受着迎面而来的信风,仰头遥望将两条腿齐齐并拢横空飞去的白鹭,倾听令人回味无穷的满匙爱乐队的歌曲。

第二章 人是如何成为跑步小说家的

  • 跑步有好几个长处。首先是不需要伙伴或对手,也不需要特别的器具和装备,更不必特地赶赴某个特别的场所。只要有一双适合跑步的鞋,有一条马马虎虎的路,就可以在兴之所至时爱跑多久就跑多久。游泳虽然一个人就能游,也得找个适宜的游泳池才行。
  • 我知道对感兴趣的领域和相关的事物,按照与自己相配的节奏,借助自己喜欢的方法去探求,就能极其高效地掌握知识和技术。花费了许多时间,技艺才得以成熟,还反复出现过错误,但正因如此,学到的东西才更加扎实。
  • 一天中,身体机能最为活跃的时间因人而异,我是清晨的几小时。在这段时间内集中精力完成重要的工作。随后的时间或是用于运动,或是处理杂物,打理那些不必高度集中精力的工作。日暮时分便优哉游哉,不再继续工作。或是读书或是听音乐,放松精神,尽量早点就寝。拜其所赐,这二十来年工作顺利,效率甚高。只不过照这种模式生活,所谓的夜生活几乎不复存在,与别人的交际往来无疑也受影响。还有人动怒光火。因为别人约我去哪儿玩呀,去做什么事呀,这一类邀请均一一遭到拒绝。
  • 人生中总有一个先后顺序,也就是如何依序安排时间和能量。到一定的年龄之前,如果不在心中制定好这样的规划,人生就会失去焦点,变得张弛失当
  • 假如十个人中有一个人说“这家店很好,我很中意,下次还要来”,就已经足够了。十个客人中只要有一个回头客,这家店就能维持下去。经营者必须拥有明确的姿态和哲学,作为自己的旗帜高高地举起,坚韧不拔地顶住狂风暴雨坚持下去。这是我从开店的亲身经历中学到的
  • 我是那种放任不管的话,什么事都不做也会渐渐发胖的体质。我太太却不管吃多少(吃得不多,可一有什么事就吃甜点),不做运动也根本不会变胖,连赘肉都不长。我尝尝寻思:“人生真不公平啊!”一些人不努力便得不到的东西,有些人却无需努力便唾手可得。不过细想起来,这种生来容易发胖的体质或许是一种幸运。比如说,我这种人为了不增加体重,每天得剧烈地运动,留意饮食,有所节制。何等费劲的人生啊!但倘若从不偷懒,坚持努力,代谢便可以维持在高水平,身体愈来愈健康强壮,老化恐怕也会减缓。什么都不做也不发胖的人无须留意运动和饮食。并无必要却去寻这种麻烦事儿做的人肯定不会太多,因此这种体制的人,体力每每随着年龄的增长日渐衰退。不着意锻炼的话,肌肉自然而然便会松弛,骨质便会疏松。什么才是公平,还得以长远的眼光来看才能看明白
  • 然而并非只凭意志坚强就可以无所不能,人世不是那么单纯。人生来如此,喜欢的事自然可以坚持下去,不喜欢的事怎么也坚持不了。意志之类恐怕也与“坚持”有一丁点瓜葛,然而无论何等意志坚强的人、何等争强好胜的人,不喜欢的事情终究做不到持之以恒;就算做到了,也对身体不利。
  • 我们在学校里学到的最重要的东西,就是“最重要的东西在学校里学不到”这个真理

第三章 在盛夏的雅典跑第一个四十二公里

  • 肌肉难长,易消。赘肉易长,难消。
  • 失去理智的人怀抱的美好幻想,在现实世界中根本是子虚乌有。

第四章 我写小说的许多方法,是每天清晨沿着道路跑步时学到的

  • 肌肉很像记忆力良好的动物,只要注意分阶段地增加负荷量,它就能自然地适应和承受。示以实例,反复地说服肌肉:“你一定得完成这些工作。”它就会“明白”,力气逐渐大起来。当然需要花费时间。过分奴役肌肉,它会发生故障。然而肯花时间循序渐进,它就毫无怨言,只会偶尔苦着脸,顽强而顺从地不断提升强韧度。通过一再重复,将“一定得做好这些工作”的记忆输入肌肉里去。我们的肌肉非常循规蹈矩,只要我们严格遵守程序,它就无怨无悔。倘若一连几天都不给它负荷,肌肉便会自作主张:“哦,没必要那么努力了。哎呀,太好了。”然后自行将承受极限降低。肌肉也同有血有肉的动物一般无二,它也愿意过更舒服的日子,不继续给它负荷,它便会心安理得地将记忆除去。想再度输入的话,必须得从头开始,将同样的模式重复一遍。
  • 才华的问题是,在大部分情况下,它的质量与数理都是主人难以掌控的。有时我们心想数量有些不足,最好再增加一点,或是寻思,节约点使,每次只拿个一星点出来,好使得长久些。哪有这等好事!才华这东西跟我们的一厢情愿毫不相干,它想喷发的时候便径自喷涌而出,想喷多少就喷多少,而一旦枯竭则万事皆休。像舒伯特和莫扎特那样,或某类诗人和摇滚乐手那样,将丰润的才华在短暂的时期内汹涌澎湃地使光用尽,然后戏剧性地逝去,化作一个美丽的传说,这样一种活法固然极具魅力,对我们大多数人却不具参考意义。
  • 集中力是将自己有限的才能汇集起来,倾注在最为需要之处的能力。没有它便不足以做成任何大事。好好使用这种力量,就能弥补才华的不足和偏颇
  • 集中力同耐力与才能不同,可以通过训练在后天获得,也可以不断提升资质。只要每天坐在书桌前,训练将意识倾注于一点,自然就能掌握。这同前面写过的强化肌肉的做法很相似。每天必须集中意识工作——将这样的信息持续不断地传递给身体系统,让它牢牢地记住,再稍稍移动刻度,一点一点将极限值向上提升,注意不让身体发觉。这跟每天坚持慢跑,强化肌肉,逐步打造出跑步者的体型是异曲同工的。给它刺激,持续。再给它刺激,持续。这个过程当然需要耐心,不过一定会得到相应的回报。
  • 人格的成熟也许会弥补才华的衰减,这种弥补当然是有限的,从中还能感受到丧失优势后那淡淡的悲哀。
  • 同样是十年,与其稀里糊涂地活,目的明确、生气勃勃地活当然令人更满意。跑步无疑大有裨益。在个人的局限性中,可以让自己更为有效地燃烧,哪怕只是一丁点,这便是跑步一事的本质,也是活着一事的隐喻。

Java 8 新特性之日期、时间 API

早在Java 8 之前,JDK 为我们提供了 Date 和 Calendar 这两个类来操作日期和时间,从使用角度来看,这两个类的 API 设计简直是反人类,所以在我们开始讲解新API之前,我们先看看已有的 API 存在什么问题,请看下面的代码:

1
2
Date date = new Date(12, 12, 12);
System.out.println(date);

你能说出上面的代码会打印出什么吗?很多程序员会说 “0012-12-12”,但实际上会打印出“Sun Jan 12 00:00:00 IST 1913”,是不是与期望的相差很多,这说明这个 API 从设计上来说对程序员很不友好,使用起来容易产生很多不必要的麻烦,下面我们来分析下上面的代码都有哪些问题:

  • 代码中每个 12 都是什么意思?是年月日、日月年还是其他的组合?
  • 月份是从 0 开始的,如果想要设置为 12 月那么参数应该写成 11,当写成 12 时又会从 1 月开始,所以打印的结果是 1 月
  • 现有的 API 年份是从 1900 年开始计算的,而月份写成 12 后会进位,所以打印的结果年份是 1900 + 12 + 1 = 1913
  • 我们在 new Date() 的过程中只传入了年月日,而打印出来的还有时分秒以及时区,这些信息对我们来说都是多余的

此外,用于解析、格式化日期或时间的 DateFormat 类只能处理 Date 类型,无法处理时间类型且还是非线程安全的,上面只是列举了几个反例而已,实际使用起来还有不少坑要踩,总之用两个字来总结,那就是——难用!

由于存在这些难用的日期 API,大家伙儿都转到了第三方库上,比如 Joda-Time。这让 Oracle 颜面有些挂不住了,于是奋发图强,找来了一批牛人,在吸取了 Joda-Time 的不少设计上的优点以及分析了以前 API 中存在的问题后重新设计了一套原生日期时间 API,这些 API 都位于 java.time 包下。设计者们在设计这些 API 时应用了 领域驱动(domain-driven design)和不可变(Immutability)的设计原则使得 API 简洁容易理解。注意,由于所有位于 java.time 包下的核心类都是不可变的,也就是说对这些类的对象进行的所有操作都会创建一个新的对象,并不会修改原有对象的值,这就避免了线程安全的问题。下面我们将通过代码快速讲解如何去使用它们。

《请回答1988》中那些触动心灵的话语

《请回答1988》这部剧是从微信里看到的,网上评价很好,于是快马加鞭地看了 2 个周。对于我这个多年没看过电视剧的宅男来说真是个不小的挑战,从一开始感觉一集时长太长(1.5 小时),有点吃不消,到后来一下班就抱着笔记本躺在床上期待着剧中洋溢的各种幸福。不得不说,这部剧让我收获了太多美好的事物,比如有丈夫对妻子隐隐的关爱、对追求爱情的执着、邻里互助的温暖……剧的目的就在于弘扬真善美,将亲情、友情、爱情穿插在一个胡同里 5 个家庭的日常生活中,每集中都有不同的故事线,而每个小故事都是导演在告诉观众(尤其是像我这样的)如果我们遇到这样的情景要怎么去处理才能算是成熟的表现,算是有担当。

这部剧让我成长了很多(不可思议的是我竟然开始研究星座了),里面有很多触动心灵的话语,在此记下来,以后常复习,下面斜体的是原话,正体字是我个人见解。我坚信和有思想的人在一起,慢慢地我也会有思想,我必须很努力才会成为未来我想成为的人。

最终消除隔阂的不是无所不知的脑袋,而是手拉手,坚决不放手的那颗心,归根结底是家人。就算是英雄,英雄他爷爷,最后那一刻也要回到家人身边。出了家门从外面世界所受的伤害,各自在生活中留下的伤疤,甚至把家人留给我们的伤痕,也会来抚摸的最后一个安慰,归根结底是家人。还有,不即便如此历史还是在重演——《请回答1988》第一集末尾

大人们只是在忍,只是在忙着大人们的事,只是在用故作坚强来承担年龄的重担,大人们也会疼——《请回答1988》1:07:00开始

为什么 Spring 里 DAO 是单例的

当我们使用 Spring 框架进行企业级软件开发时,系统通常采用分层架构的方案,其中一层就是数据访问层即 DAO层,所以我们经常会写出下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Dao 类
@Repository
public class StudentDao{
    @PersistenceContext
    private EntityManager em;

    public void save(Student student){
        em.persist(student);
    }
}

// Service 层
@Service
@Transactional
public class StudentServiceImpl impl StudentService{
    @Autowired
    private StudentDao dao;

    public void saveStudent(Student student){
        dao.save(student);
    }
}

上面的代码使用了 Spring + JPA 来表示,众所周知 Service 和 Dao 类都是单例的,当多个请求到达控制器时就会创建多个线程并发执行 service 对象,service 对象内部有一个字段 dao(这个字段属于一种状态变量),而 dao 对象内部又注入了一个实体管理器对象 em,我们都知道 EntityManager 是线程不安全的,所以当多个线程执行 service 对象,就相当于多个线程在并发调用 dao 对象,进而相当于多个线程在并发调用 em 对象,那么这个 em 对象中管理的持久化数据就会不断地变化,甚至相互干扰冲突,进而可能造成意料不到的结果。那么既然存在线程安全问题,那么为什么我们按照这种模式编写代码却不会出错呢?

原因在于当使用 Spring 时,在 Dao 类中注入的 EntityManager 并不是真正的实体管理器,而是一个代理,也就是说每次我们调用 em 的方法时,都被代理对象给拦截并做了一些预处理后才把方法调用委托给真正的实体管理器去处理。下面我们通过解析源码的方式来回答这个问题。

JavaScript 作用域和变量声明提升

首先给大家出道题,运行下面的js代码后,foo的值是什么?

1
2
3
4
5
6
7
8
var foo = 1;
function bar() {
  if (!foo) {
      var foo = 10;
  }
  alert(foo);
}
bar();

答案是 10,如果这个结果让你感到不解的话,请接着看下面的题,相信我,它会让你抓狂的:

1
2
3
4
5
6
7
8
var a = 1;
function b() {
  a = 10;
  return;
   function a() {}
}
b();
alert(a)

这次答案是“1”。怎么样,又答错了吧?这段代码看起来比较奇怪,让人困惑,但这也正体现了 JS 这门语言所具有的强大而又具有展现力的语言特性。上面代码的行为之所以会产生让你意想不到的结果,关键在于 JS 有‘变量声明提升’这个特性,本文将对此机制进行分析,但是首先让我们先回顾一下 JavaScript 的作用域这个知识点。

JavaScript作用域

对于 JavaScript 初学者来说,最难以接受的恐怕就是其作用域了。实际上,即使是有经验的 JS 程序员有时也得犯迷糊。主要原因是 JS 看起来具有 C 语言的风格,首先请看下面的C代码:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
  int x = 1;
  printf("%d, ", x); // 1
  if (1) {
      int x = 2;
      printf("%d, ", x); // 2
  }
  printf("%d\n", x); // 1
}

上面的程序将会输出 1,2,1。因为 C 语言的作用域是块级的,当程序执行到一个块级代码时,比如上面的 if 语句,在块内可以声明新的变量,与外部的变量不会产生命名冲突。而这在 JavaScript 中可是会产生问题的。请在控制台中运行下面的代码:

1
2
3
4
5
6
7
var x = 1;
console.log(x); // 1
if (true) {
  var x = 2;
  console.log(x); // 2
}
console.log(x); // 2

运行后可发现会输出 1,2,2。这是因为 JavaScript 的作用域是函数作用域,函数的边界确定了变量的生命周期,只有函数才能产生新的作用域。

对于 C,C++,C# 或 Java 程序员来说,这种特性可能难以接受,但好在 JavaScript 函数比较灵活,还是值的我们去学习的。如果想要在一个函数内创建一个临时的作用域,可以使用像下面的代码来实现:

1
2
3
4
5
6
7
8
9
10
function foo() {
  var x = 1;
  if (x) {
      (function () {
          var x = 2;
          // some other code
      }());
  }
  // x is still 1.
}

上面的代码比较灵活,可以用在任何需要临时作用域的环境下。如果理解了 JS 的作用域,那么掌握变量的声明提升就容易的多了。

声明,名字和提升

在 JavaScript 中, 可以通过以下 4 种方式将变量(或者命名)引入到某个作用域中:

  • 语言内置的方式: JS 的所有作用域(即全局和函数作用域)中都默认带有 this 和 arguments 这两个变量,这两个变量会自动引入到所在的作用域中,无需用户干涉。
  • 函数参数的方式: 每个函数都可以声明形参,而这些形参的作用域就限定在此函数内。
  • 函数声明的方式: 形如 function foo() {} 这样的声明会在函数所在的作用域内引入 foo 这个命名。
  • 变量声明的方式: 形如 var foo 的变量声明,会在变量所在的作用域内引入 foo 这个命名。

函数声明和变量声明总是会被 JavaScript 解释器隐式地移动(提升)到它们所在作用域的最顶端,而函数参数和语言内置的命名本来就已经处于最顶端了。什么意思呢?请看下面的代码:

1
2
3
4
function foo() {
  bar();
  var x = 1;
}

上面的代码实际上会被解释器转换为下面的形式:

1
2
3
4
5
function foo() {
    var x;
    bar();
    x = 1;
}

可以看出变量的声明与执行顺序无关。而下面的两个函数也是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
  if (false) {
      var x = 1;
  }
  return;
  var y = 1;
}
function foo() {
  var x, y;
  if (false) {
      x = 1;
  }
  return;
  y = 1;
}

需要注意的是变量的赋值是不会提升的,只有声明才会被提升。而函数的声明又有些细微的不同,因为函数声明是将整个函数体一起提升。但是要知道函数声明有以下两种方式,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
function test() {
  foo(); // TypeError "foo 不是一个函数"
  bar(); // "这行代码是有效的!"
  var foo = function () { // 将函数表达式赋值给局部变量 'foo'
      alert("this won't run!");
  }
  function bar() { // 函数声明, 引入 'bar' 命名
      alert("this will run!");
  }
}
test();

在上面的例子中,只有 bar 函数及其函数体被提升,以及 foo 变量的声明被提升,但对 foo 的赋值并未提升,整个方法体仍保留在了原来的位置上,因为赋值只有在运行时才会执行。

上面就是 JS 声明提升的基础知识,还不太复杂吧。但是在某些特例中,声明提升还是会让人摸不着头脑。

命名解析次序

有了上面的基础后,我们再来说说特例,其实这些所谓的特例(或者说是按常理推算但结果却是大相径庭的情况)大多数是由命名解析次序导致的。上文中我们提到有四种方式可以将某个变量引入到作用域中,其实我列出的顺序就是它们被解析的顺序。通常,如果某个变量已经定义了,再定义同名变量并不会覆盖原来的值,JS 会忽略定义语句。但是需要注意的是函数声明的优先级要比变量声明的优先级要高,这并不意味着没法把函数赋值给变量,只是这种情况下函数声明会被跳过。 还有几个需要注意的地方:

  • JS内置的变量 arguments 的行为比较怪异。它的声明是位于函数形参之后,函数声明之前,也就是说如果函数的形参也叫 arguments, 即使其值是 undefined,它声明的优先级也高于内置的 arguments。这是 JS 里比较恶心人的地方,所以不要使用 arguments 作为参数名。
  • 如果使用 this 作为变量名将会报 SyntaxError 错误,也是 JS 好的一面。
  • 如果函数的多个参数同名,那么最后一个优先级最高。

命名函数表达式

我们可以通过使用函数表达式的方式给函数命名,其语法与函数声明很类似。但这种方式并不是函数声明的变体,名字不会引入到作用域中,不会对函数体进行声明提升,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
foo(); // TypeError "foo 不是一个函数"
bar(); // 有效
baz(); // TypeError "baz 不是一个函数"
spam(); // ReferenceError "spam 未定义"

var foo = function () {}; // 匿名函数表达式 ('foo' 变量的声明会提升)
function bar() {}; // 函数声明 ('bar' 命名以及函数体都会提升)
var baz = function spam() {}; // 命名函数表达式 (只有变量 'baz' 的声明会提升)

foo(); // 有效
bar(); // 有效
baz(); // 有效
spam(); // ReferenceError "spam 未定义"

编码实践

到目前为止,大家应该都对 JS 的作用域和提升有了比较深的理解了,但是这些知识对 JS 编码有何帮助呢?我想从中得出最重要的经验就是总是用 var 来声明变量。我个人强烈推荐大家在每个作用域的顶部用一个 var 来声明所有的变量。 如果大家都能按这种方式编码,那么几乎不可能踩到与变量提升相关的坑。然而这种做法也有些不足,比如它让编码变得不清晰,我们无法保证作用域内的所有变量都是用 var 声明过得,如若漏掉一个,那么变量就会泄露到全局作用域中。我推荐使用 JSLint,开启 onevar 选项来对代码进行安全检查,使用方法如下:

1
2
3
4
5
6
/*jslint onevar: true [...] */
function foo(a, b, c) {
    var x = 1,
      bar,
      baz = "something";
}

引述规范

我想当遇到问题时,无论查找什么资料,规范(ECMAScript Standard (pdf))无疑是最具有权威性的。那么本文所讲解的关于变量声明和作用域等知识在规范中是如何定义的呢?请看下面(摘录自12.2.2节, 老版本):

If the variable statement occurs inside a FunctionDeclaration, the variables are defined with function-local scope in that function, as described in section 10.1.3. Otherwise, they are defined with global scope (that is, they are created as members of the global object, as described in section 10.1.3) using property attributes { DontDelete }. Variables are created when the execution scope is entered. A Block does not define a new execution scope. Only Program and FunctionDeclaration produce a new scope. Variables are initialised to undefined when created. A variable with an Initialiser is assigned the value of its AssignmentExpression when the VariableStatement is executed, not when the variable is created.

如果变量出现在函数声明内部,那么变量的作用域就是它所在函数的(函数)作用域。否则,它就处于全局作用域中(也就是说它将作为全局对象的一个属性)。变量创建的时机是代码一旦进入某个作用域后就立即创建。而代码块并不会创建作用域。只有程序和函数声明才会创建新的作用域。变量创建后将会初始化为 undefined。变量的值只有在对变量的赋值语句执行后才有,而不是变量创建的时候就赋值。

我希望本文能为那些还在为不理解 JS 的某些令人困惑的问题而苦苦挣扎的同学带来些许光明,同时我也尽可能的按照通俗、透彻的原则进行讲解从而避免挖更多的坑。

注:本文翻译自http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

Java 7 新特性之语法新特性

Java 7 于 2011 年 7 月发布,给大家带来了一系列的新特性。不论从功能上还是语法上都给程序员带来了便利。这篇文章围绕 java 7 在语法上的改进进行介绍。

本文主要介绍内容如下:

  • switch语句支持字符串类型作为变量
  • 增强型的数字表达方式
  • 同时捕获多个异常处理
  • try-with-resources 语句
  • 钻石符改善类型引用

1. switch语句可以用字符串类型作为变量

Java7 之前的 switch 语句只支持 integer 类型的变量,如今也开始支持 String 类型的变量了。其实仔细想想,用字符串作为判断条件是很常见的,switch 这一新特性将在判断时简化一堆 if 语句。下面用一个例子来演示一下用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
        String userName = args[0];
        switch (userName) {
            case "boss":
                System.out.println("Hi boss.Have a good day");
                break;
            case "employee":
                System.out.println("Good morning. Work hard");
                break;
            default:
                System.out.println("welcome");
        }
    }

在使用字符串作为变量时需要注意两点:一是要先检查字符串是否为 null,否则执行 switch 语句时将引发空指针异常;二是 case 语句后的字符串是大小写敏感的,否则 case 比较失败。

2.增强型的数字表达方式

所谓的增强型数字表达式即书写数值时可以用下划线,下划线将字符分组从而提高代码的可阅读性。下划线不仅可以应用到原生数据类型(如二进制、八进制、十六进制或十进制)上,而且也适用于整型和浮点型数值上。下面看一下例子:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
        float deposit = 5_000f;
        float withdraw = 2_500f;
        float minAmount = 2_000f;
        if ((deposit - withdraw) > minAmount) {
            System.out.println("current deposit: " + (deposit - withdraw));
        }
    }

程序输出的结果是:current deposit: 2500.0

可以看出,在书写较大数字的时候下划线会给程序员带来极大的帮助,防止错误发生,并且在输出的结果中并不带有下划线。还需要注意的是,在实际项目中,如果涉及货币的计算,不能用 float 或 double 来表示数值,因为浮点型是无法精确表示小数,在进行运算时极易发生错误,因此要用 java.util.Currency 或将货币转换成最小的货币单位再用 BigDecimal 类进行包装计算。

下划线的唯一目的就是方便程序员阅读,编译器在生成字节码时会忽略掉下划线,并且连续的下划线也会被当作为一个下划线,最终编译器也会将其忽略。尽管下划线使用起来很简单,但语法上也有一些限制:不能放在数值的开头;不能与小数点相邻;书写 float 或 double 类型的数值时不得放在 D,F,L 后缀的前面。下面分别演示下错误的用法:

1
2
3
long deposit=_1234_5678_90L;
float pi=3._1415926f;
long number=123_456_789_L;

Java 虚拟机读书笔记之垃圾收集器

如何判读对象“已死”?

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器值为0的对象就是不可能再被使用的。引用技术法实现简单、效率高,但Java语言并没有选用引用技术法来管理内存,最主要的原因是它很难解决对象之间的相互循环引用的问题。

根搜索算法

在主流的程序语言中,都使用根搜索算法(GC Roots Tracing)判断对象是否是活的。此算法的基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径成为“引用链”,当一个对象到“GC Roots”没有任何引用链相连接时,则证明此对象是不可用的。在Java中可作为GC Roots的对象有如下几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

Java中的引用定义的很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用。这四种引用强度依次逐渐减弱,意义如下:

  • 强引用:强引用在程序代码中是普遍存在的,如 Object o= new Object() 中 o 就是强引用,只要强引用还在,垃圾回收器永远不会回收被引用的对象;
  • 软应用:软引用用来描述一些还有用,但并非必需的对象。对于软引用所引用的对象,在系统发生内存溢出之前会被列入垃圾回收范围之内并进行第二次回收,使用 SoftReference 类实现软引用。
  • 弱引用:也用来描述非必需的对象,被弱引用所引用的对象只能生存到下一次垃圾回收发生之前,使用 WeakReference 类实现弱引用。
  • 虚引用:是最弱的一种引用关系,它并不会影响其所引用的对象的生存时间,也无法通过虚引用来获取一个对象的实例。为一个对象设置一个虚引用的唯一目的就是希望能在这个对象被回收时收到一个系统通知。使用 PhantomReference 来实现虚引用。

在根搜索算法中不可达的对象并非必被回收,在被回收前至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。如果对象被判定为有必要执行 finalize() 方法,这个对象将被放置在名为 F-Queue 的队列之中。finalize() 方法是对象逃脱被回收的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,然后就进行垃圾回收。

垃圾收集算法

1.标记-清除算法

此算法是最基础的算法,分为两个阶段:首先标记出所有需要被回收的对象,在标记完成后统一回收掉被标记的对象。有两个主要的缺点:一个是效率问题,标记和清除的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,当程序需要分配较大的对象时无法找到足够大的连续内存而不得不进行垃圾回收。

2.复制算法

此算法将内存划分为大小相等的两个区域,每次只是用其中的一块,每当这块内存用完了,就将还存活的对象复制到另一块内存区域中,然后把使用的这块内存给清空,这样使得每次只对其中一块内存进行内存回收,也不会有内存碎片的情况了,只要移动堆顶指针即可顺序分配内存。代价是将内存缩小为原来的一半。

据研究表明,新生代中对象 98% 都是朝生夕死的,所以并不需要按照 1:1 的比例进行内存划分,而是将内存划分为一块较大的 Eden 空间和两个较小的 Survivor 空间,每次只是用 Eden 空间和其中的一块 Survivor 空间。当进行垃圾回收时,将 Eden 和 Survivor 空间中还存活的对象复制到另一块 Survivor 空间中,然后对 Eden 和使用过的 Survivor 空间进行清理。Hotspot 虚拟机默认 Eden 和S urvivor 空间的大小比例为8:1.

3.标记-整理算法

复制算法在对象存活率较高时会进行较多的复制操作,效率会变低且空间浪费严重。因此出现了标记整理算法,其思路是先标记所有需要被回收的对象,然后将存活的对象向一端移动,然后直接清理掉边界以外的内存区域即可。

4.分代收集算法

根据对象的存活周期不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次都会回收大量对象,只有少量对象会存活下来,那就选用复制算法,只需少量复制操作即可完成收集。而老年代中的对象存活率较高,没有额外空间对其进行担保,所以就要使用“标记-清理”或“标记-整理”算法进行回收。

垃圾收集器

1.Serial收集器

Serial 收集器是最基本、历史最悠久的收集器,曾经是新生代收集器的唯一选择。这个收集器是一个单线程的收集器,单线程的意思不仅仅只它只能使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾回收时必须暂停所有其他的工作线程,直到它收集结束。在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说是不可接受的。它是 jvm 运行在 client 模式下的默认新生代垃圾回收器。

2.ParNew收集器

ParNew 收集器其实就是 Serial 收集器的多线程版,除了使用多条线程进行垃圾回收外,其余行为包括 Serial 收集器可用的所有控制参数(-XX:SurvivorRation、-XX:PretenurSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、对象分配规则、回收策略等都与 Serial 收集器完全一样。它是许多运行在 server 模式下 JVM 的首选新生代收集器,目前它只能与 CMS 收集器配合工作(CMS 收集器是 Hotspot 第一款真正意义上的并发收集器,第一次实现了垃圾回收线程与用户线程基本同时工作)。不幸的的是 CMS 作为老年代收集器,无法与 Parallel Scavenge 收集器配合工作,其只能与 Serial 或 ParNew 进行配合工作。ParNew 也是使用了 -XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用 -XX:+UseParNewGC 选项强制使用。

3.Parallel Scavenge收集器

Parallel Scavenge 收集器也是新生代的收集器,也是使用复制算法的收集器,也是并行的多线程的收集器。

CMS 等收集器的关注点在于尽可能的缩短垃圾收集时用户线程暂停时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。吞吐量就是 CPU 运行用户代码的时间与 CPU 总时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),jvm 总共运行了100分钟,其中垃圾回收用来1分钟,那么吞吐量为 99%。停顿时间越短越适合需要与用户交互的程序,而吞吐量则可以最高效率的使用 cpu 时间,尽可能快的完成程序计算任务,主要适合在后台运算不需与用户有交互的应用。

Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数及设置吞吐量大小的 -XX:GCTimeRatio 参数。MaxGCPauseMillis 参数允许的值是一个大于 0 的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。GCTimeRatio 参数的值应当是一个大于 0 小于 100 的参数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数,公式为1/(n+1)。Parallel Scavenge 收集器也称为“吞吐量优先”收集器, 它除了上面两个选项外,还有另外一个选项需要注意 -XX:UseAdaptiveSizePolicy,这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大的吞吐量,这种调节方式成为GC自适应的调节策略(GC Ergonomics),只需要把基本的内存数据设置好(如 -Xmx 设置最大堆),然后使用 MaxGCPauseMillis 参数或 GCTimeRatioc 参数给虚拟机设立一个优化目标即可。自适应调节策略也是 Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别。

4.Serial Old收集器

Serial Old 是 Serial 收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法,主要在 client 模式下使用,在 server 模式下,可以与 Parallel Scavenge 收集器搭配使用,也可以作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

5.Parallel Old收集器

Parallel Old 是 Parallel Scanvege 收集器的老年代版本,使用多线程和“标记整理”算法,在注重吞吐量和CPU资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,重视服务的响应速度,系统停顿时间最短,给用户带来较好的体验。CMS 收集器是基于“标记-清除”算法的,垃圾回收分为4个步骤:1初始标记,2并发标记,3重新标记,4并发清除。其中初始标记和重新标记仍然需要暂停用户线程。初始标记仅仅是标记一下 GC Roots 能关联到的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器缺点如下:

  • 1.CMS 收集器默认启动的收集线程数是(CPU数量+3)/4,当 CPU 数量在 4 个以上时,并发垃圾回收时垃圾收集线程最多占用不超过 25% 的CPU资源,当时当 CPU 数量在4个一下时,CPU的负载就会比较高。

  • 2.CMS 收集器无法处理“浮动垃圾”,因而会导致 Full GC 的产生。由于 CMS 在并发清理阶段用户线程也在运行,在运行过程中有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS 无法在本次收集中处理它们,只能留到下一次垃圾回事再处理,之一部分垃圾就成为浮动垃圾。在进行垃圾回收时要预留一部分内存空间给用户线程使用,所以 CMS 收集器默认情况下在老年代内存空间使用 68% 时就会被触发,要是 CMS 运行期间预留的内存无法满足程序需要,就会出现”Concurrent Mode Failure”失败,这是将启动后备预案:调用 Serial Old 收集器来对老年代进行垃圾回收,这样停顿时间就变长了。如果在应用中老年代增长的不是特别快,可以适当的调高参数 -XX:CMSInitiatingOccupancyFraction 的值来提高触发百分比,以便降低内存回收次数以获取更好的性能。

  • 3.CMS 是一款基于“标记-清除”算法实现的收集器,垃圾收集后会产生内存碎片,将会给大对象分配带来很大的麻烦。为解决这个问题,CMS收集器提供了一个 -XX:UseCMSCompactAtFullCollection 的开关参数,用于在 Full GC 后进行一次内存碎片整理,但这会导致停顿时间变长,它还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在经过几次 Full GC 后才进行一次内存碎片整理。

7.G1收集器

G1 收集器是垃圾收集器理论进一步发展的参数,与 CMS 收集器相比有 2 个显著改进:一是 G1 是基于“标记-整理”算法实现的收集器,不会产生内存碎片,二是可以非常精确地控制停顿,既能让使用者明确指定一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。

G1 收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,能极力的避免全区域的垃圾回收,原理是它将整个 Java 堆划分为多个大小固定的独立区域,跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。

Docker 入门

Docker 是最近比较火的一种新型轻量级虚拟化技术,看了其官网的介绍后,其中的一些特性对我来说还是比较有吸引力的,所以在通读了入门教程后试用了一把,感觉还不错,所以写了这篇文章简单记录下。

首先,引用一下网上对 docker 的介绍——PaaS 供应商 dotCloud 开源了自有平台上的关键组件 Docker。Docker 是一种增加了高级 API 的 LinuX Container(LXC)技术,提供了能够独立运行 Unix 进程的轻量级虚拟化解决方案。它提供了一种在安全、可重复的环境中自动部署软件的方式”。从名字上就可以看出 dotCloud 是一家云计算公司,为了方便应用的部署,节省成本,开发出了 docker 这门技术。

下面说一下自己对 docker 的理解:docker 底层依靠的 Linux 原生 LXC 技术(lxc技术类似于浏览器中的沙盒,由内核直接负责其中应用的资源分配),只是在其上面又添加了一层 API,用户只需简单的调用某个 api 就能完成 lxc 的多个繁琐的步骤。docker 是一种轻量级的虚拟化技术,它不同于普通的虚拟机技术,通常虚拟机技术需要有一个 Hypervisor 也就是虚拟机监视器,它主要负责创建虚拟机、为虚拟机分配资源、回收资源等,而虚拟机运行的指令都要先转换为 hypervisor 的指令然后再由 hypervisor 调用操作系统 api 执行相应的功能,因此效率不高。而 docker 这是轻量级的,将文件系统和应用程序打包成一个镜像,运行时只载入文件系统,而其中的应用程序则单独运行,开多个“虚拟机”时,只运行其中的程序,多个程序间共享这个文件系统,每个“虚拟机”都作为一个进程运行,这样就好像一个本地程序一样,由操作系统负责资源的分配与释放,这样极大的降低了系统资源的消耗,并且启动、运行速度也得到了巨大的提高。下面就通过实际的操作来演示下docker的具体用法:

实验环境: CentOS 6.5, docker-io-0.8.1

需要注意的是CentOS需要安装EPEL源才能安装 docker,如果你使用的是其他 Linux 发行版,请直接到官网查找相应的安装方法,此处不再赘述。

第1步

和其他虚拟机类似,docker 也需要一个镜像文件才能启动虚拟机,所以首先我们要下载相应的镜像,例如你想跑个 Ubuntu,就要下载 Ubuntu 的镜像,想要跑 Fedora 系统就得下载 Fedora 的镜像,docker 官网为我们提供了诸多发行版的镜像,并且也有许多用户将他们自己制作的镜像上传到仓库中,通常这些镜像文件中都安装了不同用途的软件或运行环境,便于用户使用。当然,我们也可以制作自己的镜像并上传到官网上。我们可以访问http://index.docker.io 来查找需要的镜像,假如我们想要运行的环境是 CentOS,我们只需要搜索 ‘centos’ 即可列出所有符合条件的镜像,如下图所示:

从图中可以看出结果中有官方的镜像也有普通用户上传的镜像,用户自己上传的镜像的名字通常都以repository/image的格式显示,而官方的镜像则只有镜像名,所以上图中第一条结果是官方的镜像,第二条结果是用户的镜像。当然我们安装 docker 后也可以通过 docker 提供的命令来搜索镜像,如下图所示:

第2步 下载镜像。找到符合需求的镜像后我们就需要把它给下载下来,使用如下命令进行下载:

注意到图中最后一行包含了一串特殊的字符串:’539c0211cd76′,它代表了当前下载的这个镜像的唯一标识,并且以后我们对镜像所做的任何修改都会返回唯一的一个标识用于区分以前的镜像。

第3步 查看镜像。如果我们经常需要运行不同的 Linux 发行版,那么我们就会下载许多的镜像,这时候我们就需要查看到底机器中存在哪些镜像,有些过时或不用的镜像就可以进行删除以节省空间,所以使用下面的命令来查看、删除系统中存在的镜像:

从图中可以看出,首先使用docker images命令列出系统中所有的镜像,图中显示有 2 个不同的镜像(id 分别为 4535…和 539c02…),然后通过docker rmi image_id命令来删除镜像文件,会返回镜像的 id,最后再列一遍镜像,发现文件确实删除了。

需要注意的是,有时我们在移除镜像的时候通常会发生如下所示的错误:

从图中可以看出,当我们要移除 Image ID 为’d472a2307d5b’的镜像时,会报错’Error: image_delete: Conflict, d472a…. wasn’t delete’。这是什么原因呢?使用docker images –tree命令会列出每个镜像的依赖关系,从图中可以看出 ID 为1c0dc1cf…的镜像依赖于 ID 为d472a…的镜像,前面我们说过 Docker 是轻量级的,我们对镜像所做的每个修改都会以增量的形式保存起来,这里的增量就是一个个新的镜像,所以如果直接删除父镜像,那么无法保证子镜像能正常的跑起来,因此这种情况下只有先删除子镜像才行。

第4步 运行虚拟机。下载完镜像后我们就可以通过 Docker 来跑虚拟机了。这里用到命令是docker run,可以通过使用docker run -h来列出命令的详细使用方法,下面我们先简单的运行一下,如下图所示:

从图中可以看出,docker run命令后接 IMAGE ID 和命令参数。我们这个例子中就运行了 ID 为’1c0dc1cf8fc9’的镜像,镜像启动后执行命令echo “hello world。执行完后我们可以看到返回了’hello world’ 字符串。注意的是当命令运行完成后我们的“虚拟机”就停止退出了,此时的虚拟机称为“容器”,但容器的状态仍然被保存起来以备再启动,我们可以通过docker ps -a命令来查看所有的容器,如图所示:

从图中可以看出容器的状态:ID 为1db2…e30,使用的镜像是 CentOS,容器运行的命令是echo hello world等等。如果我们想重新运行下这个容器,则需运行命令docker start -i containerID即可,这里的 -i 选项指的是 interactive,把容器运行的结果返回到当前的 tty 中显示,此处不再演示。每调用一次docker run命令都会生成唯一ID的容器,所以一个镜像可以运行出多个容器,当容器运行完,对我们没有用时,就需要删除容器以节省空间,调用docker rm containerID来删除容器,此处不再赘述。还需要注意的是,如果想要删除某个镜像,而依赖此镜像的容器没有先删除的话也会出现错误。

有人可能会疑问,难道 Docker 的功能仅限于运行单条命令吗?命令一运行完容器就停止了,无法持久化运行程序吗?答案当然是肯定的,Docker 和别的虚拟机一样,别人能做的事 Docker 也能完成。下面就来看一下如何持久化运行某个程序,这对于像 web、email 等网络服务来说是最非常有用的。

从图中可以看出我们在运行ID为1c0d…8fc9的镜像时,使用了’-t -i’ 参数,分别用于分配 tty,将容器的 shell 与当前的 tty 相关联和进入交互模式,这样容器启动后我们就能获得容器的 shell 了(如图中我们看到容器的 shell 提示符’bash-4.1#’),然后我们就可以在容器的 shell 中运行各种程序,最后只需按 Ctrl+p, Ctrl+q 就可切换到宿主机上,此时运行docker ps可以看到容器一直在运行中。

好了,上面讲的是docker的一些基础的操作,还有一些细枝末节的知识,大家只要阅读官网的教程都很简单,比如启动、停止、重启容器用docker start/stop/restart命令等等。

下面讲一下 Docker 最吸引人的地方就是它可以将应用程序打包成镜像,分发到网络上,然后可以随时随地的获取运行。Docker 简化应用程序的部署,会极大提高效率,可以说的上是开箱即用。试想我们将来在服务器上将要部署 java、php、django 应用,这些应用的打包方式和运行环境都各不一样,系统管理员在部署时要耗费许多精力来配置其运行环境,再加上调试,几乎一天就不用干其他的事了,而 Docker 的出现正好解决了这个问题,我们只需把不同的应用分别封装到镜像里,而镜像的环境可以由第三方来配置好,我们只需下载下来简单的配置下环境就可通过 Docker 在服务器上跑起来,并且容器的状态可以随时被保存,对于系统备份和迁移也是及其方便的,所以 Docker这种轻量级虚拟技术将极大地简化部署、维护工作。下面我们就以配置一个 jdk7+tomcat7 的 Java web 运行环境为例,来演示下如何将应用打包,配置好容器的运行环境。

首先我们需要熟悉一下 Dockfile。什么是 Dockfile?在前面的例子中,我们从下载镜像,启动容器,在容器中输入命令来运行程序,这些命令都是手工一条条往里输入的,无法重复利用,而且效率很低。所以就需要一种文件或脚本,我们把想执行的操作以命令的方式写入其中,然后让 Docker 读取并分析、执行,那么重复构建、更新将变得很方便,所以 Dockfile 就此诞生了。

Docker 官网的教程对 Dockfile 的讲解很详细也很简单,这里只简单的介绍下几个常用的命令:

  • FROM 命令。用法:FROM :。FROM 命令告诉 Docker 我们构建的镜像是以哪个(发行版)镜像为基础的?,例如 FROM centos ,Docker 就会先 pull 下官方的 CentOS 镜像,然后在其上运行后续命令。注意,FROM 命令必须是 Dockfile 第一个非注释的命令。

  • RUN 命令。用法:RUN 。RUN 后面接要执行的命令,比如,我们想在镜像中安装 vim,只需在 Dockfile 中写入 RUN yum install -y vim,这样 Docker 在执行这条命令的时候就会在容器中调用 yum 命令从而安装相应的包。

  • ENV命令。用法:ENV 。ENV 命令主要用于设置容器运行时的环境变量,比如,我们将 JDK 安装到了 /home/user/jdk1.7 下,要在 Dockfile 中写入 ENV JAVA_HOME /home/user/jdk1.7,这样容器启动后就能运行 Java 应用了,当然还要把 JAVA_HOME 变量添加到 PATH 环境变量中才行,这在后边的例子中有演示。

  • ADD命令。用法:ADD 。ADD 主要用于将宿主机中的文件添加到镜像中,例如我们将下载好的 JDK 和 Tomcat 放到某一目录中,在 Dockfile 中写入 ADD /some/path/jdk /usr/loacl/jdk,这样 JDK 就被复制到镜像中 /usr/local/jdk 目录中了。

了解完这几个基本的命令后,我们就可以开始构建一套我们自己的 JDK+Tomcat 的运行环境了。例子比较简单,只给出 Dockfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pull down centos image
FROM centos
MAINTAINER myu myu@live.cn

#copy jdk and tomcat into image
ADD ./apache-tomcat-7.0.52.tar.gz /root
ADD ./jdk-7u51-linux-x64.tar.gz /root

#set environment variable
ENV JAVA_HOME /root/jdk1.7.0_51
ENV PATH $JAVA_HOME/bin:$PATH

#define entry point which will be run first when the container starts up
ENTRYPOINT /root/apache-tomcat-7.0.52/bin/startup.sh && tail -F /root/apacher-tomcat-7.0.52/logs/catalina.out

运行结果如下:

-t 选择指定生成镜像的仓库名和 tag

–rm=true 指定在生成镜像过程中删除中间产生的临时容器。

然后我们就可以运行新生成的容器了,如下图所示:

-d 指定容器运行后与当前 tty 分离,后台运行

-p 指定主机 80 端口与容器 8080 端口进行绑定

此时我们只要访问本地的80端口即可看到熟悉的 tomcat 的页面了。

因此按照这个思路,只要把程序一个个地添加到容器中,最后生成一个镜像就可以制作成一个隔绝依赖、开箱即用的应用了。

Hotspot JVM 常用选项

翻译自一篇外文,网上也可找到其他翻译版,但因翻译的不完全,因此自己翻译了一下,以备不时之需。

Sun官方 JDK 中带的 Hotspot JVM 有不计其数的启动参数,我们没法记住每个参数以及它们的作用,但作为开发人员,我们只需重点关注下与堆、垃圾回收以及远程调试相关的参数即可。但是我们也需要熟悉一下其他一些重要的参数,以便日后出问题时能快速的找到相关参数进行排错。下面让我们看一下一些常用的JVM参数。

首先要说明的是,JVM 参数根据传入的方式,通常可以被划分为两类,一类是通过 -X 选项进行传入的,另一类是通过 -XX 选项进行指定的:

1) 以-X开头的参数都是非标准选项,不能确保所有厂商的JVM都支持此参数,并且后续JDK版本如有更改,不另行通知。

2) 以-XX开头的选项都是不稳定的,不推荐经常使用,并且如有更改,也不另行通知。

重要的JVM参数:

1) 布尔型参数可以通过-XX:+选项进行开启或通过-XX:-选项关闭

2) 数值型参数可以通过-XX:=传入。数值中可包含’m’或’M’,’k’或’K’,’g’或’G’用于表示大小,例如32k等价于32768.

3) 字符串型参数通过-XX:=选项指定,通常这种类型的参数用于指定文件名、路径或一系列命令。

运行java -help命令,可打印出标准选项(即所有厂商的JVM都遵循的选项)。也可以通过运行java -X命令打印出当前JVM所支持的非标准选项。如果想要列出当前程序的运行参数,可以通过运行以下语句得到:ManagementFactory.getRuntimeMXBean().getInputArguments();