[ [ "1", "3", "0", "高效快速地加载 AngularJS 视图", "", "当AngularJS应用程序变大时,很多问题就开始显现出来了,比如多层级视图的加载问题,如果在子视图显示之前没有预加载,则可能在需要展示时,发生视觉闪烁的情况。这种问题在网络缓慢,或者服务器使用较慢的https连接时更容易出现。", "/uploads/article/thumb/2016071211354628.jpg", "高效快速地加载 AngularJS 视图", "JavaScript, AngularJS", "当AngularJS应用程序变大时,很多问题就开始显现出来了,比如多层级视图的加载问题,如果在子视图显示之前没有预加载,则可能在需要展示时,发生视觉闪烁的情况。这种问题在网络缓慢,或者服务器使用较慢的https连接时更容易出现。", "1", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468294546", "1476717356", ], [ "2", "3", "0", "如何定位 Node.js 的内存泄漏", "", "在 《一次 Node.js 应用内存暴涨分析》中,我们处理了一个 Node.js vm 引发的内存泄漏问题,处理过程也是比较艰辛。而在我们实际开发中,可能经常会碰到内存泄漏的问题,但很多情况下,我们对于这种问题的处理是有些迷茫的,没有一定的操作流程,效率比较低。虽然这种问题对于经验的要求比较高,但如果有一个简单的排查流程,还是会有一定帮助的。", "/uploads/article/thumb/201607121141407.png", "", "", "在 《一次 Node.js 应用内存暴涨分析》中,我们处理了一个 Node.js vm 引发的内存泄漏问题,处理过程也是比较艰辛。而在我们实际开发中,可能经常会碰到内存泄漏的问题,但很多情况下,我们对于这种问题的处理是有些迷茫的,没有一定的操作流程,效率比较低。虽然这种问题对于经验的要求比较高,但如果有一个简单的排查流程,还是会有一定帮助的。", "1", "0", "1", "admin", "3", "0", "1", "1", "0", "0", "0", "1", "0", "0", "0", "1468294900", "1476717356", ], [ "3", "1", "0", "Hack:用于HHVM的一种新编程语言", "", "", "/uploads/article/thumb/2016071212382956.jpg", "", "Facebook, hack, php", "", "1", "0", "1", "admin", "0", "0", "1", "1", "1", "0", "0", "1", "1", "0", "0", "1468298309", "1476717356", ], [ "4", "3", "0", "抛弃jQuery,拥抱原生JavaScript", "", "原生javascript", "/uploads/article/thumb/2016071212414099.png", "抛弃jQuery,拥抱原生JavaScript", "jquery,javascript,web", "", "1", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468298500", "1476717356", ], [ "5", "2", "0", "[JAVA · 初级]:GC-垃圾回收机制", "", "在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。", "/uploads/article/thumb/2016071212441669.jpg", "[JAVA · 初级]:GC-垃圾回收机制", "gc,垃圾回收,java", "在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。", "1", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468298628", "1476717356", ], [ "6", "1", "0", "PHP 7.0.2正式版发布:WordPress速度提升3倍!", "", "提到PHP,肯定会有人说这是世界上最好的编程语言。", "/uploads/article/thumb/2016071212472374.jpeg", "PHP 7.0.2正式版发布:WordPress速度提升3倍!", "php7,php,wordpress", "提到PHP,肯定会有人说这是世界上最好的编程语言。", "1", "0", "1", "admin", "1", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468298843", "1476717356", ], [ "7", "3", "0", "为何要学HTML5开发?HTML5发展前景如何?", "", "", "", "为何要学HTML5开发?HTML5发展前景如何?", "html,html5", "", "1", "0", "1", "admin", "1", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468298894", "1476717356", ], [ "8", "2", "0", "Spring知识点提炼", "", "spring框架", "/uploads/article/thumb/2016071212525610.jpg", "Spring知识点提炼", "spring,java", "spring框架", "1", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "0", "1", "0", "0", "0", "1468299176", "1476717356", ], [ "9", "3", "0", "CSS代码重构与优化之路", "", "着项目规模的增加,项目中的CSS代码也会越来越多,如果没有及时对CSS代码进行维护,CSS代码不断会越来越多。", "/uploads/article/thumb/2016071212553820.jpeg", "CSS代码重构与优化之路", "css,重构", "着项目规模的增加,项目中的CSS代码也会越来越多,如果没有及时对CSS代码进行维护,CSS代码不断会越来越多。", "1", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468299338", "1476717356", ], [ "10", "1", "0", "PHP分页技术的代码和示例", "", "分页是目前在显示大量结果时所采用的最好的方式。", "/uploads/article/thumb/2016071212580919.png", "PHP分页技术的代码和示例", "php,分页,php分页", "分页是目前在显示大量结果时所采用的最好的方式。", "1", "0", "1", "admin", "1", "0", "1", "1", "1", "0", "0", "0", "0", "0", "0", "1468299489", "1476717356", ], [ "11", "1", "0", "10个免费下载PHP脚本的网站", "", "免费的PHP脚本下载", "/uploads/article/thumb/2016071213000120.png", "10个免费下载PHP脚本的网站", "php脚本,下载,代码下载", "免费的PHP脚本下载", "1", "0", "1", "admin", "4", "0", "1", "1", "0", "0", "1", "0", "0", "0", "0", "1468299601", "1476717356", ], [ "12", "1", "0", "趣文:如果编程语言是女人(新编版)", "", "语言趣文", "/uploads/article/thumb/2016071213020658.png", "趣文:如果编程语言是女人(新编版)", "java, Javascript, Lisp, php, Python, Ruby, 编程语言, 趣文", "语言趣文", "1", "0", "1", "admin", "0", "3", "1", "1", "0", "0", "0", "1", "0", "0", "0", "1468299726", "1476717891", ], [ "13", "3", "0", "浏览器缓存机制", "", "浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires; Cache-control等)。", "/uploads/article/thumb/2016071213054793.jpg", "浏览器缓存机制", "缓存,浏览器缓存,http协议", "浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires; Cache-control等)。", "1", "0", "1", "admin", "1", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468299947", "1476717356", ], [ "14", "3", "0", "JavaScript 统治的世界,烤面包机将能运行 JS 了", "", "从浏览器到手机,从平板电脑到桌面电脑,从工业自动化到最小的微控制器——最近JavaScript 似乎蔓延到了最意想不到的地方,不远的将来,你的烤面包机也将会运行 JavaScript ……。但是为什么?", "", "JavaScript 统治的世界,烤面包机将能运行 JS 了", "javascript,流行", "从浏览器到手机,从平板电脑到桌面电脑,从工业自动化到最小的微控制器——最近JavaScript 似乎蔓延到了最意想不到的地方,不远的将来,你的烤面包机也将会运行 JavaScript ……。但是为什么?", "1", "0", "1", "admin", "1", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468300042", "1476717356", ], [ "15", "2", "0", "给Java说句公道话", "", "有些人问我,在现有的语言里面,有什么好的推荐?我说:“Java。” 他们很惊讶:“什么?Java!” 所以我现在来解释一下。", "/uploads/article/thumb/2016071213095350.jpg", "给Java说句公道话", "java", "有些人问我,在现有的语言里面,有什么好的推荐?我说:“Java。” 他们很惊讶:“什么?Java!” 所以我现在来解释一下。", "1", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "1", "0", "0", "0", "0", "1468300193", "1476717356", ], [ "16", "2", "0", "Java编程入门(1.6):现代用户界面", "", "算机刚问世时,普通人——包括大多数程序员——都不允许靠近计算机。", "/uploads/article/thumb/2016071214100948.jpg", "Java编程入门(1.6):现代用户界面", "java,用户界面", "算机刚问世时,普通人——包括大多数程序员——都不允许靠近计算机。", "1", "0", "1", "admin", "1", "0", "1", "1", "0", "0", "1", "0", "0", "0", "0", "1468300356", "1476717356", ], [ "17", "3", "0", "精简页面的样式文件,去掉不用的样式", "", "精简css样式", "", "精简页面的样式文件,去掉不用的样式", "css,样式", "精简css样式", "1", "0", "1", "admin", "6", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468300509", "1476717356", ], [ "18", "2", "0", "Java编程入门:前言", "", "Java编程入门》是一本使用Java作为入门语言的免费计算机编程课本", "/uploads/article/thumb/2016071213165538.jpg", "Java编程入门:前言", "java,入门,编程书籍", "Java编程入门》是一本使用Java作为入门语言的免费计算机编程课本", "1", "0", "1", "admin", "0", "0", "1", "1", "1", "0", "0", "0", "0", "0", "0", "1468300615", "1476717915", ], [ "19", "2", "0", "Java 8最快的垃圾搜集器是什么?", "", "OpenJDK 8 有多种 GC(Garbage Collector)算法,如 Parallel GC、CMS 和 G1。哪一个才是最快的呢?如果在 Java 9 中将 Java 8 默认的 GC 从 Parallel GC 改为 G1 (目前只是建议)将会怎么样呢?让我们对此进行基准测试。", "/uploads/article/thumb/2016071213182356.jpg", "Java 8最快的垃圾搜集器是什么?", "java,java8,垃圾收集", "OpenJDK 8 有多种 GC(Garbage Collector)算法,如 Parallel GC、CMS 和 G1。哪一个才是最快的呢?如果在 Java 9 中将 Java 8 默认的 GC 从 Parallel GC 改为 G1 (目前只是建议)将会怎么样呢?让我们对此进行基准测试。", "1", "0", "1", "admin", "33", "0", "1", "1", "1", "0", "0", "1", "0", "0", "0", "1468300703", "1476717356", ], [ "20", "2", "0", "使用Memcached改进Java企业级应用性能(1):架构和设置", "", "Memcached由Danga Interactive开发,用来提升LiveJournal.com网站性能。Memcached分布式架构支持众多的社交网络应用,Twitter、Facebook还有Wikipedia。在接下来的两部分教程中,Sunil Patil介绍了Memcached分布式哈希表架构,以及利用它帮助你为数据驱动Java企业应用做数据缓存。", "/uploads/article/thumb/201607121325288.png", "使用Memcached改进Java企业级应用性能(1):架构和设置", "java,memcached", "Memcached由Danga Interactive开发,用来提升LiveJournal.com网站性能。Memcached分布式架构支持众多的社交网络应用,Twitter、Facebook还有Wikipedia。在接下来的两部分教程中,Sunil Patil介绍了Memcached分布式哈希表架构,以及利用它帮助你为数据驱动Java企业应用做数据缓存。", "1", "0", "1", "admin", "1", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468300831", "1476717356", ], [ "21", "2", "0", "JVM的相关知识整理和学习", "", "诺依曼体系结构中,指出计算机处理的数据和指令都是二进制数,采用存储程序方式不加区分的存储在同一个存储器里,并且顺序执行,指令由操作码和地址码组成,操作码决定了操作类型和所操作的数的数字类型,地址码则指出地址码和操作数。", "/uploads/article/thumb/2016071213203123.jpg", "JVM的相关知识整理和学习", "jvm,java", "诺依曼体系结构中,指出计算机处理的数据和指令都是二进制数,采用存储程序方式不加区分的存储在同一个存储器里,并且顺序执行,指令由操作码和地址码组成,操作码决定了操作类型和所操作的数的数字类型,地址码则指出地址码和操作数。", "1", "0", "1", "admin", "0", "1", "1", "1", "1", "0", "0", "0", "0", "0", "0", "1468300831", "1476717356", ], [ "22", "2", "0", "关于Java集合的小抄", "", "在尽可能短的篇幅里,将所有集合与并发集合的特征,实现方式,性能捋一遍。适合所有”精通Java”其实还不那么自信的人阅读。", "/uploads/article/thumb/2016071213224495.jpg", "关于Java集合的小抄", "java,java集合", "在尽可能短的篇幅里,将所有集合与并发集合的特征,实现方式,性能捋一遍。适合所有”精通Java”其实还不那么自信的人阅读。", "1", "0", "1", "admin", "0", "2", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468300964", "1476717385", ], [ "23", "0", "2", "关于我们", "about", "", "", "", "", "", "1", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468309252", "1476717356", ], [ "24", "0", "2", "联系方式", "contact", "", "", "", "", "", "1", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468309318", "1476717356", ], [ "25", "0", "0", "dfdf", "dfd", "fsafsdfsdf", "", "", "", "", "0", "0", "1", "admin", "0", "0", "1", "1", "0", "0", "0", "0", "0", "0", "0", "1468898361", "1476717356", ], ], 'article_content' => [ [ '1', '1', '
本文将讨论更高效加载AngularJS视图的系统方法。
AngularJS视图也并不是什么特别神奇的技术,在其内部就是按普通的directive来处理的。也就是说,当一个位置需要显示view时,AngularJS会尝试使用某种方法获得其HTML模板文件的具体内容、包装成directive,执行directive的标准流程,最后添加到页面上。

回想一下,directive本身是不是正好也支持templateUrl属性?这就与view技术衔接上了。
这样说来,是不是视图模板也可以使用行内DOM甚至是字符串字面量值了呢?答案是肯定的!我们本来就可以使用一段行内DOM来作为view的模板。例如:

当然,作为一个大型的AngularJS应用程序,将所有view都放在字符串值里,或者行内DOM里是不太现实的,我们希望可以使用多个小的HTML文件来作为子模板。这样,虽然整个应用很大,但每个子模板的文件并不大,一般都是几KB的小文件,当用户点击到指定位置,需要时使用对应界面的模板时再去加载,也就显著提高了效率。
我们可以用下图来表示“行内DOM”与“多个子模板文件”的性能对比:

上面提到了“多个子模板文件”的模板组织方式,这本是一件很平常、很自然的工作方式而已。也正是因此,才让人们感觉AngularJS工作方式与自己的期望的一致:因为在没有使用AngularJS之前,人们在开发一个 Web应用时,页面就是这样一个个组织的。
即使在以前,我们在提到性能的时候,自然会想到“缓存”。在以前,页面与页面之间的跳转使得每个页面都是相互独立的单位,因此页面内容的缓存只能有赖于浏览器了。而今,AngularJS让所有页面子模板都在“单页应用”中加载,于是,我们在这个单页面应用内便获得了缓存页面内容的机会。
AngularJS中内建了缓存机制templateCache:只要已经加载过某个页面子模板,就会在templateCahce中缓存起来,下次从服务器加载页面模板之前,先检查templateCache,如果已有缓存则不需要从服务器上加载,直接使用。

AngularJS中内建了templateCache 机制之后,加载视图的过程变得高效而轻松,Web应用本身,以及开发者都不需要关心这一过程。不过,即使有页面内的templateCache,页面模板在初次使用时还是需要从服务器加载,因此偶尔能见到一些视觉闪烁的情况,比如标签切换、页面跳转等。
作为一种优化手段,我们很自然能想到,既然页面能够在加载之后在templateCache起来就能提高性能,如果在应用启动之初templateCache中就有了所有页面的缓存,也就根本不需要服务器了,那么在页面需要显示时,也就基本不需要加载时间了。图可以变成这样:

要实现这一目标,只需要在发布应用之前,构建额外的templates.js 文件,在其中将所有的页面模板读取出来并提前put到templateCache中,再将形成的templates.js嵌入到应用中即可在Web应用启动时就已经拥有所有页面模板内容的缓存版本了。
不过,对于大型AngularJS Web应用来说,我们很快发现一个问题:这个templates.js文件本身的体积迅速大了起来,它又会成为一个新的性能问题。
于是,我们可以使用另一个已有的经验:“异步加载”。有了异步加载的支持,在加载templates.js 的请求还没有完成之前,可以“降级”使用AngularJS内建的机制,而一旦templates.js加载完成,就立即拥有了所有模板的缓存。

理想中,templateCache最好能达到最佳的性能表现,但实际应用中,如果不加优化,templates.js文件本身的体积会令这种优化效果有所折扣,而加上异步加载 templates.js和降级到逐个加载单个htm模板文件之后,又有了一些改善。
现在再来讨论一下浏览器缓存,可以结合上一节的templates.js一起来讨论了。浏览器缓存是浏览器里内置的一种缓存功能,当服务器正确配置了对htm和js文件的缓存支持时,浏览器将按指示缓存这些文件。不管是对一个个htm模板,还是对templates.js,都可能被缓存。
也就是说,只要在服务器上正确配置,那么上一节所述的“异步 templates.js”,以及“降级的多个htm模板文件”都可以被浏览器缓存。这样,我们将加载htm模板文件和templates.js的需求都减少到第一次使用应用之时。
但在服务器上配置缓存也需要谨慎,如果配置不当,就会出现当服务器上文件已经更新,但客户端浏览器仍在使用老的缓存版本的问题。由于AngularJS应用使用绑定表达式显示界面,因此如果程序已经更新,而视图还是老版本,那么绑定表达式很可能失效。这种情况下,轻则局部界面错乱,重则整个Web应用完全无法使用。

浏览器缓存原本是一个“杀手锏”,不管是只使用单个模板文件,还是使用templateCache,浏览器缓存都可以极大地改善其性能效果。但一旦缓存配置不当致使客户端浏览器里使用了错误的版本,就直接导致应用错误,更不谈性能表现了。
要处理缓存问题也有成熟的经验可供借鉴:也就是在文件名上使用版本号,每次需要更新文件内容时,同时更改版本号,那么整个文件名也就发生变化,也就不会发生缓存版本错误问题。结合上面的论述,我们在templates.js 上添加上版本号,另一方面配置AngularJS,在加载单个htm模板文件时,也会在请求上附上版本号,即可解决这一问题。当然,我们希望在开发时,标记要使用的视图模板时,不需要指定这个需要经常变化的版本号,从而最大程度地保障开发体验,并将维护成本降到最低。

上面讨论了AngularJS视图各种可能的方式,分别实施的方法,以及其性能表现差异。主要值得关注的是经优化的templateCache机制,以及结合浏览器缓存的templateCache方法。总结来说,可以形成这样一个更直观的图形:
经过一番努力,最终我们能够达到这样的结果:

在应用里添加仅在生产环境才生效的策略:支持在加载视图模板文件时在文件名中添加版本号(从页面中templates.js的文件路径中分析版本号);
开发时不需要经过改变;
发布时预读取所有模板的内容,并生成带版本号的templates.js,嵌入应用页面中;
在服务器上配置所有htm模板文件及templates.js的缓存策略为“允许缓存”;
用户首次使用应用时,集中所有网络带宽加载AngularJS基础脚;本,以及应用程序业务逻辑系统,令应用程序尽早能够使用;此时应用使用htm模板文件作为视图模板;
异步加载templates.js;加载完成之后应用开始使用页面内模板缓存;
用户再次使用应用时,从浏览器缓存中加载templates.js;
再次发布应用时,修改templates.js 文件名中的版本号,嵌入页面中。
所以,在首次用户使用应用时,其网络加载图形就像这样:

最先加载的是应用程序AngularJS框架本身,以及业务逻辑,这时候应用已经可用;此时再异步去加载templates.js文件。事实上,上面的图形即是我们实际项目中的状况,具体实现在这里就不贴了,也欢迎读者一起探讨更多的可能性。
从本文的讨论不难看出,只要通过各种方法,好好管理浏览器的加载行为,形成一个系统方法,便能令视图加载的性能表现变得更好。
这里简单整理一个流程,欢迎一起探讨,补充。
Node.js 进程的内存管理,都是有 V8 自动处理的,包括内存分配和释放。那么 V8 什么时候会将内存释放呢?
在 V8 内部,会为程序中的所有变量构建一个图,来表示变量间的关联关系,当变量从根节点无法触达时,就意味着这个变量不会再被使用了,就是可以回收的了。
而这个回收是一个过程性的,从快速 GC 到 最后的 Full GC,是需要一段时间的。
另外,Full GC 是有触发阈值的,所以可能会出现内存长期占用在一个高值,也可以算是一种内存泄漏,可以从《一次 Node.js 应用内存暴涨分析》中找到例子。还有一种就是引用不释放,导致无法进入 GC 环节,并且一直产生新的占用,这一般会发生在 Javascript 层面。
所以,定位内存泄漏问题,一般方案就是找那些不被使用又不会被释放的变量,处理了这些变量,问题一般就可以解决了。如果是 Node.js 底层变量不释放,除了提交 issue 等待解决外,只能通过优化启动参数来解决。
工欲善其事必先利其器,在排查时,我们还是需要一些工具来帮忙的。
这个是今年初出的 Node.js 调试工具,基于 Electron 将 Node.js 和 Chromium 的功能融合在了一起。操作起来比 node-inspector 方便,开放的 Timeline 功能还是比较实用的,虽然不是实时显示。
仅需要 devtool xxx.js,还可以通过 .devtoolrc 来进行参数定制,具体见 GitHub
这个是比较传统的定位内存泄漏的组合。heapdump 可以直接在代码中调用生成内存快照,然后将快照文件导入到 chrome devTool 进行分析,之后操作其实和前者就差不多了。不过,这个方案和前者有一点区别就是,前者实际还是在浏览器环境中,所以生成的内存快照会有一些 DOM 对象的存在,会有一定的干扰。而这个方案,是直接调用底层 V8 的方法,生成的快照只有 Node.js 环境中的对象。
这个可以在代码里直接使用,实时检测内存动态,当发生内存泄漏的时候,会触发 ‘leak’ 事件,会传递当前的堆状态,配合 heapdump 有奇效。详见 memwatch。
对于垃圾回收,V8 引擎有很复杂的逻辑来决定什么时候进行回收。很多时候,当我们发现 Node.js 进程所使用的内存快速增长的时候,并不能确定是否是内存泄漏导致的,很有可能是程序设计问题,导致内存的不合理利用。只有当垃圾回收触发,未使用内存被释放后,内存增长还在持续,我们才能确定是发生了内存泄漏。
隐藏的内存泄漏问题,大多是有触发条件的,重现问题是需要这些条件的,所以我们在平时写代码的时候,可以将一些重要环节的参数细节打印在 log 中,这样我们在重现问题是就不会摸不着头脑,乱试一气。
有了参数可以用来重现问题,接下来要确定问题。我们要确定,这部分内存是否没有被 GC 正确释放。那么问题来了,我们如何知道程序进行了垃圾回收呢?很显然,等待并不是办法,我们要主动。
在 Node.js 的启动参数中,提供了暴露手动调用 GC 方法的参数,即 --expose-gc。我们用这个参数来启动应用后,就可以在代码中调用 global.gc() 手动触发垃圾回收操作。同时,使用 process.memoryUsage().heapUsed 获取进程运行时所占用的内存。如果 GC 之后,内存依然没有下降,就可以确定是内存泄露了。
既然内存是问题,我们就需要获取程序运行的内存快照来帮助定位问题。但内存快照并不是随便打得,是有一定技巧的。
我们至少要生成三次内存快照,才能更好的定位问题。这三次中又一次要在问题出现前生成,之后可以在问题持续的过程中生成两次或更多。
为什么要这样做呢?理解起来很简单。第一次是为了获取正常情况下的堆栈信息,而在问题出现后,堆栈信息一定会发生变化,有了第一次的信息,我们才好进行后面的比对,过滤一些无用的信息。而后两次的快照,用来比对某一对象的堆栈变化,来确定是否是有问题的对象。下面会详细应用到。
用 devTool 的可以忽略下面的过程:
打开 Chrome Devtools ,进入到 Profiles 选项卡,点 Load 按钮,加载之前生成的快照。
对于内存快照,有四个视图,Summary,Comparison,Containment,Statistics,这里面常用的是前三个。
在 Summary 视图中,我们可以看到当前快照的全部信息,以及多个快照之间的信息。在列表里显示的都是对象的构造函数名字,可以先忽略被括号包裹的对象,优先观察其他的对象,最后再来看他们。后面的 shallow size 表示的是对象自身的大小,retained size表示的是对象和它依赖对象的大小,一般是 GC 不可达的。
在 Comparison 视图中,我们可以进行多个快照之间的对比,这个用处比较大,如果我们将前两次快照进行对比,可能比较快速的定位出问题的对象。注意观察 New、Deleted、Delta,如果是内存泄漏的对象,可能是一直在 New,而没有 Deleted。
在 Containment 视图中,我们可以查看整个 GC 路径,当然一般不会用到。因为展开在 Summary 和 Comparison 列举的每一项,都可以看到从 GC roots 到这个对象的路径。通过这些路径,你可以看到这个对象的句柄被什么持有,从而定位问题产生的原因。值的注意的是,其中背景色黄色的,表示这个对象在 Javascript 中还存在引用,所以可能没有被清除。如果是红色的,表示的是这个对象在 Javascript 中不存在引用,但是依然存活在内存中,一般常见于 DOM 对象,它们存放的位置和 Javascript 中对象还是有不同的,在 Node.js 中很少遇见。
更多的操作方法,可以看这个视频 Memory Profiling with Chrome DevTools 和 Memory Management Masterclass。还有 Chrome 的文档 Memory Profiling(旧) 和 Memory Diagnosis(新)。讲的还是很详细的。(请自备梯子)
一般在 Javascript 中存在引用而导致内存泄漏的情况,是比较好处理的,只需要在使用后及时的将引用释放掉即可。
但像 《一次 Node.js 应用内存暴涨分析》 所存在的那种内存问题,是属于底层机制的问题,如果等不了 bugfix,就只能先通过一些启动参数来优化内存管理。常用的参数:
--max-old-space-size 限制老生区大小,可以控制内存占用的最大值,即使发生泄漏,也不会让内存占用保持很高。可以根据开启进程数以及是否同机部署来优化。
--gc_global 这其实是个 V8 的 debug flag,让 GC 永远都是 Full GC,使用上会有一定的性能损耗,根据应用复杂度不同,损耗不同。
当我们找到问题,进行修复后,重复上面的步骤,确认问题已经被解决。有时可能一次并不能解决问题,所以耐心还是很重要的。
可以在这里下载使用到的代码, GitHub,进入 memory-leak 文件夹。
我们来举个例子,应用上面的步骤排查问题,使用 leak-memory 的例子,代码还有另外一个例子,可以自己实践。
这里我们为了方便,我们使用了 devTool。
devTool leak-memory.js
然后在打开的界面中进入内存快照界面,生成第一次快照。当控制台有输出后,间隔的生成两次快照,结果如下。

我们切换视图,对比下三次快照间的区别,可以看到 Foo 这个对象一直在创建而没有被删除。


我们展开 Foo,选择下面的一个实例,查看它的 GC path,可以看到它一直被 neverRelease 持有引用(黄色),所以没有被释放,之后就可以进行问题的处理了。

去掉 // neverRelease.splice(index, 1); 前的注释,然后在重复上面的步骤,你会发现内存的变化已经正常了。
在使用 devTool 时,可以查看运行时的 memory timeline,如果图像呈现阶梯式增长,一般就是存在内存泄漏问题了。正常的应用曲线会类似于锯齿,如图:

内存泄漏问题的定位,经验很重要,但有了良好工具的辅助,可以节省很多时间。如果懒得自己一步步的操作,可以接入 alinode,这个可以帮助你很方便的生成快照等运行时数据,并有一定的分析辅助,还是方便的。
你可能看到很多内存分析的文章会有一些图来表示内存的增长,可以使用 python 来快速生成相关的图片,使用 matplotlib.pyplot 这个包。

想急切尝试Hack?传送门:http://hacklang.org/
今天我们发行了Hack,一门能够在HHVM上与PHP无缝交互的编程语言。Hack的静态类型兼顾及协调了PHP的快速开发周期。同时增加了在其他现代编程语言中常见的许多功能。
我们已经在Facebook上部署了Hack,并且取得了巨大的成功。在过去的几年里,借助自家开发的一些重构工具,我们几乎将有的PHP代码迁移到了Hack上。
我们也很自豪地向外发布一个开源版本的Hack,作为我们的 HHVM runtime 平台的一部分,它现在同时支持Hack和PHP。
【补充信息】:HipHop for PHP是一系列PHP脚本语言的程式码转换器的集合,它包含HPHPc、HPHPi、HPHPd以及HHVM,这四个脚本引擎各有所不同,但是他们共用相同的执行时期(Runtime)及工具集(Toolset)。HipHop是由Facebook所建立,他们用它来节省服务器的资源。HipHop 由 C++ 和 C 语言所编写,发布时代码量已高达60万行,它以自由软件发布,采用PHP许可证3.01版。(摘自维基百科)

每一个PHP程序员对于每天棘手又笨重的任务都很熟悉。上面的代码是一个常见错误,其中一个方法可能意外地被一个空的对象所调用,从而导致难以发现的错误,直到运行时报错。另一个例子是一个复杂的API,虽然开发者对于它的语义有直观的理解,但仍需花时间在文档中查找方法名。
在Facebook这样的规模——成千上万的工程师每天要发布两次新代码——这样的开发速度变慢将更成问题。在没有Hack之前,我们有一个能够快速反馈回溯的简单语言——但我们如何能减轻各种各样的上述问题?早期的错误检测和快速迭代是否能共存,同时还能保证在PHP上投入的精力有所回报?是否能够改善代码分析和帮助审查使开发者更具生产力,就像有了一个自动化完成工具?
传统上,动态类型语言允许快速开发,但会牺牲及早发现错误并迅速审查代码的能力,特别是在大的代码库。相反,静态类型语言提供更多的安全网,但往往增加快速迭代的成本。我们相信必须有一个平衡点。
因此,Hack就诞生了。我们相信,它提供了最好的动态类型和静态类型语言各有的特性,而且,它也适合各种规模的项目。
Hack和PHP有着很深的根源。实际上,许多PHP文件已经是合法的Hack文件。我们有意识地选择放弃那些与静态语言不兼容的少数便捷但将被被弃用的功能和特性。(例如:可变变量和extract()函数)。我们已经增加了许多新的特性,且这些特性能够使开发者们更具生产力。
我们新增的最主要的特性是支持静态类型。我们已经开发了一个系统来标注函数签名和类成员的类型信息,我们的类型检查算法(“类型检查器”)能够推断出来。类型检查是增量的,这样,即使在同一个文件中的一些代码可以转换为Hack,而其他部分保持动态类型。从技术上讲,Hack是一个“渐增类型”语言:动态类型代码和静态类型代码无缝地交互。
在Hack的类型系统中,我们引入了几个特性,例如泛型、可空类型、类型别名,和类型参数约束。这些新的语言特性是不显眼的,所以你写的Hack代码将仍然看起来像是使用动态语言的PHP程序员所编写的。
然而,Hack增加了额外的特性,超越静态类型检查,包括集合,lambda表达式和运行时强制约束返回类型和参数类型。
集合提供了一个替代PHP数组的简洁的,类型安全的数据结构。我们设计它专门与静态类型和泛型一起工作。集合API提供了许多经典的高阶功能,如map()和filter()用于适应函数式编程风格。
Lambda表达式给出一个简洁的语法来创建闭包。虽然PHP也有闭包,但它要求程序员显示地声明它们所要使用到的外部变量。使用Hack的lambda表达式,它会自动推断出这些用途,节省您不必要的工作。Lambda表达式使你能够更方便地充分利用集合的API。
运行时强制约束返回类型和参数类型(包括标量类型,如int和string)提供更高的安全性,胜过静态的代码审查,同时类型标注也会逐渐加入到代码库中。运行时强制约束帮助程序员发现和诊断某些类型的问题更加容易,并且为了优化,它可以帮助HHVM的JIT生产更高效的代码更加安全,通过对类型标注的信任。
在开发过程中,一个PHP程序员通常会来来回回快速地在源代码和浏览器之间切换。工程师们可以进行尽可能地快速迭代,测试和调优实验,直到代码运行完美。
传统上,一种类型的检测器会破坏这种反馈环,因为它需要时间去分析源代码。我们不想减缓PHP的工作流程,所以我们想出了一个新的方法来协调类型安全的即时反馈。
我们的解决方案是将类型检查器构建为监视文件系统的本地服务器。服务器在内存中保存有关源代码的所有信息,并且当磁盘上的文件改变时会进行自动更新。这种做法已见成效:类型检查器通常运行在不到200毫秒,很少需要超过一秒钟,因此很容易集成到开发流程,而不会引入明显的延迟。
Hack的类型安全和重构随着在代码库的增加会带来更多的好处。需要明确可能对于一些代码很难完整地迁移到Hack上,对于我们更重要的是在增量引入Hack代码时,能够直接与现有的PHP代码共存。
迁移过程的其余事项,如添加类型注释,并使用新的语言特性,可以在代码库中适当的修改。例如一个类型标注能够在一个函数上添加,而另一个函数没有,即使在同一个文件中。如果一个函数的参数或者类的成员属性没有明确的类型标注,类型检查器会认为它的类型是动态的,并且它不检查该值的类型。
在Facebook,我们发现,我们的工程师们赞赏Hack,以至于他们开始自愿迁移大部分自己的代码。在我们的代码库有着数百万行的代码,我们也希望能够将一些工作自动化地执行,所以我们开发和使用了几个代码修改工具来帮助迁移过程(这些工具已经作为Hack的一部分发布)。
HHVM仍然是一个PHP运行平台,我们打算继续保持这种方式。事实上,我们正努力达到PHP-5的标准。其中HHVM的首要任务是能够运行未经修改的PHP 5的源代码,不仅是为了社区,也因为我们依赖许多第三方PHP底层库。
HHVM现在是一个能同时运行PHP和Hack的平台,所以你可以逐渐体验Hack所带来的新特性。
我们很高兴能够开源Hack,和自动转换代码库的工具。这仅仅是第一步,我们致力于继续发展此软件,使我们自己的工程师和更广泛的社区能够更加容易地进行开发。Hack的价值不局限于大项目:拥有类型信息,非常棒的错误提示,以及快速地反馈等特性,小的代码库也能享受到Hack的好处。
下个月,我们将在Menlo Park的Facebook校园举办Hack Developer Day来介绍Hack,我们希望在那里能看到你或者在线上。
我们很想听听您对我们工作的反馈,并欢迎大家加入HHVM和Hack社区。
在这里有许多人对Hack的开发贡献了一份力量。
Hack核心团队由 Joel Beales、Eugene Letuchy、Gabriel Levi、Joel Marcey、Erik Meijer、Alok Menghrajani、Bryan O’Sullivan、Drew Paroski、James Pearce、Joel Pobar 和 Joshua Van Dyke Watzman组成。
特别感谢我们的社区早期采用者提供宝贵的反馈意见:James Miller、Simon Welsh、Nils Adermann、Fabien Potencier 和 Alexander Mols。
Hack主要是用OCaml语言编写开发的。我们也同样感谢Gallium团队发展了OCaml语言,和Ocsigen团队(CNRS – University of Paris Diderot – INRIA)开发的js_of_ocaml。
当然,感谢每一位帮助过Hack发展的人。这份名单无法在博文中详细列出,但是你知道你所贡献的一切。
前端发展很快,现代浏览器原生 API 已经足够好用。我们并不需要为了操作 DOM、Event 等再学习一下 jQuery 的 API。同时由于 React、Angular、Vue 等框架的流行,直接操作 DOM 不再是好的模式,jQuery 使用场景大大减少。因此我们项目组在双十一后抽了一周时间,把所有代码中的 jQuery 移除。下面总结一下:
1. 模式变革
jQuery 代表着传统的以 DOM 为中心的开发模式,但现在复杂页面开发流行的是以 React 为代表的以数据/状态为中心的开发模式
应用复杂后,直接操作 DOM 意味着手动维护状态,当状态复杂后,变得不可控。React 以状态为中心,自动帮我们渲染出 DOM,同时通过高效的 DOM Diff 算法,也能保证性能。我们在 React 应用实践中也发现,大部分当你想直接操作 DOM 的时候,就意味着你可能做错了。
2. 不支持同构渲染
重构就是前后端运行同一份代码,后端也可以渲染出页面,这对 SEO 要求高的场景非常合适。由于 React 等流行框架天然支持,已经具有可行性。当我们在尝试把现有应用改成同构时,因为代码要运行在服务器端,但服务器端没有 DOM,所以引用 jQuery 就会报错。这也是要移除 jQuery 的迫切原因。同时不但要移除 jQuery,在很多场合也要避免直接操作 DOM。
3. 原生 API 足够好用
由于浏览器的历史原因,曾经的前端开发为了兼容不同浏览器怪癖,需要增加很多成本。jQuery 由于提供了非常易用的 API,屏蔽了浏览器差异,极大地提高了开发效率。这也导致很多前端只懂 jQuery。其实这几年浏览器更新很快,也借鉴了很多 jQuery 的 API,如 querySelector,querySelectorAll 和 jQuery 选择器同样好用,而且性能更优。
4. 性能
前端开发一般不需要考虑性能问题,但你想在性能上追求极致的话,一定要知道 jQuery 性能很差。原生 API 选择器相比 jQuery 丰富很多,如 document.getElementsByClassName 性能是 $(classSelector) 的 50 多倍!

测试链接:http://jsperf.com/jquery-vs-native-api
English Poets of the Eighteenth Century
5. 时机成熟
差的浏览器(IE)已经淘汰的差不多了。
If We Didn’t Spend So Much on IE Support, We Could Be Taking Vacations on Mars
Christian Alfoni
我们的主打产品现在有千万用户,因为我们一直引导用户升级浏览器,上个月统计 IE 9 以下用户只占不到 3%。但为了这 3% 的用户我们前端开发却增加了很多工作量,也限制了我们升级我们的架构,因此放弃支持他们利大于弊。当然这要根据产品来定,比如这是百度统计的国内浏览器占有率,IE8 竟然还有 22%。有些产品可以为了保证用户体验,在旧的浏览器上投入很大成本,甚至做到了极致。其实我觉得产品更应该做的是引导用户升级浏览器。微软也宣布 2016年1月12号停止支持 IE 11 以下浏览器,继续使用旧浏览器就会有安全风险,我们更应该主动引导,只要产品有足够吸引力,大部分用户升级并不困难。

数据来源 百度统计
下面是国际上 IE 占有率,IE8 已经跌出前 10,IE 11 比较多,还好支持他们并不难。

数据来源 W3 Counter
1. 替换代码
移除 jQuery 可以很顺利,我们把整个过程详细整理了,并开源。
打开 https://github.com/oneuijs/You-Dont-Need-jQuery 对 API 查找替换即可。
同时我们简单封装了一些方法:
oui-dom-utils 来做选择器和样式相关
oui-dom-events 来做 Event,支持命名空间和事件代理
刚去了 jQuery 又引了新的库,这不是玩我吗??其实以上两个库很简单,只是常用方法的简单封装,建议你看一下代码。你当然可以不用。
以上的库都用于我们的生产环境,我们会长期维护,保证更新。
2. 旧浏览器自动跳转
代码替换后,当用户用旧浏览器打开时,你还要做一个跳转,把用户定位到提示页面,提示用户下载升级浏览器。IE9 以下浏览器都支持条件判断语句,可以在 </head> 标签结束前添加如下代码做自动跳转
<!–[if lte IE 9]>
<script>if (!/update\.htm/.test(location.href)) window.location = ‘//abc.com/update.htm’; </script>
<![endif]–>
总结
本文并不是强迫你一定要移除 jQuery,jQuery 为支持旧浏览器节省了很多成本。但条件成熟的情况下,移除 jQuery,参照 You Don’t Need jQuery 拥抱原生 JavaScript 能同样保证开发效率,也可以给产品带来更好的性能,同时也能提高开发者水平。
年轻代组成部分
为了理解GC,我们学习一下年轻代,对象第一次创建发生在这块内存区域。年轻代分为3块,Eden区和2个Survivor区。
年轻代总共有3块空间,其中2块为Survivor区。各个空间的执行顺序如下:
绝大多数新创建的对象分配在Eden区。
在Eden区发生一次GC后,存活的对象移到其中一个Survivor区。
在Eden区发生一次GC后,对象是存放到Survivor区,这个Survivor区已经存在其他存活的对象。
一旦一个Survivor区已满,存活的对象移动到另外一个Survivor区。然后之前那个空间已满Survivor区将置为空,没有任何数据。
经过重复多次这样的步骤后依旧存活的对象将被移到老年代。
通过检查这些步骤,如你看到的样子,其中一个Survivor区必须保持空。如果数据存在于两个Survivor区,或两个都没使用,你可以将这个情况作为系统错误的一个标志。
经过多次minor GC,数据被转移到老年代过程如下面的图表所示:
图3: GC前和GC后
请注意,在HotSpot虚拟机中,使用两种技术加快内存的分配。一个被称为“指针碰撞(bump-the-pointer)”,另外一个被称为“TLABs(线程本地分配缓冲)”。
指针碰撞技术跟踪分配给Eden区上最新的对象。该对象将位于Eden 区的顶部。如果之后有一个对象被创建,只需检查Eden区是否有足够大的空间存放该对象。如果空间够用,它将被放置在Eden区,存放在空间的顶部。因此,在创建新对象时,只需检查最后被添加对象,看是否还有更多的内存空间允许分配。然而,如果考虑多线程的环境,则是另外一种情况。为了实现多线程环境下,在Eden 区线程安全的去创建保存对象,那么必须加锁,因此性能会下降。在HotSpot虚拟机中TLABs能够解决这一问题。它允许每个线程在Eden区有自己的一小块私有空间。因为每一个线程只能访问自己的TLAB,所以在这个区域甚至可以使用无锁的指针碰撞技术进行内存分配。
我们已经对年轻代有了一个快速的浏览。你不需要要记住我刚才提到的两种技术。即便你不知道他们,也不会怎么样。但请务必记住:对象第一次被创建发生在Eden区,长期存活的对象被移动到老年代的Survivor区。
老年代GC
当老年代数据满时,基本上会执行一次GC。执行程序根据不同GC类型而变化,所以如果你知道不同类型的垃圾收集器,会更容易理解垃圾回收过程。
在JDK7中,有5种垃圾收集器:
Serial收集器
Parallel收集器
Parallel Old收集器 (ParallelCompacting GC)收集器
Concurrent Mark & Sweep GC (or “CMS”)收集器
Garbage First (G1) 收集器
其中,serial 收集器一定不能用于服务器端。这个收集器类型仅应用于单核CPU桌面电脑。使用serial收集器会显着降低应用程序的性能。
现在让我们来了解每个收集器类型。
Serial收集器
我们在前一段的解释了在年轻代发生的垃圾回收算法类型。在老年代的GC使用算法被称为“标记-清除-整理”。
该算法的第一步是在老年代标记存活的对象。
从头开始检查堆内存空间,并且只留下依然幸存的对象(清除)。
最后一步,从头开始,顺序地填满堆内存空间,将存活的对象连续存放在一起,这样堆分成两部分:一边有存放的对象,一边没有对象(整理)。
serial收集器应用于小的存储器和少量的CPU。
Parallel收集器
图4: Serial收集器和 Parallel收集器的差异
从这幅图中,你可以很容易看到Serial收集器和 Parallel收集器的差异。serial收集器只使用一个线程来处理的GC,而parallel收集器使用多线程并行处理GC,因此更快。当有足够大的内存和大量芯数时,parallel收集器是有用的。它也被称为“吞吐量优先垃圾收集器。”
ParallelOld 垃圾收集器
Parallel Old收集器是自JDK 5开始支持的。相比于parallel收集器,他们的唯一区别就是在老年代所执行的GC算法的不同。它执行三个步骤:标记-汇总-压缩(mark – summary – compaction)。汇总步骤与清理的不同之处在于,其将依然幸存的对象分发到GC预先处理好的不同区域,算法相对清理来说略微复杂一点。
CMSGC
图5: Serial GC & CMS GC
CMS垃圾收集器
如你在上图看到的那样, CMS垃圾收集器比之前我解释的各种算法都要复杂很多。初始标记(initial mark)比较简单。这一步骤只是查找距离类加载器最近的幸存对象。所以停顿时间非常短。之后的并发标记步骤,所有被幸存对象引用的对象会被确认是否已经被追踪检查。这一步的不同之处在于,在标记的过程中,其他的线程依然在执行。在重新标记步骤会修正那些在并发标记步骤中,因新增或者删除对象而导致变动的那部分标记记录。最后,在并发清除步骤,垃圾收集器执行。垃圾收集器进行垃圾收集时,其他线程的依旧在工作。一旦采取了这种GC类型,由于垃圾回收导致的停顿时间会极其短暂。CMS 收集器也被称为低延迟垃圾收集器。它经常被用在那些对于响应时间要求十分苛刻的应用上。
当然,这种GC类型在拥有stop-the-world时间很短的优点的同时,也有如下缺点:
它会比其他GC类型占用更多的内存和CPU,默认情况下不支持压缩步骤,在使用这个GC类型之前你需要慎重考虑。如果因为内存碎片过多而导致压缩任务不得不执行,那么stop-the-world的时间要比其他任何GC类型都长,你需要考虑压缩任务的发生频率以及执行时间。
G1GC
最后,我们来学习一下G1类型。
图6: Layout of G1 GC
如果你想要理解G1收集器,首先你要忘记你所理解的新生代和老年代。正如你在上图所看到的,每个对象被分配到不同的网格中,随后执行垃圾回收。当一个区域填满之后,对象被转移到另一个区域,并再执行一次垃圾回收。在这种垃圾回收算法中,不再有从新生代移动到老年代的三部曲。这个类型的垃圾收集算法是为了替代CMS 收集器而被创建的,因为CMS 收集器在长时间持续运行时会产生很多问题。
G1最大的好处是他的性能,他比我们在上面讨论过的任何一种GC都要快。但是在JDK 6中,他还只是一个早期试用版本。在JDK7之后才由官方正式发布。就我个人看来,NHN在将JDK 7正式投入商用之前需要很长的一段测试期(至少一年)。因此你可能需要再等一段时间。并且,我也听过几次使用了JDK 6中的G1而导致Java虚拟机宕机的事件。请耐心的等待它更稳定吧。
通过对垃圾收集器的介绍和梳理,在管理垃圾回收方面提出了五个建议,降低收集器开销,进一步提升项目性能。
保持GC低开销最实用的建议是什么?
早有消息声称Java 9即将发布,但如今却一再推迟,其中比较值得关注的是G1(“Garbage-First”)垃圾收集器将成为HotSpot JVM的默认收集器。从串行收集器到CMS收集器,在整个生命周期中JVM已历经多代GC的实现和更新,而接下来,G1收集器将谱写新的篇章。
随着垃圾收集器的持续发展,每一代都会进行改善和提高。在串行收集器之后的并行收集器利用多核机器强大的计算能力,实现了垃圾收集多线程。而之后的CMS(Concurrent Mark-Sweep)收集器,将收集分为多个阶段执行,允许在应用线程运行同时进行大量的收集,大大降低了“stop-the-world”全局停顿的出现频率。而现在,G1在JVM上加入了大量堆和可预测的均匀停顿,有效地提升了性能。
尽管GC不断在完善,其致命弱点还是一样:多余的和不可预知的对象分配。但本文中提出了一些高效的长期实用的建议,不管你选择哪种垃圾收集器,都可以帮助你降低GC开销。
随着垃圾收集器不断进步,以及实时优化和JIT编译器变得更加智能,作为开发者的我们,可以越来越少地操心代码的GC友好性。尽管如此,无论G1有多先进,在提高JVM方面,我们还有许多问题需要不断探索和实践,百尺竿头仍需更进一步。
提到PHP,肯定会有人说这是世界上最好的编程语言。单说流行程度,目前全球超过81.7%的服务器后端都采用了PHP语言,它驱动着全球超过2亿多个网站。上月初PHP7正式版发布,迎来自2004年以来最大的版本更新。现在,PHP 7.0.2又正式发布。
PHP 7.0.2正式版发布:WordPress速度提升3倍!
PHP7最显著的变化就是性能的极大提升,已接近Facebook开发的PHP执行引擎HHVM。在WordPress基准性能测试中,速度比5.6版本要快2~3倍,大大减少了内存占用。PHP7在语言上也有一些变化,比如添加返回类型声明、增加了一些新的保留关键字等。在安全方面,去除了PHP安全模式,添加魔术引号等。不仅如此,新版还支持64位,而且包含最新版Zend引擎。
虽然已正式发布,但PHP7的普及还需要很长时间,很多Web托管服务、企业Web应用出于兼容性考虑,在未来很长一段时间内可能都还会继续使用旧版本。不过,目前来看,全球最流行的PHP博客平台WordPress已经为PHP7最好了准备。
为何要学HTML5开发?HTML5发展前景如何?随着HTML5的卷土重来,它像是互联网行业扔下的一颗炸弹,有些人还未反应过来,原先专注的领域也许就将面临彻底的革新。比如像前几年疯狂甚至有点野蛮成长的App,现如今,细心者会发现一个新的移动互联网黄金时代就要到来。HTML5一时间也成为众人议论的话题。
虽然8年前,HTML5曾遭受了失败和冷遇,但今天的HTML5却显现了强大的生命力,它所带来的诸多优势始终吸引着开发者们继续探索,毕竟在开发商看来,能够在保证产品质量的同时减低成本是乐意至极的,现在的HTML5已经离这个目标很近很近了,2015年的HTML5已经无限接近原生APP。
2015年1月28日,世界上最大的视频网站YouTuBe正式宣布HTML5取代flash。事实上,HTML5的威力远远不止是视频这一个领域。
在蓝鸥HTML5开发培训小编看来:“现在HTML5对于之前版本来说,并非简单的版本升级,而是一次全面的框架和性能的提升与优化;这表现在:语法更简单、新增了大量的语义性标签、强大的canvas元素代替flash、丰富的API接口使用极大方便了开发者与浏览器的交互。其中,关键提升在于:基于HTML5强大的新增加框架,如手机端设备与页面进行交互,如重力感应、地理定位、离线操作等, 在主流移动端平台,可以很轻松地自定义性能强大的webapp,包括游戏、动画和企业级的应用开发。”
腾讯利用微信JS SDK在HTML5上的布局
腾讯,作为掌控着国内最大的移动入口平台公司,正在通过微信公众平台开始构建一个强大的轻应用平台。微信JS-SDK包提供的11类接口集,开发者不仅能够在网页上使用微信本身的拍照、选图、语音、位置等基本能力,还可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,这也意味着微信公众平台将更加开放,微信公众号的可定制性更加灵活。
对用户来说,在微信中看到的html5不再局限于简单的页面展示,而是一个功能较全面的App,它包含更多的互动功能。对于第三方开发者来说,舍弃原生应用,专注做好基于微信的html5应用,比以往时候都更容易。而对企业来说,微信营销将变得更加轻松,企业与用户之间的对接将更加容易。当然,还有QQ浏览器也是功不可没。
百度的轻应用本质就是html5
2014年,百度通过收购了91手机助手、安卓市场以及自家推出的百度手机卫士、百度搜索等,百度在移动应用分发上已经确立了老大的位置。
但是从百度的战略发展来看,百度更重视html5平台的搭建。而且百度在很早之前就已经推出了“轻应用”这个概念,百度的这个轻应用本质就是html5,并向开发者和企业推出了各种技术开放接口。
与此同时,百度通过百度浏览器、百度搜索等为这些企业和用户提供导流,促使让用户保持原有的搜索习惯,并推出了为企业服务的直达号。
阿里巴巴支付宝内部的HTML5
全球最大的在线支付平台,支付宝也正在通过服务窗为企业和用户之间提供一个桥梁,而这个桥梁的应用也正是基于支付宝内部的html5。
作为国内最大的移动浏览器,UC浏览器同样也把html5的开放放在了重心。目前,UC网页应用中心的月活跃用户已经超过了6000万,收录了20大类超过数万款的轻应用,并且国内超过90%的应用开发者都会通过UC网页应用中心来推广他们的轻应用。
当然,像BAT这样的在HTML5布局的企业还有很多,360的浏览器和360搜索,搜狐的手机新闻客户端等等。也由此可以看出,HTML5的发展是前途不可估量。
在HTML5发展浪潮中,蓝鸥广州HTML5培训机构已经开始了人才培养和储备,以便风暴席卷之时,国内企业能够拥有及时的应对,HTML5开发者无疑是最好的“战略储备”,并且,移动互联高度发展,自然偏重移动端的HTML5人才才是市场所需,企业所求。
Spring由7个模块组成:
Spring Core: 核心容器提供 Spring 框架的基本功能。核心容器的主要组件是BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转 (IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
Spring 上下文:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。
Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。
Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。
Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。
Sping的容器可以分为两种类型
1. BeanFactory:(org.springframework.beans.factory.BeanFactory接口定义)是最简答的容器,提供了基本的DI支持。最常用的BeanFactory实现就是XmlBeanFactory类,它根据XML文件中的定义加载beans,该容器从XML文件读取配置元数据并用它去创建一个完全配置的系统或应用。
2. ApplicationContext应用上下文:(org.springframework.context.ApplicationContext)基于BeanFactory之上构建,并提供面向应用的服务。
ClassPathXmlApplicationContext:从类路径下的XML配置文件中加载上下文定义,把应用上下文定义文件当做类资源。
FileSystemXmlApplicationContext:读取文件系统下的XML配置文件并加载上下文定义。
XmlWebApplicationContext:读取Web应用下的XML配置文件并装载上下文定义。
1 | ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); |
Inversion of Control, 一般分为两种类型:依赖注入DI(Dependency Injection)和依赖查找(Dependency Lookup).依赖注入应用比较广泛。
Spring IOC扶着创建对象,管理对象(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
优点:把应用的代码量降到最低。容器测试,最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载。
DI依赖注入是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用床架对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述组件需要哪些服务,之后一个IOC容器辅助把他们组装起来。
IOC的注入方式:1. 构造器依赖注入;2. Setter方法注入。
写CSS的同学们往往会体会到,随着项目规模的增加,项目中的CSS代码也会越来越多,如果没有及时对CSS代码进行维护,CSS代码不断会越来越多。CSS代码交错复杂,像一张庞大的蜘蛛网分布在网站的各个位置,你不知道修改这行代码会有什么影响,所以如果有修改或增加新功能时,开发人员往往不敢去删除旧的冗余的代码,而保险地增加新代码,最终的坏处就是项目中的CSS会越来越多,最终陷入无底洞。
CSS代码重构的目的
我们写CSS代码时,不仅仅只是完成页面设计的效果,还应该让CSS代码易于管理,维护。我们对CSS代码重构主要有两个目的:
Northern Dance No. 3 in G major - From "La Tersicore del Nord" Op. 147
1、提高代码性能
2、提高代码的可维护性
提高代码性能
提高页面的加载性能,简单说就是减小CSS文件的大小,提高页面的加载速度,尽可以的利用http缓存
2、提高CSS代码性能
不同的CSS代码,浏览器对其解析的速度也是不一样的,如何提高浏览器解析CSS代码的速度也是我们要考虑的
提高代码的可维护性
提高CSS代码的可维护性主要是体现在下面几点:
1、可重用性
一般来说,一个项目的整体设计风格是一致的,页面中肯定有几个风格一致但有些许不同的模块,如何在尽可能多地重用CSS代码,尽可能少地增加新代码,这是CSS代码中非常重要的一点。如果CSS代码的重用性高,我们可能只需要写一些不一样的地方,对页面性能和可维护性、提高开发效率都有很大的帮助。
2、可扩展性
如果产品增加了某个功能,我们应该保证新增加的CSS代码不会影响到旧的CSS代码和页面,并且尽可能少地增加新代码而重用旧代码。
3、可修改性
如果某个模块产品经理觉得要修改样式,或者要删掉它,如果没有规划好相应的CSS代码,过了一段时间之后,开发人员可能已经不记得这段代码作用了几个地方,不敢修改或删除它,这样下去CSS代码也就越来越多,影响了页面的性能,还造成了代码的复杂度。
CSS代码重构的基本方法
前面说到了CSS代码重构的目的,现在我们来说说一些如何达到这些目的的一些基本方法,这些方法都是易于理解,容易实施的一些手段,大家平时可能也不知不觉地在使用它。
提高CSS性能的手段
首先说说如何提高CSS性能,根据页面的加载性能和CSS代码性能,主要总结有下面几点:
1、尽量将样式写在单独的css文件里面,在head元素中引用
有时候为了图方便或者快速搞定功能,我们可能会直接将样式写在页面的style标签或者直接内联在元素上,这样虽然简单方便,但是非常不利于日后的维护。将代码写成单独的css文件有几点好处:
(1)内容和样式分离,易于管理和维护
(2)减少页面体积
(3)css文件可以被缓存、重用,维护成本降低
转载请注明:翠竹林 » CSS代码重构与优化之路
分页是目前在显示大量结果时所采用的最好的方式。有了下面这些代码的帮助,开发人员可以在多个页面中显示大量的数据。在互联网上,分页是一般用于搜索结果或是浏览全部信息(比如:一个论坛主题)。几乎在每一个Web应用程序都需要划分返回的数据,并按页显示。下面的这个列表给出的代码可以让你的开发很有帮助。学习这些代码,对于初学者也很有帮助。
1) 使用Ajax分页
下面这个示例使用了jQuery + PHP。Demo link
2) MySql 分页
数据库的分页处理。
5) 分页风格
6) PHP 分页类
这是一个PHP库,可以让你更容易的做分页。
8 ) 基本分页
9) Php Page
也是一个分页教程。
本文介绍的10个网站是下载PHP脚本为主,伯乐在线曾推荐 《16个下载超酷脚本的热门网站》,这些网站除了PHP脚本,还有JavaScript、Java、Perl、ASP等脚本。如果你已是脚本代码巧匠,不妨把你的出色脚本放到Code Canyon网站上去出售,这不失为一种赚钱之道,尤其是在工资涨不过物价的时期。
这里提供免费的PHP脚本下载。包括PHP资源,教程,文章等等。
Free-Php为你提供分类列表,包括免费的PHP脚本,商业PHP脚本,PHP资源,PHP教程,网页资源,PHP主机等等。
这里可以为您提供网站备用的PHP应用程序和脚本。
这个网站提供多样化的PHP脚本。
您可以免费找到最大的PHP脚本目录。您也可以从这里获得大量的PHP相关资源。
这个网站提供PHP脚本的大列表,比如反馈表,搜索引擎,贺卡,内容管理,调查等等。
这个网站为你提供免费的PHP脚本,在线工具,文章,与PHP相关主题的教程。这些免费的PHP脚本大多数是由站长编写,为程序员提供了最初素材。
这个网站提供了免费的PHP脚本目录,一直保持快速增长的趋势,比如PHP论坛,PHP图片库,CMS,PHP的电子商务解决方案,以及其它的开源脚本。每一个脚本都有相应的demo,你可以提前测试,不用浪费时间在安装上,大部分的PHP脚本都提供直接的下载地址。
这里为您提供PHP脚本的大集合。
这个网站列出了免费的PHP脚本,你可以在你的网站上利用这些脚本,来实现不同的功能,(比如反馈表、搜索引擎、贺卡、内容管理等)
Ruby 是一个略显丰满的女孩,但其令人窒息的甜蜜面容绝对会成为你一生的梦想,然而,当你最终得到了她,她却开始迅速发胖,你开始认识到了她华丽外表下所有恶劣的性格缺陷。你坚持认为事情会有好转,但事实却不是这样,你最终不得不苦恼的把所有时间都花在寻找新的能让她保持高兴的事情上。
Java 是一个过于精于计算的生意场上的女人,她会盲目的遵循各种习惯风俗——不论它们是如何的相互矛盾和荒谬。虽然相貌平平,但你和她出门或做任何事情前都必须经过各种的礼节、复杂的装饰、规定的仪式。她通过各种方法、没完没了的自我宣传来保持一个好的名声。
Python是大众情人,她体贴周到,魅力迷人,是一位好听众,更是伶牙俐齿,思想有见地,有时她会进入野外自我探索的旅途,在那里,她能彻底的自我再造,当她会来时,你看到的已经是完全另外一个人了。
JavaScript是精力充沛的狂热信徒,她愿意去寻找出任何荒谬的办法来跑的更快——即使会因此变得体积太庞大而无法干任何其它的事情。然而,你无法忽视她,因为她无处不在,你总能遇到她。幸运的是,当她变的有点失控时,她的胖朋友们会为你提醒她。
Lisp将会成为你事业上的导师,能在任何事情上为您提供理论方案,让你佩服的五体投地,但是,按她的方法做出的锤子不能直直钉入一个钉子。永远都是玄虚的思想,宏大的计划,跟着她,你会发现自已永远都只能住在桥洞里饿的瑟瑟发抖,直到她发现另外一个愿意为她花钱的傻瓜再为她可笑的追逐50年的风车。
PHP是一个很平常的女孩,能真心的和你交谈。她超重,不是很聪明,皮肤不好,虽然化妆也不是很漂亮,每有流行事情她都在几年后才去追随,安于现状。你最终会勉强接受她。
浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires; Cache-control等)。但是也有非HTTP协议定义的缓存机制,如使用HTML Meta 标签,Web开发者可以在HTML页面的<head>节点中加入<meta>标签,代码如下:
| html code |
| <META HTTP-EQUIV=”Pragma” CONTENT=”no-cache”> |
上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析HTML内容本身。
下面我主要介绍HTTP协议定义的缓存机制。
Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
下面是宝宝PK项目中,浏览器拉取jquery.js web服务器的响应头:
注:Date头域表示消息发送的时间,时间的描述格式由rfc822定义。例如,Date: Mon,31 Dec 2001 04:25:57GMT。
Web服务器告诉浏览器在2012-11-28 03:30:01这个时间点之前,可以使用缓存文件。发送请求的时间是2012-11-28 03:25:01,即缓存5分钟。
不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。
| http协议头Cache-Control : |
| 值可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age 各个消息中的指令含义如下:
|
还是上面那个请求,web服务器返回的Cache-Control头的值为max-age=300,即5分钟(和上面的Expires时间一致,这个不是必须的)。
Last-Modified/If-Modified-Since要配合Cache-Control使用。
l Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
l If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。
Etag/If-None-Match也要配合Cache-Control使用。
l Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器觉得)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
l If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304。
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
l Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
l 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存
l 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
浏览器缓存行为还有用户的行为有关!!!
| 用户操作 | Expires/Cache-Control | Last-Modified/Etag |
| 地址栏回车 | 有效 | 有效 |
| 页面链接跳转 | 有效 | 有效 |
| 新开窗口 | 有效 | 有效 |
| 前进、后退 | 有效 | 有效 |
| F5刷新 | 无效 | 有效 |
| Ctrl+F5刷新 | 无效 | 无效 |
浏览器第一次请求:
浏览器再次请求时:

