众所周知,Java应用程序是运行在JVM上的,可是你对JVM有所了解么?做为这个系列文章的第一篇,本文将对经典Java虚拟机的运行机制作简单介绍,内容包括“一次编写,处处运行”的利弊、垃圾回收的基本原理、经常使用垃圾回收算法的示例和编译器优化等。后续的系列文章将会JVM性能优化的内容进行介绍,包括新一代JVM的设计思路,以及如何支持当今Java应用程序对高性能和高扩展性的要求。html
若是你是一名程序员,那么毫无疑问,你确定有过某种兴奋的感受,就像是当一束灵感之光照亮了你思考方向,又像是神经元最终创建链接,又像是你解放思想开拓了新的局面。就我我的来讲,我喜欢这种学习新知识的感受。我在工做时就经常会有这种感受,个人工做会涉及到一些JVM的相关技术,这着实令我兴奋,尤为是工做涉及到垃圾回收和JVM性能优化的时候。在这个系列中,我但愿能够与你分享一些这方面的经验,但愿你也会像我同样热爱JVM相关技术。java
这个系列文章主要面向那些想要裂解JVM底层运行原理的Java程序员。文章立足于较高的层面展开讨论,内容涉及到垃圾回收和在不影响应用程序运行的状况下对安全快速的释放/分配内存。你将对JVM的核心模块有所了解:垃圾回收、GC算法、编译器行为,以及一些经常使用优化技巧。此外,还会讨论为何对Java作基准测试(benchmark)是件很困难的事,并提供一些建议来帮助作基准测试。最后,将会介绍一些JVM和GC的前沿技术,内容涉及到Azul的Zing JVM,IBM JVM和Oracle的Garbage First(G1)垃圾回收器。git
但愿在阅读此系列文章后,你能对影响Java伸缩性的因素有所了解,而且知道这些因素是如何影响Java开发的,如何使Java难以优化的。但愿会你有那种发自心里的惊叹,而且可以激励你为Java作一点事情:拒绝限制,努力改变。若是你还没准备好为开源事业贡献力量,但愿本系列文章能够为你指明方向。程序员
JVM职业生涯github
在我职业生涯的早期,垃圾回收的问题曾经很难解决。垃圾回收问题和JVM的跨平台问题我更加为JVM和中间件的相关技术而着迷。我对JVM的热情源于十年前在JRockit团队工做的经历,当时要编码实现一种新的、可以自动学习、自动调优的垃圾回收算法(参见相关资源)。从那个项目开始,我踏上了JVM技术之旅,期间在BEA System公司工做的不少年,与Intel公司和Sun公司有过合做关系,在Oracle收购BEA公司和Sun公司以后为Oracle工做了一年。另外,个人硕士论文深刻分析了JRockit的试验性特性,为Deterministic Garbage Collection算法打下了基础。当我加入Azul公司的团队后,我回到了熟悉的工做中,负责管理维护Zing JVM的垃圾回收算法。如今个人工做有了一点小变化,负责日程安排与资源管理,关注分布式的可伸缩数据处理框架,目前在Cloudera公司工做,负责开源项目Hadoop的开发。算法
有很多人认为,Java平台自己就挺慢。其主要观点简单来讲就是,Java性能低已经有些年头了 ―― 最先能够追溯到Java第一次用于企业级应用程序开发的时候。但这早就是老黄历了。事实是,若是你对不一样的开发平台上运行简单的、静态的、肯定性任务的运行结果作比较,你就会发现使用通过机器级优化(machine-optimized)代码的平台比任何使用虚拟环境进行运算的都要强,JVM也不例外。可是,在过去的10年中,Java的性能有了大幅提高。市场上不断增加的需求催生了垃圾回收算法的出现和编译技术的革新,在不断探索与优化的过程当中,JVM茁壮成长。在这个系列文章中,我将介绍其中的一些内容。编程
JVM技术中最迷人的地方也正是其最具挑战性的地方:“一次编写,处处运行”。JVM并不对具体的用例、应用程序或用户负载进行优化,而是在应用程序运行过程当中不断收集运行时信息,并以此为根据动态的进行优化。这种动态的运行时特性带来了不少动态问题。在设计优化方案时,以JVM为工做平台的程序没法依靠静态编译和可预测的内存分配速率(predictable allocation rates)对应用程序作性能评估,至少在对生产环境进行性能评估时是不行的。安全
机器级优化过的代码有时能够达到更好的性能,但它是以牺牲可移植性为代价的,在企业级应用程序中,动态负载和快速迭代更新是更加剧要的。大多数企业会愿意牺牲一点机器级优化代码带来的性能,以此换取Java平台的诸多优点:性能优化
做为一名Java程序员,你能够已经对编码、编译和运行这一套流程比较熟悉了。假如说,如今你写了一个程序代码MyApp.java,准备编译运行。为了运行这个程序,首先,你须要使用JDK内建的Java语言编译器,javac,对这个文件进行编译,它能够将Java源代码编译为字节码。javac将根据Java程序的源代码生成对应的可执行字节码,并将其保存为同名类文件:MyApp.class。在通过编译阶段后,你就能够在命令行中使用java命令或其余启动脚本载入可执行的类文件来运行程序,而且能够为程序添加启动参数。以后,类会被载入到运行时(这里指的是正在运行的JVM),程序开始运行。服务器
上面所描述的就是在运行Java应用程序时的表面过程,但如今,咱们要深刻挖掘一下,在调用Java命令时,到底发生了什么?JVM究竟是什么?大多数程序员是经过不断的调优,即便用相应的启动参数,与JVM进行交互,使Java程序运行的更快,同时避免程序出现“out of memory”错误。但你是否想过,为何咱们必需要经过JVM来运行Java应用程序呢?
简单来讲,JVM是用于执行Java应用程序和字节码的软件模块,而且能够将字节码转换为特定硬件和特定操做系统的本地代码。正因如此,JVM使Java程序作到了“一次编写,处处运行”。Java语言的可移植性是获得企业级应用程序开发者青睐的关键:开发者无需因平台不一样而把程序从新编写一遍,由于有JVM负责处理字节码到本地代码的转换和平台相关优化的工做。
基本上来讲,JVM是一个虚拟运行环境,对于字节码来讲就像是一个机器同样,能够执行任务,并经过底层实现执行内存相关的操做。
JVM也能够在运行java应用程序时,很好的管理动态资源。这指的是他能够正确的分配、回收内存,在不一样的上维护一个具备一致性的线程模型,而且能够为当前的CPU架构组织可执行指令。JVM解放了程序员,使程序员没必要再关系对象的生命周期,使程序员没必要再关心应该在什么时候释放内存。而这,正是使用着相似C语言的非动态语言的程序员心中永远的痛。
你能够将JVM当作是一种专为Java而生的特殊的操做系统,它的工做是管理运行Java应用程序的运行时环境。简单来讲,JVM就是运行字节码指令的虚拟执行环境,而且能够分配执行任务,或经过底层实现对内存进行操做。
关于JVM内部原理与性能优化有不少内容可写。做为这个系列的开篇文章,我简单介绍JVM的内部组件。这个简要介绍对于那些JVM新手比较有帮助,也是为后面的深刻讨论作个铺垫。
编译器
以一种语言为输入,生成另外一种可执行语言做为输出。Java编译器主要完成2个任务:
编译器能够是静态的,也能够是动态的。静态编译器,如javac,它以Java源代码为输入,将其编译为字节码(一种能够运行JVM中的语言)。*静态编译器*解释输入的源代码,而生成可执行输出代码则会在程序真正运行时用到。由于输入是静态的,全部输出结果老是相同的。只有当你修改的源代码并从新编译时,才有可能看到不一样的编译结果。
动态编译器,如使用Just-In-Time(JIT,即时编译)技术的编译器,会动态的将一种编程语言编译为另外一种语言,这个过程是在程序运行中同时进行的。JIT编译器会收集程序的运行时数据(在程序中插入性能计数器),再根据运行时数据和当前运行环境数据动态规划编译方案。动态编译能够生成更好的序列指令,使用更有效率的指令集合替换原指令集合,或剔除冗余操做。收集到的运行时数据的越多,动态编译的效果就越好;这一般称为代码优化或重编译。
动态编译使你的程序能够应对在不一样负载和行为下对新优化的需求。这也是为何动态编译器很是适合Java运行时。这里须要注意的地方是,动态编译器须要动用额外的数据结构、线程资源和CPU指令周期,才能收集运行时信息和优化的工做。若想完成更高级点的优化工做,就须要更多的资源。可是在大多数运行环境中,相对于得到的性能提高来讲,动态编译的带来的性能损耗实际上是很是小的 ―― 动态编译后的代码的运行效率能够比纯解释执行(即按照字节码运行,不作任何修改)快5到10倍。
内存分配
是以线程为单位,在“Java进程专有内存地址空间”中,也就是Java堆中分配的。在普通的客户端Java应用程序中,内存分配都是单线程进行的。可是,在企业级应用程序和服务器端应用程序中,单线程内存分配却并非个好办法,由于它没法充分利用现代多核时代的并行特性。
并行应用程序设计要求JVM确保多线程内存分配不会在同一时间将同一块地址空间分配给多个线程。你能够在整个内存空间中加锁来解决这个问题,可是这个方法(即所谓的“堆锁”)开销较大,由于它迫使全部线程在分配内存时逐个执行,对资源利用和应用程序性能有较大影响。多核程序的一个额外特色是须要有新的资源分配方案,避免出现单线程、序列化资源分配的性能瓶颈。
经常使用的解决方案是将堆划分为几个区域,每一个区域都有适当的大小,固然具体的大小须要根据实际状况作相应的调整,由于不一样应用程序之间,内存分配速率、对象大小和线程数量的差异是很是大的。Thread Local Allocation Buffer(TLAB),有时也称为Thraed Local Area(TLA),是线程本身使用的专用内存分配区域,在使用的时候无需获取堆锁。当这个区域用满的时候,线程会申请新的区域,直到堆中全部预留的区域都用光了。当堆中没有足够的空间来分配内存时,堆就“满”了,即堆上剩余的空间装不下待分配空间的对象。当堆满了的时候,垃圾回收就开始了。
使用TLAB的一个风险是,因为堆上内存碎片的增长,使用内存的效率会降低。若是应用程序建立的对象的大小没法填满TLAB,而这块TLAB中剩下的空间又过小,没法分配给新的对象,那么这块空间就被浪费了,这就是所谓的“碎片”。若是“碎片”周围已分配出去的内存长时间没法回收,那么这块碎片研究长时间没法获得利用。
碎片化
是指堆上存在了大量的碎片
,因为这些小碎片的存在而使堆没法获得有效利用,浪费了堆空间。为应用程序设置TLAB的大小时,如果没有对应用程序中对象大小和生命周期和合理评估,致使TLAB的大小设置不当,就会是使堆逐渐碎片化。随着应用程序的运行,被浪费的碎片空间会逐渐增多,致使应用程序性能降低。这是由于系统没法为新线程和新对象分配空间,因而为防止出现OOM(out-of-memory)错误,而频繁GC的缘故。
对于TLAB产生的空间浪费这个问题,能够采用“曲线救国”的策略来解决。例如,能够根据应用程序的具体环境调整TLAB的大小。这个方法既能够临时,也能够完全的避免堆空间的碎片化,但须要随着应用程序内存分配行为的变化而修改TLAB的值。此外,还可使用一些复杂的JVM算法和其余的方法来组织堆空间来得到更有效率的内存分配行为。例如,JVM能够实现空闲列表(free-list),空闲列表中保存了堆中指定大小的空闲块。具备相似大小空闲块保存在一个空闲列表中,所以能够建立多个空闲列表,每一个空闲列表保存某个范围内的空闲块。在某些事例中,使用空闲列表会比使用按实际大小分配内存的策略更有效率。线程为某个对象分配内存时,能够在空闲列表中寻找与对象大小最接近的空间块使用,相对于使用固定大小的TLAB,这种方法更有利于避免碎片化的出现。
GC往事
早期的垃圾回收器有多个老年代,但实际上,存在多个老年代是弊大于利的。
另外一种对抗碎片化的方法是建立一个所谓的年轻代,在这个专有的堆空间中,保存了全部新建立的对象。堆空间中剩余的空间就是所谓的老年代。老年代用于保存具备较长生命周期的对象,即当对象可以挺过几轮GC而不被回收,或者对象自己很大(通常来讲,大对象都具备较长的寿命周期)时,它们就会被保存到老年代。为了让你可以更好的理解这个方法,咱们有必要谈谈垃圾回收。
垃圾回收就是JVM释放那些没有引用指向的堆内存的操做。当垃圾回收首次触发时,有引用指向的对象会被保存下来,那些没有引用指向的对象占用的空间会被回收。当全部可回收的内存都被回收后,这些空间就能够被分配给新的对象了。
垃圾回收不会回收仍有引用指向的对象;不然就会违反JVM规范。这个规则有一个例外,就是对软引用或弱引用的使用,当垃圾回收器发现内存快要用完时,会回收只有软引用或弱引用指向的对象所占用的内存。个人建议是,尽可能避免使用弱引用,由于Java规范中存在的模糊的表述可能会使你对弱引用的使用产生误解。此外,Java自己是动态内存管理的,你不必考虑何时该释放哪块内存。
对于垃圾回收来讲,挑战在于,如何将垃圾回收对应用程序形成的影响降到最小。若是垃圾回收执行的不充分,那么应用程序早晚会发生OOM错误;若是垃圾回收执行的太频繁,会对应用程序的吞吐量和响应时间形成影响,固然,这都不是好的影响。
目前已经出现了不少垃圾回收算法。在这个系列文章中将对其中的一些进行介绍。归纳来讲,垃圾回收主要有两种方式,引用计数(reference counting)和引用追踪(reference tracing)。
上面提到的两种算法有多种不一样的实现方法,其中最著名可算是标记或拷贝算法(marking or copying algorithm)和并行或并发算法(parallel or concurrent algorithm)。我将在后续的文章中对它们进行介绍。
分代垃圾回收的意思是,将堆划分为几个不一样的区域,分别用于存储新对象和老对象。其中“老对象”指的是挺过了几轮垃圾回收而不死的对象。将堆空间分为年轻代和老年代,分别用于存储新对象和老对象能够经过回收生命周期较短的对象,并将生命周期较长的对象从年轻代提高到老年代的方法来减小堆空间中的碎片,下降堆空间碎片化的风险。此外,使用年轻代还有一个好处是,它能够推出对老年代进行垃圾回收的需求(对老年代进行垃圾回收的代价比较大,由于老年代中那些生命周期较长的对象一般包含有更多的引用,遍历一次须要花费更多的时间),因那些生命周期较短的对一般会重用年轻代中的空间。
还有一个值得一提的算法改进是压缩,它能够用来管理堆空间中的碎片。基本上将,压缩就是将对象移动到一块儿,再释放掉较大的连续空间。若是你对磁盘碎片和处理磁盘碎片的工具比较熟悉的话你就会理解压缩的含义了,只不过这里的压缩是工做在Java堆空间中的。我将在该系列后续的内容中对压缩进行介绍。
JVM实现了可移植性(“一次编写,处处运行”)和动态内存管理,这两个特色也是其广受欢迎,而且具备较高生产力的缘由。
做为这个系列文章的第一篇,我介绍了编译器如何将字节码转换为平台相关指令的语言,以及如何动态
优化Java程序的运行性能。不一样的编译器迎合了不一样应用程序的须要。
此外,简单介绍了内存分配和垃圾回收的一点内容,及其与Java应用程序性能的关系。基本上将,Java应用程序运行的速度越快,填满Java堆所需的时间就越短,触发垃圾回收的频率也越高。这里遇到的问题就是,在应用程序出现OOM错误以前,如何在对应用程序形成的影响尽量小的状况下,回收足够多的内存空间。将后续的文章中,咱们将对传统垃圾回收方法和现今的垃圾回收方法对JVM性能优化的影响作详细讨论。
Eva Andearsson对JVM技术、SOA、云计算和其余企业级中间件解决方案有着10多年的从业经验。在2001年,她以JRockit JVM开发者的身份加盟了创业公司Appeal Virtual Solutions(即BEA公司的前身)。在垃圾回收领域的研究和算法方面,EVA得到了两项专利。此外她仍是提出了肯定性垃圾回收(Deterministic Garbage Collection),后来造成了JRockit实时系统(JRockit Real Time)。在技术上,Eva与SUn公司和Intel公司合做密切,涉及到不少将JRockit产品线、WebLogic和Coherence整合的项目。2009年,Eva加盟了Azul System公,担任产品经理。负责新的Zing Java平台的开发工做。最近,她改换门庭,以高级产品经理的身份加盟Cloudera公司,负责管理Cloudera公司Hadoop分布式系统,致力于高扩展性、分布式数据处理框架的开发。
英文原文:JVM performance optimization, Part 1,翻译:ImportNew - 曹旭东