再回味 David Linden 的原著“The Accidental Mind”
要想弄懂 ECMAScript 标准中那些晦涩难懂的内容,就必须学一大堆东西——不光要学 JavaScript 的未来,还要(为了完全理解编辑的动机及其所受限制)学它的历史。我就是这样过来的,随着我将 JavaScript 的知识碎片慢慢地拼缀到一起,事情也就渐渐变得明朗了,这是一种有着很多“包袱”的语言——有着各种不宜见光的秘密。

ECMAScript 2015 候选发布版 1
占领浏览器
本文不是为 ES6 的卖点做宣传——有很多相当好的资源可以了解那些即将面世的酷炫功能和扩展——不管怎么说,我认为我们对于 JavaScript 已经基本上统治了浏览器市场这个事实的认识应该是一致的。当今的浏览器中并没有太多的客户端语言,这是有原因的:VBScript 已经从 IE 11 中卸掉了,虽然谷歌仍试图将 Dart 硬塞进 web 中(嗯,大多是指 Chrome)——但并不很成功。

随着集纳越来越多开发者的智慧分享,JavaScript 变得越来越普遍,开始出现在一些陌生的、出乎意料的地方:起初,这些地方只是 web 的天涯海角,纵使 JavaScript 广为流行,也仍然没有被触及到。随后不久,JavaScript 就无处不在了。本质上,IT 领域有两条(左右)广为人知的定律,为 JavaScript 统治世界做出了贡献,甚至预言了这一点。

摩尔定律的解释
摩尔定律使移动 web 变得可行——同时,JavaScript 突进到了服务端、设备及移动操作系统领域,最终将渗入极微小的微控制器中。

Netscape 最初的客户与服务端 JavaScript 架构

但并没有坚持下去。

IE 的 dHTML(摘自《Developing XML Solutions》一书)

DOM 中的基本模式——James Padolsey 提供图片
最终,除了 DOM 之外,又出现了几个其他的功能及扩展,有些进入了标准化程序,被接受为跨浏览器的解决方案,有些仍然局限于单一的浏览器厂商或环境。直至今天,仍然在持续不断地努力工作(其中一个合作成果是 Service Worker 规范)。作为扩展 web 的手段,以使其跟上技术发展的速度,这种推陈出新的努力是受欢迎的。

哇,到目前为止,关于 node,我是鼓吹得太多了(但我无意推销给任何人),但愿我能公正地回答下面这个问题:

浏览器大战其间,JavaScript 的速度提升
浏览器大战是激烈的,JavaScript 的执行速度已经 10 倍于我们此前见过的任何东西——而且由于激烈的竞争,其执行速度仍然在稳步地提升……然而,node.js 的创立者 Ryan Dahl 甚至都没有试过将 JavaScript 塞入服务器环境中。
相反,应当承认,他只是在研究事件驱动的服务器技术时,碰巧撞上了强大的 V8 引擎——JavaScript 正好适合于事件驱动的、非阻塞I/O的、基于单线程循环的环境。
即便异步、非阻塞/事件驱动的 I/O 这样的想法根本不算新鲜,事件驱动的 web 服务器却也并不多。事件驱动的服务器要溯源到 1999 年,新生的 Flash(不,不是那个 flash)服务器就使用的这种技术——但其他使用这种模式的服务端解决方案很快就消失了。事实上,(如果我错了,请纠正我),比起我知道的唯一一个相关的解决方案——Tornado web 服务器(用 Python 写成),node.js 在时间上至少领先半年。
Alexandre 在评论里指出——Nginx 服务器也是事件驱动的(这实际上在 Ryan 最初的演示中就提到了,用来与 Apache的线程系统作对比),不过,我不认为这是一个公正的比较,因为 Nginx 事件驱动的功能太弱,也不太灵活。
也承几位读者指出,Twisted Framework(也是用 Python 写成)是一个事件驱动的异步网络框架,确实早于 node 的发布。Twisted 是不是适合上面的讨论,这一点可以争论,但无疑很有说服力——谢谢参与!
(对于上面提到的 Tornado web 服务器,Twisted 也具有丰富的互操作功能,但我认为 Twisted 更适合与 node 实现互操作,不过这只是个人看法。)

2015年初 IO.js 的到来进一步扩大了 node 社区,并为社区注入了新的生命力
分支作者选择的开放管理模式开始发挥魔力,贡献者们蜂拥而至:短短几个月,活跃的贡献者数量就超过了早期的 node.js,而且本地化团队将社区推进到了新的疆界。
“好,好——node.js 给服务端编程注入了新的生命,它是我们的救世主,穿着闪亮盔甲的无畏勇士,是那啥和那啥——明白了,我的老天呐,烦死了……”——好吧,但它远不止这些!

易于使用,可扩展,可用于所有相关平台,Grunt 和 Gulp 不仅围绕 JavaScript 和 web 来扩展工具,而是任何能想到的平台。(你知道连 Photoshop 都有自己内置的、可通过脚本访问的 node.js 实例吗?)
Jaxer 没能做成的(带有 DOM 的嵌入式浏览器),对 node 来说相当自然——首先借助 PhantomJS,其后不久,利用 JSDOM 中加入的 JavaScript 原生一类对象支持,在 node.js 中即可访问 DOM——而在一个类似的程序包中,重新实现了对 web 内容的自动化客户端测试(请查看 Domenic Denicola 关于 JSDOM 及其动机的精彩演讲!)你猜怎么着,继征服浏览器、服务器以及命令行之后,甚至还不满足——node 瞄准了鱼缸中的那条更大的鱼:桌面应用。

Atom Editor 界面(全部以纯粹的 HTML5 写成),可以与任何当代原生代码编辑器相媲美
Node-webkit 激发出了一整套全新的桌面项目。在 Cloud9 IDE 成功整合 node.js 和 web 技术从而成为最先进的集成开发环境之后,像 Brackets 和 ATOM 这样的项目都将 web 体验带入了桌面(同时,两个世界各自最棒的部分——跨平台互操作性和扩展性——保持不变)。

万物皆用 JavaScript
有人可能会说,“物联网”的观念目前十分流行。智能手表,智能照明,智能取暖,智能房屋,联网的冰箱、洗衣机以及电热水壶——应有尽有。
这些都是小型的、低功耗的设备,小型的芯片加上有限的内存,再加上低功耗——像 JavaScript 这样的资源独占、耗电量大的语言,可能驱动不了这些设备,是这样么?
嗯,再想一想!万物皆用 JavaScript 的时代可能比你想象的来得要快。

几年以前,人们认为低功耗、成本敏感的设备根本不可能使用 web 技术——但 Firefox OS 证明了这不仅可行,而且最近两年里在大约 30 个国家发布了 15 种不同的基于 Firefox OS 的设备。
所有这些设备都是成熟的联网、触屏的智能手机——售价低到只有 33 美金,而且其他类型的装置(如松下的 FirefoxOS 驱动的电视)也证明了,JavaScript 和 web 技术仍然远未触顶。
实际上离所说的界限还远着呢,这不,三星决定将其用于它自己的运行 Tizen 操作系统的智能手表平台上——而同时,还与 Ecma 国际的 TC39 工作组进行接触,看能不能标准化一种用于移动的更加节俭的语言子集,以便用在更小尺寸的设备上。
我们目前还不知道三星的提议有什么结果——嵌入式 JavaScript 本身就是一个非常活跃的话题。三星最初的 TC39 宣传材料上引用了 Technical Machine 公司的 Tessel 以及 Espruino,这是两种使用 JS 的微控制器平台。
下面我们来看看如何为这样资源高度受限的硬件创建一个 JavaScript 的解决方案。在进入细节之前,我想先介绍一下第二个定律,与大家分享:
“任何能用 JavaScript 写的应用,最后都将会用 JavaScript 来写。”——由 Jeff Atwood 发明的 Atwood 定律
我就不用过度解释这个定律了(其实定律本身已经自我解释得很好了),但我会在下面展示其应用。

Espruino 的下一代,Pico——计划在 2015 年 5 月份发

第一代 Tessel(取自 Technical Machine 的 Instagram)
由于以上所述的这些原因,Technical Machine 公司开发 Tessel 的高手们选择了一条不同的路径。Tessel 有丰裕的内存(32 MB)由 JavaScript 解释器支配——然而驱动整个机器的 Cortex M3 微控制器才有大约 200KB 的内存,致使在上面运行任何高级 JavaScript 解释器都非常困难。

Jan OS 是一头怪兽,由疯狂的荷兰人,阿姆斯特丹的科学怪人 Jan Jongboom 所发明——它既非一个手机平台(不再是),也非微控制器平台(还不是)——而是两者皆有那么一点。

所有这些传感器和电路要装在一张信用卡大小的 IC 板上!
其背后原理,主要是运行了一个精简的、为移动优化过的 Linux 内核(Firefox OS 的内核,Gonk,是基于 AOSP 内核的),带 Mozilla 的 Gecko 引擎(包括内置的 SpiderMonkey JavaScript 引擎——全优化的,具备所有移动或桌面浏览器上所有花哨功能的——哈喽呜~~~,Jaxer!),但要有额外的 WebAPI,才能访问移动芯片组、wifi、FM 广播(!)、发送短信、打电话或捕捉陀螺仪数据!
Tessel 2——裸机原型

两个 Tessel 连接到一个 Tessel 2 的 USB 端口上

“分形”式的概念图
ASM.js 并不是新技术。——它甚至都称不上是一种技术,只是一串迭代优化,碰巧最后很容易被优化,而且运行确实比较快——如果你愿意,也可以说它更像一种制导的演进(guided evolution)。某些优化(像 AST)已经在前面提过了,这些优化有利于 JavaScript 的性能提升。AST 用于提升解释器的速度,从而使得运行时的执行更快——但没有人认为这是执行 JavaScript 的唯一方式。
这里的关键词是:编译。
自从早期的解释语言以来,即时(JIT)编译已广为人知,且广泛应用——JavaScript 也不例外(自然,对于 JavaScript 这样的动态类型语言来说,JIT有着一些怪异的行为和陷阱)。

Firefox 中 SpiderMonkey JIT 结构
优化热点代码(像长循环或频繁调用的函数),将其直接编译为机器指令会使执行性能大幅提升。ASM.js 试图要做的就是,通过定义一种中间形式的 JavaScript 子集语法,在执行前将 JS 源代码进行 AOT(事先)编译。编译生成类型安全的、可直接执行的机器代码,消除了托管内存和垃圾回收,因而性能就变得可以预测了。

即将加入 JavaScript——对原生 SIMD 指令的支持
对 ASM.js 来说,还有一个第二位的,不那么重要的目标——将 JavaScript 建立为一种快速的目标语言。编译为 JS 的工具,像 Emscripten 或 GWT,在 ASM.js 出现之前已经有一段时间了,由于将底层语言编译为 JavaScript 的习惯使然,对于这些编译器生成的源代码,引擎开始做优化。ASM.js 自然延续了这种做法(正像你注意到的,我努力不去过度使用“演进”这个词),定义一种通用的“语言”(一种标准语法格式),使得这些工具输出的源代码可以进行比较,易于优化,或许还有那么点儿可读性(当然,不可能照顾到每个人的习惯)。

也许是因为年轻人的逆反心理,人们都不把自己的入门语言当回事。很早的时候,计算机系的学生用Scheme或者Pascal入门,现在大部分学校用Java。这也许就是为什么很多人恨Java,瞧不起用Java的人。提到Java,感觉就像是爷爷那辈人用的东西。大家都会用Java,怎么能显得我优秀出众呢?于是他们说:“Java老气,庞大,复杂,臃肿。我更愿意探索新的语言……”
某些Python程序员,在论坛里跟初学者讲解Python有什么好,其中一个原因竟然是:“因为Python不是Java!” 他们喜欢这样宣传:“看Python多简单清晰啊,都不需要写类型……” 对于Java的无缘无故的恨,盲目的否认,导致了他们看不到它很重要的优点,以至于迷失自己的方向。虽然气势上占上风,然而其实Python作为一个编程语言,是完全无法和Java抗衡的。
在性能上,Python比Java慢几十倍。由于缺乏静态类型等重要设施,Python代码有bug很不容易发现,发现了也不容易debug,所以Python无法用于构造大规模的,复杂的系统。你也许发现某些startup公司的主要代码是Python写的,然而这些公司的软件,质量其实相当的低。在成熟的公司里,Python最多只用来写工具性质的东西,或者小型的,不会影响系统可靠性的脚本。
静态类型的缺乏,也导致了Python不可能有很好的IDE支持,你不能完全可靠地“跳转到定义”,不可能完全可靠地重构(refactor)Python代码。PyCharm对于早期的Python编程环境,是一个很大的改进,然而理论决定了,它不可能完全可靠地进行“变量换名”等基本的重构操作。就算是比PyCharm强大很多的PySonar,对此也无能为力。由于Python的设计过度的“动态”,没有类型标记,使得完全准确的定义查找,成为了不可判定(undecidable)的问题。
在设计上,Python,Ruby比起Java,其实复杂很多。缺少了很多重要的特性,有毛病的“强大特性”倒是多了一堆。由于盲目的推崇所谓“正宗的面向对象”方式,所谓“late binding”,这些语言里面有太多可以“重载”语义的地方,不管什么都可以被重定义,这导致代码具有很大的不确定性和复杂性,很多bug就是被隐藏在这些被重载的语言结构里面了。因此,Python和Ruby代码很容易被滥用,不容易理解,容易写得很乱,容易出问题。
很多JavaScript程序员也盲目地鄙视Java,而其实JavaScript比Python和Ruby还要差。不但具有它们的几乎所有缺点,而且缺乏一些必要的设施。JavaScript的各种“WEB框架”,层出不穷,似乎一直在推陈出新,而其实呢,全都是在黑暗里瞎蒙乱撞。JavaScript的社区以幼稚和愚昧著称。你经常发现一些非常基本的常识,被JavaScript“专家”们当成了不起的发现似的,在大会上宣讲。我看不出来JavaScript社区开那些会议,到底有什么意义,仿佛只是为了拉关系找工作。
Python凑合可以用在不重要的地方,Ruby是垃圾,JavaScript是垃圾中的垃圾。原因很简单,因为Ruby和JavaScript的设计者,其实都是一知半解的民科。然而世界就是这么奇怪,一个彻底的垃圾语言,仍然可以宣称是“程序员最好的朋友”,从而得到某些人的爱戴……
最近一段时间,很多人热衷于Scala,Clojure,Go等新兴的语言,他们以为这些是比Java更现代,更先进的语言,以为它们最终会取代Java。然而这些狂热分子们逐渐发现,Scala,Clojure和Go其实并没有解决它们声称能解决的问题,反而带来了它们自己的毛病,而这些毛病很多是Java没有的。然后他们才意识到,Java离寿终正寝的时候,还远得很……
Go语言
关于Go,我已经评论过很多了,有兴趣的人可以看这里。总之,Go是民科加自大狂的产物,奇葩得不得了。这里我就不多说它了,只谈谈Scala和Clojure。
Scala
我认识一些人,开头很推崇Scala,仿佛什么救星似的。我建议他们别去折腾了,老老实实用Java。没听我的,结果到后来,成天都在骂Scala的各种毛病。但是没办法啊,项目上了贼船,不得不继续用下去。我不喜欢进行人身攻击,然而我发现一个语言的好坏,往往取决于它的设计者的背景,觉悟,人品和动机。很多时候我看人的直觉是异常的准,以至于依据对语言设计者的第一印象,我就能预测到这个语言将来会怎么发展。在这里,我想谈一下对Scala和Clojure的设计者的看法。
Scala的设计者Martin Odersky,在PL领域有所建树,发表了不少学术论文( 包括著名的《The Call-by-Need Lambda Calculus》),而且还是大名鼎鼎的Niklaus Wirth的门徒,我因此以为他还比较靠谱。可是开始接触Scala没多久,我就很惊讶的发现,有些非常基本的东西,Scala都设计错了。这就是为什么我几度试图采用Scala,最后都不了了之。因为我一边看,一边发现让人跌眼镜的设计失误,而这些问题都是Java没有的。这样几次之后,我就对Odersky失去了信心,对Scala失去了兴趣。
回头看看Odersky那些论文的本质,我发现虽然理论性貌似很强,其实很多是在故弄玄虚(包括那所谓的“call-by-need lambda calculus”)。他虽然对某些特定的问题有一定深度,然而知识面其实不是很广,眼光比较片面。对于语言的整体设计,把握不够好。感觉他是把各种语言里的特性,强行拼凑在一起,并没有考虑过它们是否能够“和谐”的共存,也很少考虑“可用性”。
由于Odersky是大学教授,名声在外,很多人想找他拿个PhD,所以东拉西扯,喜欢往Scala里面加入一些不明不白,有潜在问题的“特性”,其目的就是发paper,混毕业。这导致Scala不加选择的加入过多的特性,过度繁复。加入的特性很多后来被证明没有多大用处,反而带来了问题。学生把代码实现加入到Scala的编译器,毕业就走人不管了,所以Scala编译器里,就留下一堆堆的历史遗留垃圾和bug。这也许不是Odersky一个人的错,然而至少说明他把关不严,或者品位确实有问题。
最有名的采用Scala的公司,无非是Twitter。其实像Twitter那样的系统,用Java照样写得出来。Twitter后来怎么样了呢?CEO都跑了 :P 新CEO上台就裁员300多人,包括工程师在内。我估计Twitter裁员的一个原因是,有太多的Scala程序员,扯着各种高大上不实用的口号,比如“函数式编程”,进行过度工程,浪费公司的资源。花着公司的钱,开着各种会议,组织各种meetup和hackathon,提高自己在open source领域的威望,其实没有为公司创造很多价值……
Clojure
再来说一下Clojure。当Clojure最初“横空面世”的时候,有些人热血沸腾地向我推荐。于是我看了一下它的设计者Rich Hickey做的宣传讲座视频。当时我就对他一知半解拍胸脯的本事,印象非常的深刻。Rich Hickey真的是半路出家,连个CS学位都没有。可他那种气势,仿佛其他的语言设计者什么都不懂,只有他看到了真理似的。不过也只有这样的人,才能创造出“宗教”吧?
满口热门的名词,什么lazy啊,pure啊,STM啊,号称能解决“大规模并发”的问题,…… 这就很容易让人上钩。其实他这些词儿,都是从别的语言道听途说来,却又没能深刻理解其精髓。有些“函数式语言”的特性,本来就是有问题的,却为了主义正确,为了显得高大上,抄过来。所以最后你发现这语言是挂着羊头卖狗肉,狗皮膏药一样说得头头是道,用起来怎么就那么蹩脚。
Clojure的社区,一直忙着从Scheme和Racket的项目里抄袭思想,却又想标榜是自己的发明。比如Typed Clojure,就是原封不动抄袭Typed Racket。有些一模一样的基本概念,在Scheme里面都几十年了,恁是要改个不一样的名字,免得你们发现那是Scheme先有的。甚至有人把SICP,The Little Schemer等名著里的代码,全都用Clojure改写一遍,结果完全失去了原作的简单和清晰。最后你发现,Clojure里面好的地方,全都是Scheme已经有的,Clojure里面新的特性,几乎全都有问题。我参加过一些Clojure的meetup,可是后来发现,里面竟是各种喊着大口号的小白,各种趾高气昂的民科,愚昧之至。
如果现在要做一个系统,真的宁可用Java,也不要浪费时间去折腾什么Scala或者Clojure。错误的人设计了错误的语言,拿出来浪费大家的时间。
我至今不明白,很多人对Java的仇恨和鄙视,从何而来。它也许缺少一些方便的特性,然而长久以来用Java进行教学,用Java工作,用Java开发PySonar,RubySonar,Yin语言,…… 我发现Java其实并不像很多人传说的那么可恶。我发现自己想要的95%以上的功能,在Java里面都能找到比较直接的用法。剩下的5%,用稍微笨一点的办法,一样可以解决问题。
盲目推崇Scala和Clojure的人们,很多最后都发现,这些语言里面的“新特性”,几乎都有毛病,里面最重要最有用的特性,其实早就已经在Java里了。有些人跟我说:“你看,Java做不了这件事情!” 后来经我分析,发现他们在潜意识里早已死板的认定,非得用某种最新最酷的语言特性,才能达到目的。Java没有这些特性,他们就以为非得用另外的语言。其实,如果你换一个角度来看问题,不要钻牛角尖,专注于解决问题,而不是去追求最新最酷的“写法”,你就能用Java解决它,而且解决得干净利落。
很多人说Java复杂臃肿,其实是因为早期的Design Patterns,试图提出千篇一律的模板,给程序带来了不必要的复杂性。然而Java语言本身跟Design Patterns并不是等价的。Java的设计者,跟Design Pattern的设计者,完全是不同的人。你完全可以使用Java写出非常简单的代码,而不使用Design Patterns。
Java只是一个语言。语言只提供给你基本的机制,至于代码写的复杂还是简单,取决于人。把对一些滥用Design Patterns的Java程序员的恨,转移到Java语言本身,从而完全抛弃它的一切,是不明智的。
我平时用着Java偷着乐,本来懒得评论其它语言的。可是实在不忍心看着有些人被Scala和Clojure忽悠,所以在这里说几句。如果没有超级高的性能和资源需求(可能要用C这样的低级语言),目前我建议就老老实实用Java吧。虽然不如一些新的语言炫酷,然而实际的系统,还真没有什么是Java写不出来的。少数地方可能需要绕过一些限制,或者放宽一些要求,然而这样的情况不是很多。
编程使用什么工具是重要的,然而工具终究不如自己的技术重要。很多人花了太多时间,折腾各种新的语言,希望它们会奇迹一般的改善代码质量,结果最后什么都没做出来。选择语言最重要的条件,应该是“够好用”就可以,因为项目的成功最终是靠人,而不是靠语言。既然Java没有特别大的问题,不会让你没法做好项目,为什么要去试一些不靠谱的新语言呢?
计算机刚问世时,普通人——包括大多数程序员——都不允许靠近计算机。计算机被锁在有值班员看守的房间里,这些人穿着白色工作服,负责把你的程序和数据输入电脑里,一段时间后就会返回计算机的运行结果。20世纪60年代发明了分时系统——计算机能够快速地把运行的工作从一个用户切换到另外一个用户的系统,这使多个用户同时直接与计算机交互成为可能。在分时系统里,多个用户坐在“终端”旁输入命令到计算机里,计算机就会打印出它的响应信息。之前的个人计算机同样使用输入命令和打印出响应的方式,不同之处是只能同时存在一个用户。用户与电脑的这种交互方式叫作命令行界面操作。
当然,现在的大多数人都在使用完全不同的方式与计算机进行交互。他们使用图形用户界面,简称GUI。计算机在屏幕上绘制界面组件。这些组件包括窗口、滚动条、菜单、按钮和图标等。通常来说,我们使用鼠标来操作这些组件,或者在触控屏上用手指来操作。只要你不是刚从二十世纪七十年代穿越而来的,你肯定已经很熟悉图形用户界面的基础了。
大部分的GUI交互组件都已经相当标准化了。也就是说,它们拥有相似的外观并且能在包括Mac OS、Windows和Linux这些不同的计算机平台上运行。Java能够使用所有的这些GUI组件,而且它的目标是让这些组件在不用修改程序的前提下能够在不同的平台上运行。这些组件的外观在不同的平台上可能会有点不同,但是程序在任意计算机上提供的功能应该是一样的。
下面的图片中,一个简单的Java程序演示了一些GUI界面组件。当程序运行时,计算机屏幕上就会显示出一个与下图类似的窗体。在这个窗体里有四个可以和用户交互的组件:按钮、复选框、文本域和弹出菜单。这些组件都被标签标明了。这个窗体里还有一些其他的组件。标签本身就是一个组件(即使你不能与它进行交互)。窗体的右半部是一个文本域组件,它可以显示多行文本。当文本的行数大于刚好填充满文本域的行数时,滚动条组件就会出现在文本域的一边。实际上,在Java中,这整个窗体本身就被认为是一个“组件”。

(如果你想要运行这个程序,你可以在网上获取GUIDemo.java源码和已编译的程序GUIDemo.jar,这本书更多其他示例及使用说明可以参考2.6节。)
如今,Java实际上拥有两个完整的GUI组件集合。其中一个是AWT或者叫抽象窗体工具集(Abstract Windowing Toolkit),Java的原始版本就提供了这个工具集。另外一个叫作Swing,它是在Java 1.2时发布的。并且在大多数的现代Java编程中都优先使用Swing。上面演示的程序就使用了Swing的部分组件。
当用户使用GUI组件进行交互时,“事件”的概念就产生了。例如,点击一下按钮就会产生一个事件,在文本域打字时按回车会产生事件。每次产生事件都会向程序发生一条消息,告诉它发送了什么事件,然后程序就根据它的编码去响应事件。事实上,一个典型的GUI程序由大量的“事件处理器”组成,它们负责告诉程序如何去响应各种类型的事件。在这个例子中,该程序被编程为通过在文本域里打印信息来响应每一个事件。在更实际的例子中,事件处理器可以做更多的事。
这里使用“消息(message)”术语是有意图的。就像你在上一节里所看到的,消息会被发送给对象。实际上,Java GUI组件都是一些对象。Java包括了很多预定义类,这些类代表了各种类型的GUI组件。其中的一些类又是其他类的子类。这里有一张图仅展示了一些Swing的GUI类和它们的关系:

现在不用担心它们的细节,但要去感受下在这里是如何使用面向对象编程和继承的。注意,所有的GUI类都直接或者间接地属于JComponent类的子类,这个类包含了所有Swing组件共享的常规属性。在上图中,有两个JComponent的直接子类本身就拥有自己的子类。JTextArea和JTextField类都拥有一些相同的行为,并一起被分组到JTextComponent的子类下。类似地,JButton和JToggleButton都是JAbstractButton的子类,它们都包含了按钮和复选框这些共有的属性。(顺便说一下,JComboBox是一个包含弹出菜单的Swing类。)
仅从这个简单的论述中,你或许可以看到GUI编程是怎样有效地利用面向对象设计的。实际上,拥有“可视化对象”的GUI可能是推进OOP流行的主要因素。
使用GUI组件和事件编程是Java最有趣的一个方面。但是,我们会先使用几个章节来介绍基础知识,并在第6章重返这个话题。
1、不使用@import
这条手段已经是众所周知,这里简单提一下,@import影响css文件的加载速度
2、避免使用复杂的选择器,层级越少越好
有时候项目的模块越来越多,功能越来越复杂,我们写的CSS选择器会内套多层,越来越复杂。
Homo Symbolicus: The Dawn of Language, Imagination and Spirituality
header.logo.text{}
可以优化成
haeder.logo-text{}
简洁的选择器不仅可以减少css文件大小,提高页面的加载性能,浏览器解析时也会更加高效,也会提高开发人员的开发效率,降低了维护成本。
3、精简页面的样式文件,去掉不用的样式
很多时候,我们会把所有的样式文件合并成一个文件,但是这样有一个问题:很多其他页面的CSS同时引用到当前页面中,而当前页面并没有用到它们,这种情况会造成两个问题:
(1)样式文件偏大,影响加载速度
(2)浏览器会进行多余的样式匹配,影响渲染时间。
本文由 ImportNew - 唐尤华 翻译自 math.hws.edu。欢迎加入翻译小组。转载请见文末要求。
《Java编程入门》是一本使用Java作为入门语言的免费计算机编程课本。可以用作编程入门课程教材,也可以用来自学编程。阅读本书只需要对计算机和编程有一般性了解。本书中包含了一整年大学编程课程内容。第1章至第7章可以满足大学一学期课程或者高中一学年课程的教学,余下的章节可以作为第2门课程。
本书的第七版涵盖了“Java 7”的所有内容。Java最新版本是Java8。在这本书中只有一部分内容涉及Java 8的新特性。
(译注:本书时间写得早,所以当时Java最新版本是 Java 8。)
这本书的主页是 http://math.hws.edu/javanotes/,提供了下载整个网站和本书PDF版本的链接。下载的网站内容包含这本书中使用的示例源代码、每章课后测试的答案和结尾练习的解答。非常推荐读者下载这些示例代码,在阅读的同时读代码并且运行这些程序。要想从这本书中得到最大的收获,强烈推荐读完所有练习解答。
在内容风格上,这本书更偏向于课本而不是教程。换句话说,它更专注于概念的解释,而不是指导一步步如何去做。我试图采用一种对话风格进行写作,更贴近课堂教学而不是像传统的课本那样。当然,它不是Java参考书,更不是对Java所有功能的总结。这本书不是为那些已经了解某种其它编程语言的人所编写的Java快速入门。相反,它针对的是那些第一次学习编程的人,更多的以Java为例讲授一般编程概念。我相信,这本《Java编程入门》完全可以匹敌市面上那些传统出版社发行、印刷的编程课本。(好吧,我承认在我看来这本书会更胜一筹。)
教授Java有很多方法。一种是从一开始就是用图形化编程界面。一些人认为,应该从开始就强调面向对象编程。这不是我的方式。我钟爱的方式是从更基本的编程模块开始构建,然后从基本模块继续学习。在介绍章节之后的第2、3和4章,我讨论了面向过程的程序设计。在第5章介绍了面向对象编程。第6章讨论了面向事件编程的相关话题以及图形用户界面。第3章提到的数组在第7章进行了完整介绍。第8章是一个很短的章节,标志了本书的一个转折点。从编程基础概念的介绍转向了更高级的话题。第8章涉及了如何编写健壮、正确和高效的程序。第9章和第10章讨论了递归和数据结构,包括Java集合框架。第11章是关于文件和网络。第12章讨论了线程和并发处理。最后,第13章回到了图形用户界面编程,介绍了Java更加高级的功能。
第7版《Java编程入门》没有对第6版进行大幅更新。实际上,编写新版的主要动机是从书中移除applet部分和相关讨论。Applet是运行在网页中的Java程序。Java刚诞生时,看起来applet似乎会成为创建Web动态内容的主流方式。直到第6版,本书的主页还包含了示例applet程序。然而,由于安全因素和其它技术的出现,applet不再广泛使用。加之最近发布的Java版本让applet使用更加困难,因此决定不在书中介绍applet。移除applet后,我把精力投在了让读者们可以更方便地下载和运行示例程序。
第7版的另一个显著改进是,在第3章加入了数组的简要介绍。在接下来的3个章节里会更详细地讨论数组。之前的版本中,数组在对象和GUI编程之后,到第7章才开始介绍。新版的第7章包含了数组高级用法的讨论。
除了上述变化,还有很多针对Java 7新功能的小改进。
《Java编程入门》的最新版可以在线获得 http://math.hws.edu/javanotes/。该书的第1版写于1996年,自那以后有了很多版本。各个不同的版本可以在下列网址看到:
第1版:http://math.hws.edu/eck/cs124/javanotes1/ (Java 1.0)
第2版: http://math.hws.edu/eck/cs124/javanotes2/ (Java 1.1)
第3版:http://math.hws.edu/eck/cs124/javanotes3/ (Java 1.1)
第4版: http://math.hws.edu/eck/cs124/javanotes4/ (Java 1.4)
第5版: http://math.hws.edu/eck/cs124/javanotes5/(Java 5.0)
第6版: http://math.hws.edu/eck/cs124/javanotes6/ (Java 5.0及更高版本)
第7版:http://math.hws.edu/eck/cs124/javanotes7/ (Java 7)
《Java编程入门》是免费的,不受版权限制。第7版基于“署名-非商业性使用-相同方式共享3.0”授权发布。要查看协议的副本,可以访问 http://creativecommons.org/licenses/by-nc-sa/3.0/。你可以:
在你自己的网站上发布未经修改的版本(包含作者署名和许可声明!)
在遵守协议的前提下,你可以分发或出售未经修改的版本。
对本书修改或部分修改可以在互联网上发布且用于非商业目的。要求版本归属作者、明确地标注修改内容并且修改版本遵循原协议发布,包括翻译成其它语言。
协议中未注明的使用情况,需要征得原作者许可。
虽然协议中没有明确对此进行要求,但我非常期待了解人们使用或传播我的工作。
关于本书的技术说明:本书的在线和PDF版本来自同一份原稿,主要由XML编写。为了输出PDF版本,该XML文件被处理为可以被TeX排版程序使用的格式。除了XML文件,原稿还包含了DTD、XSLT转换、Java源代码文件、图片、TeX宏文件和一些用来处理的脚本。这些脚本可以在Linux和Mac OS上运行。
本书的源文件可以从下面网址获得:
http://math.hws.edu/eck/cs124/downloads/javanotes7-full-source.zip
这些文件本意并不用来出版,因此没有非常仔细地编写,使用这些文件需要很多专业知识。然而,我收到了很多请求想要这些文件,因此就“原封不动”的提供出来。这些文件的详细信息及如何使用说明,请下载并参阅其中的README。
大卫·j·艾克(David J. Eck)教授
数学和计算机科学系
霍巴特威廉史密斯学院
美国纽约州日内瓦区普尔特尼街300号
邮件:eck@hws.edu
网站:http://math.hws.edu/eck/
运行相同的代码六次,每次使用不同的VM参数(-XX:+UseSerialGC, -XX:+UseParallelGC, -XX:+UseConcMarkSweepGC, -XX:ParallelCMSThreads=2, -XX:ParallelCMSThreads=4, -XX:+UseG1GC)。
每次运行大概花费55分钟。
其它VM参数:-Xmx2048M -server
OpenJDK版本:1.8.0_51(当前最新的版本)
软件:Linux version 4.0.4-301.fc22.x86_64
硬件:Intel? Core? i7-4790 CPU @ 3.60GHz
每次运行13个?OptaPlanner?规划问题方案。每次运行时间为5分钟。前30秒用于JVM预热,不计算在内。
解决规划问题不涉及 IO (除了启动时需要几毫秒来加载输入信息)。单个 CPU 使用完全饱和。通常会创建许多存活时间很短的对象,GC 之后就会回收这些对象。
衡量标准可以是计算每毫秒的得分,越高越好。计算一个拟议规划解决方案是一个不可小觑的问题:涉及到大量的计算,包括每个实体与其他所有实体的冲突检测。
为了能在本地重复运行这些基准测试,可以从源码进行构建,然后运行主类 GeneralOptaPlannerBenchmarkApp。
执行结果
为了方便查看,我已经对每种 GC 与 Java 8 默认 GC(Parallel GC)进行了比较。
结果非常清楚:默认(Parallel GC)是最快的。
原始基准测试数据
相对基准测试数据
有一种提议是在 OpenJDK9 的服务器端使用 G1 作为默认 GC。我第一反应就是拒绝该提议:
G1 的平均值要慢17.60%
G1 在每个数据集用例下都比较慢。
在最大数据集(Machine Reassignment B10)下,表现比其它数据集都要差,G1 慢了34.07%。
如果在开发机和服务器之间采用不同的默认 GC,则开发者基准测试的可信度就会下降。
另一方面,存在几个需要注意的细节:
G1 关注是 GC 暂停的问题,而不是吞吐量。对于这些用例(计算量比较大),GC 暂停时长基本没影响。
这是一个(基本是)单线程的基准测试。并行解决多个问题或采用多线程解决的基准测试,结果可能不同。
G1 推荐的堆内存至少是 6GB。而这次基准测试的堆内存是 2GB,即使在最大数据集(Machine Reassignment B10)也只需要这么多内存。
海量计算只是 OpenJDK 的诸多功能中的一个:这是在社区广泛争论的一个问题。如果有其他方面(如网站服务)的证明,可能值得改变默认GC。但是,请首先向我展示你实际项目的基准测试。
在 Java 8 中,对 OptaPlanner 用例来说,默认 GC(Parallel GC)通常情况是最好的选择。
本文介绍了如何利用Memcached提升Java企业应用性能。首先,总览了传统的Java缓存框架,并和Memcached做一个比较。当然,也会在你的本机上安装Memcached,如何通过telnet与Memcached交互工作。接着,创建一个”Hello Memcached”Java客户端程序。你会了解如何利用Memcached减少数据库服务器负载,缓存动态生成的页面标记。最后,考虑对spymemcached客户端做一些高级优化配置。
像EHCache和OSCache这样的Java缓存框架,本质上是存在于应用代码中的HashMap对象。无论何时添加一个新的对象到缓存中,它都保存在你的应用内存中。保存少量数据时,这个策略是没有问题的,但缓存超过GB的数据就有问题了。Memcached服务器的设计者采用一种分布式架构,这种方式便于扩展,因此,可以利用Memcached做海量数据缓存。
Memcached架构包含两部分。首先是一个拥有自身进程的Memcached服务器软件。倘若你想扩展你的应用,可以在其它机器上运行Memcached服务器软件。Memcached服务器软件实例相互独立。Memcached系统的第二部分是Memcached客户端,它确切地知道每台服务器的存在。客户端负责获取缓存录入对应的服务器,以及存储或者获得缓存录入——这一过程,稍后我会做详细地讨论。
如果曾经开发过Java EE 网络应用,你一定用过EHCache或者OSCache之类的Java开源缓存框架。你或许用过DynaCache或者JBoss Cache这样的商业缓存框架作为应用服务器的一部分。在我们亲手实践本教程之前,明白Memcached与那些传统Java缓存框架的不同之处是很重要的。
无论选择开源或者是商业方案,使用传统Java缓存架构是很容易。使用类似EHCache或者OSCache这种开源的框架,你需要下载二进制文件,添加必须的JAR文件到你的应用classpath下。同样,你需要创建配置文件,配置缓存、交换分区的大小。由于缓存框架需要与软件绑定,而缓存框架通常会与应用服务器绑定,所以无需下载任何额外的JAR文件。
图1 传统的Java缓存架构
在为你的应用程序添加缓存框架之后,通过创建CacheManager对象获取和设置其中的缓存条目(entry)。这样,你的应用和缓存框架创建的CacheManager会在相同的JVM上运行。每次增加缓存条目,此对象会添加到由缓存框架维护某类哈希表中。
一旦你的应用服务器软件运行在多个节点上,你可能需要支持分布式缓存。在分布式缓存系统中,一旦在AppServer1中添加了某个对象,在AppServer2和AppServer3上此对象也变为可用。传统的Java缓存使用复制(replication)实现分布式缓存,这意味着当你为AppServer1添加一个缓存条目,该条目会自动复制到系统的其它应用服务器上。最终,条目会在所有的站点中可用。
要使用Memcached进行缓存,必须下载并在你的平台上安装Memcached服务器软件。一旦Memcached服务器安装成功,它会通过TCP或者UDP端口监听缓存调用。
图2 Memcached架构
接着,下载一个JavaMemcached客户端,把客户端JAR文件添加到你的应用中。然后创建一个Memcached客户端对象,就可以调用它的方法获取和设置缓存条目。一旦添加某个对象到缓存中,Memcached客户端会获取该对象、对其序列化并发送字节数组到Memcached服务端保存。这时,缓存对象可能被应用运行的JVM作为垃圾回收。
当你需要缓存对象时,可以调用Memcached客户端的 get() 方法。客户端会得到这个get请求、序列化并将get请求传给Memcached服务器。Memcached服务器通过该请求从缓存中查找这个对象。如果存有此对象,服务器会把这个字节数组返回给客户端。客户端收到字节数组,反序列化并创建对象返回给你的应用。
即使你的应用跑在不止一个应用服务器上,所有的应用都能指向相同的Memcached服务器,通过它获取并设置缓存条目。倘若你拥有不止一台Memcached服务器,服务器互相之间不会知道。因此,你需要配置Memcached客户端,这样它就能知道所有Memcached服务器。比如,应用在AppServer1创建一个Java对象,接着调用Memcached的 set() 方法,Memcached客户端就找到某个Memcached服务器来存放条目。接着它只和此台Memcached服务器通信。同样,一旦存在于AppServer2或者Appserver3的代码尝试去获取某个录入时,Memcached客户端首先会找出哪个服务器存储了此条目,接着只与此服务器通信。
在缺省状态下,Memcached客户端使用非常简单的逻辑选择服务器进行get或set操作。一旦调用get()或者set(),客户端就会得到缓存键(key)调用hashcode()方法得到整数值,比如11。接着用这个数除以Memcached服务器可用数量(比如2),本例中得到的余数为1。缓存条目就会指向Memcached服务器1。这个简单的算可以确保应用服务器所在的Memcached客户端为给定的缓存键选择相同的服务器。
Memcached可以运行在Unix、Linux、windows以及MacOSX上。你可以下载Memcached源码编译,或者直接下载编译好的二进制文件安装Memcached。这里我会展示为特定平台下载二进制文件的安装过程。如果你更倾向于编译,请参见这里。
接下来的安装指令针对Windows XP 32位机器,若平台是linux等其它平台,查看这里。注意本文案例代码是在Windows XP 32位机器上开发的,不过是可以在其它平台上运行。
Jellycan code是一个Memcached修订版本,更易用更有效,我们先从下载win32二进制压缩文件开始。
解压Memcached-<versionnumber>-win32-bin.zip,注意里面包含memcached.exe,执行此文件完成服务器搭建。
使用 memcached.exe -d install 注册memcached.exe作为系统服务,你可以在服务控制台开启或者停止Memcached服务器。
当你在缺省状态下执行memcached.exe,Memcached服务器默认占用64兆内存,监听11211端口。在某些情形下,或许你想做一些更加细粒度的控制。比如,端口11211被本机其他进程占用,你希望Memcached可以监听端口12000;或者你想在质量保证或者生产环境中搭建Memcached服务器,需要的默认内存不止64兆。你可以通过命令行参数定制服务器行为。运行memcache.exe -help命令会获取所有的命令行选项,如下图3所示。
图3 Memcached服务器命令行选项
一旦Memcached服务器开始监听你指定的端口,Memcached客户端就可以通过TCP或者UDP端口与之连接,发送命令或者接受响应,最后关闭连接。
连接Memcached服务器方式有多种,我会在本教程的第二部分采用Java客户端连接,你将能够利用简单的API从缓存中存储或者获取对象。或者你可以采用Telnet客户端直接与服务器连接。懂得利用Telnet客户端与Memcached服务器交互对调试Java客户端很重要,因此我们就从这里开始。
首先你需要用Telnet客户端连接Memcached服务器。在WindowsXP平台上,如果Memcached服务器也运行在这台机器上并缺省监听端口11211,只要执行telnet localhost 11211。接下来的命令对Telnet管理Memcached很重要:
set添加一个新的项目到缓存中,使用格式是 Set <keyName> <flags> <expiryTime> <bytes>,你可以将敲入的值存入下一行。倘若不想缓存录入过期,可以输入0。
get返回缓存键的值,调用get <keyName>获得keyName的值。
add添加一个新的键,前提是此键之前并不存在,比如add <keyName> <flags> <expiryTime> <bytes>。
replace会替代某个键的值,前提是此键已存在,比如replace <keyName> <flags> <expiryTime> <bytes>。
delete删除某个键的缓存录入,调用delete <keyName>删除keyName的值。
图4的截图展示了通过Telnet与Memcached服务器交互案例。正如你所看到的,Memcached服务器会对每个命令做出回应,比如STORED、NOT_STORED等。
图4 Telnet客户端与Memcached服务器交互案例
到此,我们简要地讨论了Memcached分布式框架和众多传统Java缓存系统。在你的开发环境中安装了Memcached,通过Telnet连接Memcached。教程的下一篇中,我们将调用Java客户端sypmemcached命令,为一个Java示例应用建立分布式缓存方案。在此过程中,你会了解更多关于Memcached的信息,以及如何提升你的JavaEE应用性能。
JVM是虚拟机,也是一种规范,他遵循着冯·诺依曼体系结构的设计原理。冯·诺依曼体系结构中,指出计算机处理的数据和指令都是二进制数,采用存储程序方式不加区分的存储在同一个存储器里,并且顺序执行,指令由操作码和地址码组成,操作码决定了操作类型和所操作的数的数字类型,地址码则指出地址码和操作数。从dos到window8,从unix到ubuntu和CentOS,还有MAC OS等等,不同的操作系统指令集以及数据结构都有着差异,而JVM通过在操作系统上建立虚拟机,自己定义出来的一套统一的数据结构和操作指令,把同一套语言翻译给各大主流的操作系统,实现了跨平台运行,可以说JVM是java的核心,是java可以一次编译到处运行的本质所在。
我研究学习了JVM的组成和运行原理,JVM的统一数据格式规范、字节码文件结构,JVM关于内存的管理。
JVM的毕竟是个虚拟机,是一种规范,虽说符合冯诺依曼的计算机设计理念,但是他并不是实体计算机,所以他的组成也不是什么存储器,控制器,运算器,输入输出设备。在我看来,JVM放在运行在真实的操作系统中表现的更像应用或者说是进程,他的组成可以理解为JVM这个进程有哪些功能模块,而这些功能模块的运作可以看做是JVM的运行原理。JVM有多种实现,例如Oracle的JVM,HP的JVM和IBM的JVM等,而在本文中研究学习的则是使用最广泛的Oracle的HotSpot JVM。
1.JVM在JDK中的位置。
JDK是java开发的必备工具箱,JDK其中有一部分是JRE,JRE是JAVA运行环境,JVM则是JRE最核心的部分。我从oracle.com截取了一张关于JDK Standard Edtion的组成图,

从最底层的位置可以看出来JVM有多重要,而实际项目中JAVA应用的性能优化,OOM等异常的处理最终都得从JVM这儿来解决。HotSpot是Oracle关于JVM的商标,区别于IBM,HP等厂商开发的JVM。Java HotSpot Client VM和Java HotSpot Server VM是JDK关于JVM的两种不同的实现,前者可以减少启动时间和内存占用,而后者则提供更加优秀的程序运行速度(参考自:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/index.html ,该文档有关于各个版本的JVM的介绍)。在命令行,通过java -version可以查看关于当前机器JVM的信息,下面是我在Win8系统上执行命令的截图,

可以看出我装的是build 20.13-b02版本,HotSpot 类型Server模式的JVM。
2.JVM的组成
JVM由4大部分组成:ClassLoader,Runtime Data Area,Execution Engine,Native Interface。
我从CSDN找了一张描述JVM大致结构的图:

2.1.ClassLoader是负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
2.2.Native Interface是负责调用本地接口的。他的作用是调用不同语言的接口给JAVA用,他会在Native Method Stack中记录对应的本地方法,然后调用该方法时就通过Execution Engine加载对应的本地lib。原本多于用一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代。
2.3.Execution Engine是执行引擎,也叫Interpreter。Class文件被加载后,会把指令和数据信息放入内存中,Execution Engine则负责把这些命令解释给操作系统。
2.4.Runtime Data Area则是存放数据的,分为五部分:Stack,Heap,Method Area,PC Register,Native Method Stack。几乎所有的关于java内存方面的问题,都是集中在这块。下图是javapapers.com上关于Run-time Data Areas的描述:

可以看出它把Method Area化为了Heap的一部分,javapapers.com中认为Method Area是Heap的逻辑区域,但这取决于JVM的实现者,而HotSpot JVM中把Method Area划分为非堆内存,显然是不包含在Heap中的。下图是javacodegeeks.com中,2014年9月刊出的一片博文中关于Runtime Data Area的划分,其中指出,NonHeap包含PermGen和Code Cache,PermGen包含Method Area,而且PermGen在JAVA SE 8中已经不再用了。查阅资料(https://abhirockzz.wordpress.com/2014/03/18/java-se-8-is-knocking-are-you-there/)得知,java8中PermGen已经从JVM中移除并被MetaSpace取代,java8中也不会见到OOM:PermGen Space的异常。目前Runtime Data Area可以用下图描述它的组成:

2.4.1.Stack是java栈内存,它等价于C语言中的栈,栈的内存地址是不连续的,每个线程都拥有自己的栈。栈里面存储着的是StackFrame,在《JVM Specification》中文版中被译作java虚拟机框架,也叫做栈帧。StackFrame包含三类信息:局部变量,执行环境,操作数栈。局部变量用来存储一个类的方法中所用到的局部变量。执行环境用于保存解析器对于java字节码进行解释过程中需要的信息,包括:上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。操作数栈用于存储运算所需要的操作数和结果。StackFrame在方法被调用时创建,在某个线程中,某个时间点上,只有一个框架是活跃的,该框架被称为Current Frame,而框架中的方法被称为Current Method,其中定义的类为Current Class。局部变量和操作数栈上的操作总是引用当前框架。当Stack Frame中方法被执行完之后,或者调用别的StackFrame中的方法时,则当前栈变为另外一个StackFrame。Stack的大小是由两种类型,固定和动态的,动态类型的栈可以按照线程的需要分配。 下面两张图是关于栈之间关系以及栈和非堆内存的关系基本描述(来自http://www.programering.com/a/MzM3QzNwATA.html):


2.4.2.Heap是用来存放对象信息的,和Stack不同,Stack代表着一种运行时的状态。换句话说,栈是运行时单位,解决程序该如何执行的问题,而堆是存储的单位,解决数据存储的问题。Heap是伴随着JVM的启动而创建,负责存储所有对象实例和数组的。堆的存储空间和栈一样是不需要连续的,它分为Young Generation和Old Generation(也叫Tenured Generation)两大部分。Young Generation分为Eden和Survivor,Survivor又分为From Space和 ToSpace。
和Heap经常一起提及的概念是PermanentSpace,它是用来加载类对象的专门的内存区,是非堆内存,和Heap一起组成JAVA内存,它包含MethodArea区(在没有CodeCache的HotSpotJVM实现里,则MethodArea就相当于GenerationSpace)。在JVM初始化的时候,我们可以通过参数来分别指定,PermanentSpace的大小、堆的大小、以及Young Generation和Old Generation的比值、Eden区和From Space的比值,从而来细粒度的适应不同JAVA应用的内存需求。
2.4.3.PC Register是程序计数寄存器,每个JAVA线程都有一个单独的PC Register,他是一个指针,由Execution Engine读取下一条指令。如果该线程正在执行java方法,则PC Register存储的是 正在被执行的指令的地址,如果是本地方法,PC Register的值没有定义。PC寄存器非常小,只占用一个字宽,可以持有一个returnAdress或者特定平台的一个指针。
2.4.4.Method Area在HotSpot JVM的实现中属于非堆区,非堆区包括两部分:Permanet Generation和Code Cache,而Method Area属于Permanert Generation的一部分。Permanent Generation用来存储类信息,比如说:class definitions,structures,methods, field, method (data and code) 和 constants。Code Cache用来存储Compiled Code,即编译好的本地代码,在HotSpot JVM中通过JIT(Just In Time) Compiler生成,JIT是即时编译器,他是为了提高指令的执行效率,把字节码文件编译成本地机器代码,如下图:

引用一个经典的案例来理解Stack,Heap和Method Area的划分,就是Sring a=”xx”;Stirng b=”xx”,问是否a==b? 首先==符号是用来判断两个对象的引用地址是否相同,而在上面的题目中,a和b按理来说申请的是Stack中不同的地址,但是他们指向Method Area中Runtime Constant Pool的同一个地址,按照网上的解释,在a赋值为“xx”时,会在Runtime Contant Pool中生成一个String Constant,当b也赋值为“xx”时,那么会在常量池中查看是否存在值为“xx”的常量,存在的话,则把b的指针也指向“xx”的地址,而不是新生成一个String Constant。我查阅了网络上大家关于String Constant的存储的说说法,存在略微差别的是,它存储在哪里,有人说Heap中会分配出一个常量池,用来存储常量,所有线程共享它。而有人说常量池是Method Area的一部分,而Method Area属于非堆内存,那怎么能说常量池存在于堆中?
我认为,其实两种理解都没错。Method Area的确从逻辑上讲可以是Heap的一部分,在某些JVM实现里从堆上开辟一块存储空间来记录常量是符合JVM常量池设计目的的,所以前一种说法没问题。对于后一种说法,HotSpot JVM的实现中的确是把方法区划分为了非堆内存,意思就是它不在堆上。我在HotSpot JVM做了个简单的实验,定义多个常量之后,程序抛出OOM:PermGen Space异常,印证了JVM实现中常量池是在Permanent Space中的说法。但是,我的JDK版本是1.6的。查阅资料,JDK1.7中InternedStrings已经不再存储在PermanentSpace中,而是放到了Heap中;JDK8中PermanentSpace已经被完全移除,InternedStrings也被放到了MetaSpace中(如果出现内存溢出,会报OOM:MetaSpace,这里有个关于两者性能对比的文章:http://blog.csdn.net/zhyhang/article/details/17246223 )。 所以,仁者见仁,智者见智,一个馒头足以引发血案,就算是同一个商家的JVM,毕竟JDK版本在更新,或许正如StackOverFlow上大神们所说,对于理解JVM Runtime Data Area这一部分的划分逻辑,还是去看对应版本的JDK源码比较靠谱,或者是参考不同的版本JVM Specification( http://docs.oracle.com/javase/specs/ )。
2.4.5.Native Method Stack是供本地方法(非java)使用的栈。每个线程持有一个Native Method Stack。
3.JVM的运行原理简介
Java 程序被javac工具编译为.class字节码文件之后,我们执行java命令,该class文件便被JVM的Class Loader加载,可以看出JVM的启动是通过JAVA Path下的java.exe或者java进行的。JVM的初始化、运行到结束大概包括这么几步:
调用操作系统API判断系统的CPU架构,根据对应CPU类型寻找位于JRE目录下的/lib/jvm.cfg文件,然后通过该配置文件找到对应的jvm.dll文件(如果我们参数中有-server或者-client, 则加载对应参数所指定的jvm.dll,启动指定类型的JVM),初始化jvm.dll并且挂接到JNIENV结构的实例上,之后就可以通过JNIENV实例装载并且处理class文件了。class文件是字节码文件,它按照JVM的规范,定义了变量,方法等的详细信息,JVM管理并且分配对应的内存来执行程序,同时管理垃圾回收。直到程序结束,一种情况是JVM的所有非守护线程停止,一种情况是程序调用System.exit(),JVM的生命周期也结束。
关于JVM如何管理分配内存,我通过class文件和垃圾回收两部分进行了学习。
JVM中的内存管理主要是指JVM对于Heap的管理,这是因为Stack,PC Register和Native Method Stack都是和线程一样的生命周期,在线程结束时自然可以被再次使用。虽然说,Stack的管理不是重点,但是也不是完全不讲究的。
1.栈的管理
JVM允许栈的大小是固定的或者是动态变化的。在Oracle的关于参数设置的官方文档中有关于Stack的设置(http://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/jrdocs/refman/optionX.html#wp1024112),是通过-Xss来设置其大小。关于Stack的默认大小对于不同机器有不同的大小,并且不同厂商或者版本号的jvm的实现其大小也不同,如下表是HotSpot的默认大小:
Platform | Default |
|---|---|
Windows IA32 | 64 KB |
Linux IA32 | 128 KB |
Windows x86_64 | 128 KB |
Linux x86_64 | 256 KB |
Windows IA64 | 320 KB |
Linux IA64 | 1024 KB (1 MB) |
Solaris Sparc | 512 KB |
我们一般通过减少常量,参数的个数来减少栈的增长,在程序设计时,我们把一些常量定义到一个对象中,然后来引用他们可以体现这一点。另外,少用递归调用也可以减少栈的占用。 栈是不需要垃圾回收的,尽管说垃圾回收是java内存管理的一个很热的话题,栈中的对象如果用垃圾回收的观点来看,他永远是live状态,是可以reachable的,所以也不需要回收,他占有的空间随着Thread的结束而释放。(参考自:http://stackoverflow.com/questions/20030120/java-default-stack-size)
关于栈一般会发生以下两种异常:
1.当线程中的计算所需要的栈超过所允许大小时,会抛出StackOverflowError。
2.当Java栈试图扩展时,没有足够的存储器来实现扩展,JVM会报OutOfMemoryError。 我针对栈进行了实验,由于递归的调用可以致使栈的引用增加,导致溢出,所以设计代码如下:
我的机器是x86_64系统,所以Stack的默认大小是128KB,上述程序在运行时会报错:
而当我在Eclipse中调整了-Xss参数到3M之后,该异常消失。
另外栈上有一点得注意的是,对于本地代码调用,可能会在栈中申请内存,比如C调用malloc(),而这种情况下,GC是管不着的,需要我们在程序中,手动管理栈内存,使用free()方法释放内存。
2.堆的管理
堆的管理要比栈管理复杂的多,我通过堆的各部分的作用、设置,以及各部分可能发生的异常,以及如何避免各部分异常进行了学习。
上图是 Heap和PermanentSapce的组合图,其中 Eden区里面存着是新生的对象,From Space和To Space中存放着是每次垃圾回收后存活下来的对象 ,所以每次垃圾回收后,Eden区会被清空。 存活下来的对象先是放到From Space,当From Space满了之后移动到To Space。当To Space满了之后移动到Old Space。Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor复制过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
Old Space中则存放生命周期比较长的对象,而且有些比较大的新生对象也放在Old Space中。
堆的大小通过-Xms和-Xmx来指定最小值和最大值,通过-Xmn来指定Young Generation的大小(一些老版本也用-XX:NewSize指定), 即上图中的Eden加FromSpace和ToSpace的总大小。然后通过-XX:NewRatio来指定Eden区的大小,在Xms和Xmx相等的情况下,该参数不需要设置。通过-XX:SurvivorRatio来设置Eden和一个Survivor区的比值。(参考自博文:http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html)
堆异常分为两种,一种是Out of Memory(OOM),一种是Memory Leak(ML)。Memory Leak最终将导致OOM。实际应用中表现为:从Console看,内存监控曲线一直在顶部,程序响应慢,从线程看,大部分的线程在进行GC,占用比较多的CPU,最终程序异常终止,报OOM。OOM发生的时间不定,有短的一个小时,有长的10天一个月的。关于异常的处理,确定OOM/ML异常后,一定要注意保护现场,可以dump heap,如果没有现场则开启GCFlag收集垃圾回收日志,然后进行分析,确定问题所在。如果问题不是ML的话,一般通过增加Heap,增加物理内存来解决问题,是的话,就修改程序逻辑。
3.垃圾回收
JVM中会在以下情况触发回收:对象没有被引用,作用域发生未捕捉异常,程序正常执行完毕,程序执行了System.exit(),程序发生意外终止。
JVM中标记垃圾使用的算法是一种根搜索算法。简单的说,就是从一个叫GC Roots的对象开始,向下搜索,如果一个对象不能达到GC Roots对象的时候,说明它可以被回收了。这种算法比一种叫做引用计数法的垃圾标记算法要好,因为它避免了当两个对象啊互相引用时无法被回收的现象。
JVM中对于被标记为垃圾的对象进行回收时又分为了一下3种算法:
1.标记清除算法,该算法是从根集合扫描整个空间,标记存活的对象,然后在扫描整个空间对没有被标记的对象进行回收,这种算法在存活对象较多时比较高效,但会产生内存碎片。
2.复制算法,该算法是从根集合扫描,并将存活的对象复制到新的空间,这种算法在存活对象少时比较高效。
3.标记整理算法,标记整理算法和标记清除算法一样都会扫描并标记存活对象,在回收未标记对象的同时会整理被标记的对象,解决了内存碎片的问题。
JVM中,不同的 内存区域作用和性质不一样,使用的垃圾回收算法也不一样,所以JVM中又定义了几种不同的垃圾回收器(图中连线代表两个回收器可以同时使用):

1.Serial GC。从名字上看,串行GC意味着是一种单线程的,所以它要求收集的时候所有的线程暂停。这对于高性能的应用是不合理的,所以串行GC一般用于Client模式的JVM中。
2.ParNew GC。是在SerialGC的基础上,增加了多线程机制。但是如果机器是单CPU的,这种收集器是比SerialGC效率低的。
3.Parrallel Scavenge GC。这种收集器又叫吞吐量优先收集器,而吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。Parallel Scavenge GC由于可以提供比较不错的吞吐量,所以被作为了server模式JVM的默认配置。
4.ParallelOld是老生代并行收集器的一种,使用了标记整理算法,是JDK1.6中引进的,在之前老生代只能使用串行回收收集器。
5.Serial Old是老生代client模式下的默认收集器,单线程执行,同时也作为CMS收集器失败后的备用收集器。
6.CMS又称响应时间优先回收器,使用标记清除算法。他的回收线程数为(CPU核心数+3)/4,所以当CPU核心数为2时比较高效些。CMS分为4个过程:初始标记、并发标记、重新标记、并发清除。
7.GarbageFirst(G1)。比较特殊的是G1回收器既可以回收Young Generation,也可以回收Tenured Generation。它是在JDK6的某个版本中才引入的,性能比较高,同时注意了吞吐量和响应时间。
对于垃圾收集器的组合使用可以通过下表中的参数指定:

默认的GC种类可以通过jvm.cfg或者通过jmap dump出heap来查看,一般我们通过jstat -gcutil [pid] 1000可以查看每秒gc的大体情况,或者可以在启动参数中加入:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log来记录GC日志。
GC中有一种情况叫做Full GC,以下几种情况会触发Full GC:
1.Tenured Space空间不足以创建打的对象或者数组,会执行FullGC,并且当FullGC之后空间如果还不够,那么会OOM:java heap space。
2.Permanet Generation的大小不足,存放了太多的类信息,在非CMS情况下回触发FullGC。如果之后空间还不够,会OOM:PermGen space。
3.CMS GC时出现promotion failed和concurrent mode failure时,也会触发FullGC。promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
4.判断MinorGC后,要晋升到TenuredSpace的对象大小大于TenuredSpace的大小,也会触发FullGC。
可以看出,当FullGC频繁发生时,一定是内存出问题了。
1.数据类型规范
依据冯诺依曼的计算机理论,计算机最后处理的都是二进制的数,而JVM是怎么把java文件最后转化成了各个平台都可以识别的二进制呢?JVM自己定义了一个抽象的存储数据单位,叫做Word。一个字足够大以持有byte、char、short、int、float、reference或者returnAdress的一个值,两个字则足够持有更大的类型long、double。它通常是主机平台一个指针的大小,如32位的平台上,字是32位。
同时JVM中定义了它所支持的基本数据类型,包括两部分:数值类型和returnAddress类型。数值类型分为整形和浮点型。
整形:
byte | 值是8位的有符号二进制补码整数 |
short | 值是16位的有符号二进制补码整数 |
int | 值是32位的有符号二进制补码整数 |
long | 值是64位的有符号二进制补码整数 |
char | 值是表示Unicode字符的16位无符号整数 |
浮点:
float | 值是32位IEEE754浮点数 |
double | 值是64位IEEE754浮点数 |
returnAddress类型的值是Java虚拟机指令的操作码的指针。
对比java的基本数据类型,jvm的规范中没有boolean类型。这是因为jvm中堆boolean的操作是通过int类型来进行处理的,而boolean数组则是通过byte数组来进行处理。
至于String,我们知道它存储在常量池中,但他不是基本数据类型,之所以可以存在常量池中,是因为这是JVM的一种规定。如果查看String源码,我们就会发现,String其实就是一个基于基本数据类型char的数组。如图:

2.字节码文件
通过字节码文件的格式我们可以看出jvm是如何规范数据类型的。下面是ClassFile的结构:

关于各个字段的定义(参考自JVM Specification 和 博文:http://www.cnblogs.com/zhuYears/archive/2012/02/07/2340347.html),
magic:
魔数,魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。
minor_version、major_version:
分别为Class文件的副版本和主版本。它们共同构成了Class文件的格式版本号。不同版本的虚拟机实现支持的Class文件版本号也相应不同,高版本号的虚拟机可以支持低版本的Class文件,反之则不成立。
constant_pool_count:
常量池计数器,constant_pool_count的值等于constant_pool表中的成员数加1。
constant_pool[]:
常量池,constant_pool是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。常量池不同于其他,索引从1开始到constant_pool_count -1。
access_flags:
访问标志,access_flags是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。access_flags的取值范围和相应含义见下表:

this_class:
类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info类型常量,表示这个Class文件所定义的类或接口。
super_class:
父类索引,对于类来说,super_class的值必须为0或者是对constant_pool表中项目的一个有效索引值。如果它的值不为0,那constant_pool表在这个索引处的项必须为CONSTANT_Class_info类型常量,表示这个Class文件所定义的类的直接父类。当然,如果某个类super_class的值是0,那么它必定是java.lang.Object类,因为只有它是没有父类的。
interfaces_count:
接口计数器,interfaces_count的值表示当前类或接口的直接父接口数量。
interfaces[]:
接口表,interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引值,它的长度为interfaces_count。每个成员interfaces[i] 必须为CONSTANT_Class_info类型常量。
fields_count:
字段计数器,fields_count的值表示当前Class文件fields[]数组的成员个数。
fields[]:
字段表,fields[]数组中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。
methods_count:
方法计数器,methods_count的值表示当前Class文件methods[]数组的成员个数。
methods[]:
方法表,methods[]数组中的每个成员都必须是一个method_info结构的数据项,用于表示当前类或接口中某个方法的完整描述。
attributes_count:
属性计数器,attributes_count的值表示当前Class文件attributes表的成员个数。
attributes[]:
属性表,attributes表的每个项的值必须是attribute_info结构。
四、一个java类的实例分析
为了了解JVM的数据类型规范和内存分配的大体情况,我新建了MemeryTest.java:

编译为MemeryTest.class后,通过WinHex查看该文件,对应字节码文件各个部分不同的定义,我了解了下面16进制数值的具体含义,尽管不清楚ClassLoader的具体实现逻辑,但是可以想象这样一个严谨格式的文件给JVM对于内存管理和执行程序提供了多大的帮助。

运行程序后,我在windows资源管理器中找到对应的进程ID.

并且在控制台通过jmap -heap 10016查看堆内存的使用情况:

输出结果中表示当前java进程启动的JVM是通过4个线程进行Parallel GC,堆的最小FreeRatio是40%,堆的最大FreeRatio是70%,堆的大小是4090M,新对象占用1.5M,Young Generation可以扩展到最大是1363M, Tenured Generation的大小是254.5M,以及NewRadio和SurvivorRadio中,下面更是具体给出了目前Young Generation中1.5M的划分情况,Eden占用1.0M,使用了5.4%,Space占了0.5M,使用了93%,To Space占了0.5M,使用了0%。
下面我们通过jmap dump把heap的内容打印打文件中:

使用Eclipse的MAT插件打开对应的文件:

选择第一项内存泄露分析报告打开test.bin文件,展示出来的是MAT关于内存可能泄露的分析。


从结果来看,有3个地方可能存在内存泄露,他们占据了Heap的22.10%,13.78%,14.69%,如果内存泄露,这里一般会有一个比值非常高的对象。打开第一个Probem Suspect,结果如下:

ShallowHeap是对象本身占用的堆大小,不包含引用,RetainedHeap是对象所持有的Shallowheap的大小,包括自己ShallowHeap和可以引用的对象的ShallowHeap。垃圾回收的时候,如果一个对象不再引用后被回收,那么他的RetainedHeap是能回收的内存总和。通过上图可以看出程序中并没有什么内存泄露,可以放心了。如果还有什么不太确定的对象,则可以通过多个时间点的HeapDumpFile来研究某个对象的变化情况。
以上便是我最近几天对JVM相关资料的整理,主要围绕他的基本组成和运行原理等,内存管理,节本数据类型和字节码文件。JVM是一个非常优秀的JAVA程序,也是个不错的规范,这次整理学习让我对他有了更加清晰的认知,对Java语言的理解也更加加深。
这次学习过程,坚定了我对程序员发展的认知。知识一定要精,下一步我将边工作边仔细阅读Oracle的3个版本的《JVM Specification》,并且结合实践让自己的Java基础素养更上一个层次。
不断更新中,请尽量访问博客原文。
ArrayList
以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。
按数组下标访问元素–get(i)/set(i,e) 的性能很高,这是数组的基本优势。
直接在数组末尾加入元素–add(e)的性能也高,但如果按下标插入、删除元素–add(i,e), remove(i), remove(e),则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。
LinkedList
以双向链表实现。链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作。
按下标访问元素–get(i)/set(i,e) 要悲剧的遍历链表将指针移动到位(如果i>数组大小的一半,会从末尾移起)。
插入、删除元素时修改前后节点的指针即可,但还是要遍历部分链表的指针才能移动到下标所指的位置,只有在链表两头的操作–add(), addFirst(),removeLast()或用iterator()上的remove()能省掉指针的移动。
CopyOnWriteArrayList
并发优化的ArrayList。用CopyOnWrite策略,在修改时先复制一个快照来修改,改完再让内部指针指向新数组。
因为对快照的修改对读操作来说不可见,所以只有写锁没有读锁,加上复制的昂贵成本,典型的适合读多写少的场景。如果更新频率较高,或数组较大时,还是Collections.synchronizedList(list),对所有操作用同一把锁来保证线程安全更好。
增加了addIfAbsent(e)方法,会遍历数组来检查元素是否已存在,性能可想像的不会太好。
补充
无论哪种实现,按值返回下标–contains(e), indexOf(e), remove(e) 都需遍历所有元素进行比较,性能可想像的不会太好。
没有按元素值排序的SortedList,在线程安全类中也没有无锁算法的ConcurrentLinkedList,凑合着用Set与Queue中的等价类时,会缺少一些List特有的方法。
HashMap
以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。
插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),Entry用一个next属性实现多个Entry以单向链表存放,后入桶的Entry将next指向桶当前的Entry。
查找哈希值为17的key时,先定位到第一个哈希桶,然后以链表遍历桶里所有元素,逐个比较其key值。
当Entry数量达到桶数量的75%时(很多文章说使用的桶数量达到了75%,但看代码不是),会成倍扩容桶数组,并重新分配所有原来的Entry,所以这里也最好有个预估值。
取模用位运算(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。
iterator()时顺着哈希桶数组来遍历,看起来是个乱序。
在JDK8里,新增默认为8的閥值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。
LinkedHashMap
扩展HashMap增加双向链表的实现,号称是最占内存的数据结构。支持iterator()时按Entry的插入顺序来排序(但是更新不算, 如果设置accessOrder属性为true,则所有读写访问都算)。
实现上是在Entry上再增加属性before/after指针,插入时把自己加到Header Entry的前面去。如果所有读写访问都要排序,还要把前后Entry的before/after拼接起来以在链表中删除掉自己。
TreeMap
以红黑树实现,篇幅所限详见入门教程。支持iterator()时按Key值排序,可按实现了Comparable接口的Key的升序排序,或由传入的Comparator控制。可想象的,在树上插入/删除元素的代价一定比HashMap的大。
支持SortedMap接口,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段。
ConcurrentHashMap
并发优化的HashMap,默认16把写锁(可以设置更多),有效分散了阻塞的概率,而且没有读锁。
数据结构为Segment[],Segment里面才是哈希桶数组,每个Segment一把锁。Key先算出它在哪个Segment里,再算出它在哪个哈希桶里。
支持ConcurrentMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现CAS的replace(key, oldValue, newValue)。
没有读锁是因为put/remove动作是个原子动作(比如put是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。
ConcurrentSkipListMap
JDK6新增的并发优化的SortedMap,以SkipList实现。SkipList是红黑树的一种简化替代方案,是个流行的有序集合算法,篇幅所限见入门教程。Concurrent包选用它是因为它支持基于CAS的无锁算法,而红黑树则没有好的无锁算法。
很特殊的,它的size()不能随便调,会遍历来统计。
补充
关于null,HashMap和LinkedHashMap是随意的,TreeMap没有设置Comparator时key不能为null;ConcurrentHashMap在JDK7里value不能为null(这是为什么呢?),JDK8里key与value都不能为null;ConcurrentSkipListMap是所有JDK里key与value都不能为null。
Set几乎都是内部用一个Map来实现, 因为Map里的KeySet就是一个Set,而value是假值,全部使用同一个Object。Set的特征也继承了那些内部Map实现的特征。
HashSet:内部是HashMap。
LinkedHashSet:内部是LinkedHashMap。
TreeSet:内部是TreeMap的SortedSet。
ConcurrentSkipListSet:内部是ConcurrentSkipListMap的并发优化的SortedSet。
CopyOnWriteArraySet:内部是CopyOnWriteArrayList的并发优化的Set,利用其addIfAbsent()方法实现元素去重,如前所述该方法的性能很一般。
补充:好像少了个ConcurrentHashSet,本来也该有一个内部用ConcurrentHashMap的简单实现,但JDK偏偏没提供。Jetty就自己封了一个,Guava则直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 实现。
Queue是在两端出入的List,所以也可以用数组或链表来实现。
–普通队列–
LinkedList
是的,以双向链表实现的LinkedList既是List,也是Queue。它是唯一一个允许放入null的Queue。
ArrayDeque
以循环数组实现的双向Queue。大小是2的倍数,默认是16。
普通数组只能快速在末尾添加元素,为了支持FIFO,从数组头快速取出元素,就需要使用循环数组:有队头队尾两个下标:弹出元素时,队头下标递增;加入元素时,如果已到数组空间的末尾,则将元素循环赋值到数组[0](如果此时队头下标大于0,说明队头弹出过元素,有空位),同时队尾下标指向0,再插入下一个元素则赋值到数组[1],队尾下标指向1。如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。
PriorityQueue
用二叉堆实现的优先级队列,详见入门教程,不再是FIFO而是按元素实现的Comparable接口或传入Comparator的比较结果来出队,数值越小,优先级越高,越先出队。但是注意其iterator()的返回不会排序。
–线程安全的队列–
ConcurrentLinkedQueue/ConcurrentLinkedDeque
无界的并发优化的Queue,基于链表,实现了依赖于CAS的无锁算法。
ConcurrentLinkedQueue的结构是单向链表和head/tail两个指针,因为入队时需要修改队尾元素的next指针,以及修改tail指向新入队的元素两个CAS动作无法原子,所以需要的特殊的算法,篇幅所限见入门教程。
PriorityBlockingQueue
无界的并发优化的PriorityQueue,也是基于二叉堆。使用一把公共的读写锁。虽然实现了BlockingQueue接口,其实没有任何阻塞队列的特征,空间不够时会自动扩容。
DelayQueue
内部包含一个PriorityQueue,同样是无界的。元素需实现Delayed接口,每次调用时需返回当前离触发时间还有多久,小于0表示该触发了。
pull()时会用peek()查看队头的元素,检查是否到达触发时间。ScheduledThreadPoolExecutor用了类似的结构。
–线程安全的阻塞队列–
BlockingQueue的队列长度受限,用以保证生产者与消费者的速度不会相差太远,避免内存耗尽。队列长度设定后不可改变。当入队时队列已满,或出队时队列已空,不同函数的效果见下表:
| 可能报异常 | 返回布尔值 | 可能阻塞等待 | 可设定等待时间 | |
| 入队 | add(e) | offer(e) | put(e) | offer(e, timeout, unit) |
| 出队 | remove() | poll() | take() | poll(timeout, unit) |
| 查看 | element() | peek() | 无 | 无 |
ArrayBlockingQueue
定长的并发优化的BlockingQueue,基于循环数组实现。有一把公共的读写锁与notFull、notEmpty两个Condition管理队列满或空时的阻塞状态。
LinkedBlockingQueue/LinkedBlockingDeque
可选定长的并发优化的BlockingQueue,基于链表实现,所以可以把长度设为Integer.MAX_VALUE。利用链表的特征,分离了takeLock与putLock两把锁,继续用notEmpty、notFull管理队列满或空时的阻塞状态。
补充
JDK7有个LinkedTransferQueue,transfer(e)方法保证Producer放入的元素,被Consumer取走了再返回,比SynchronousQueue更好,有空要学习下。
飞嗨cms
', ], [ '24', '24', 'QQ:1838889850
Email:admin#feehi.com(请将@替换成#)
', ], [ '25', '25', 'ddddddddddddddddddd
' ] ] ];