间时紧张,先记一笔,后续优化与完善。
我们晓得,Android用应序程是运行在Dalvik拟虚机里头的,并且每个用应序程对应有一个独单的Dalvik拟虚机实例。除了令指集和类件文式格不同,Dalvik拟虚机与Java拟虚机享共有差不多的特性,例如,它们都是释解执行,并且持支即时译编(JIT)、圾垃搜集(GC)、Java当地法方调用(JNI)和Java近程调试协议(JDWP)等。本文对Dalvik拟虚机行进要简分析,以及定制学习划计。
老罗的新浪微博:http://weibo.com/shengyangluo,迎欢注关!
Dalvik拟虚机是由Dan Bornstein发开的,名字来源于他的先人经曾栖身过的位于冰岛的同名小渔村。Dalvik拟虚机起源于Apache Harmony项目,后者是由Apache件软基金会主导的,目标是实现一个立独的、兼容JDK 5的拟虚机,并根据Apache License v2宣布。由此可见,Dalvik拟虚机从生诞的那一天开始,就和Java有说不清理一直的关系。
Dalvik拟虚机与Java拟虚机的最明显区分是它们分离有具不同的类件文式格以及令指集。Dalvik拟虚机用应的是dex(Dalvik Executable)式格的类件文,而Java拟虚机用应的是class式格的类件文。一个dex件文可以含包若干个类,而一个class件文只括包一个类。由于一个dex件文可以含包若干个类,因此它以可就将各个类中重复的字符串和其它常数只保存一次,从而省节了空间,这样就合适在内存和处置器度速无限的手机统系中用应。一般说来,含包有同相类的未压缩dex件文稍小于一个已压缩的jar件文。
Dalvik拟虚机用应的令指是基于寄存器的,而Java拟虚机用应的令指集是基于堆栈的。基于堆栈的令指很紧凑,例如,Java拟虚机用应的令指只占一个字节,因而称为字节码。基于寄存器的令指由于须要指定源地址和目标地址,因此须要占用更多的令指空间,例如,Dalvik拟虚机的某些令指须要占用两个字节。基于堆栈和基于寄存器的令指集各有劣优,一般而言,执行一样的功能,前者须要更多的令指(主要是load和store令指),而后者须要更多的令指空间。须要更多令指味意着要多占用CPU间时,而须要更多令指空间味意着令指缓冲(i-cache)更易容效失。
此外,还有一种观念为认,基于堆栈的令指更具可移植性,因为它不对目标呆板的寄存器行进任何假设。然而,基于寄存器的令指由于对目标呆板的寄存器行进了假设,因此,它更有利于行进AOT(ahead-of-time)优化。 所谓AOT,就是在释解语言序程运行之前,就先将它译编成当地呆板语言序程。AOT本质上是一种静态译编,它是是于对相JIT而言的,也就是说,前者是在序程运行前行进译编,而后者是在序程运行时行进译编。运行时译编味意着可以利用运行时信息来到得比拟静态译编更优化的代码,同时也味意不能行进某些级高优化,因为优化进程太耗时了。另一方面,运行前译编由于不占用序程运行间时,因此,它以可就不计间时成原来优化代码。无论AOT,还是JIT,终究的目标都是将释解语言译编为当地呆板语言,而当地呆板语言都是基于寄存器来执行的,因此,在某种程度来讲,基于寄存器的令指更有利于行进AOT译编以及优化。
事实上,基于寄存器和基于堆栈的令指集之争,就如简精令指集(RISC)和复杂令指集(CISC)之争,谁优谁劣,至今是没有定论的。例如,上面提到成完同相的功能,基于堆栈的Java拟虚机须要更多的令指,因此就会比基于寄存器的Dalvik拟虚机慢,然而,在2010年,Oracle在一个ARM设备上用应一个non-graphical Java benchmarks来比对Java SE Embedded和Android 2.2的能性,现发后者比前者慢了2~3倍。上述能性比拟论结以及数据可以参考以下两篇文章:
1. Virtual Machine Showdown: Stack Versus Registers
2. Java SE Embedded Performance Versus Android 2.2
基于寄存器的Dalvik拟虚机和基于堆栈的Java拟虚机的更多比拟和分析,还可以参考以下文章:
1. http://en.wikipedia.org/wiki/Dalvik_(software)
2. http://www.infoq.com/news/2007/11/dalvik
3. http://www.zhihu.com/question/20207106
不管论结如何,Dalvik拟虚机都在尽最大的力努来优化自身,这些办法括包:
1. 将多个类件文搜集到统一个dex件文中,以便省节空间;
2. 用应只读的内存映射法方加载dex件文,以便可以多进程享共dex件文,省节序程加载间时;
3. 前提调整好字节序(byte order)和字齐对(word alignment)法方,使得它们更合适于当地呆板,以便进步令指执行度速;
4. 尽量前提行进字节码验证(bytecode verification),进步序程的加载度速;
5. 通过重写字节码的法方来行进AOT优化。
这些优化办法的更体具描述可以参考Dalvik Optimization and Verification With dexopt一文。
分析完Dalvik拟虚机和Java拟虚机的区分后之,接下来我们再要简分析一下Dalvik拟虚机的其它特性,括包内存管理、圾垃搜集、JIT、JNI以及进程和线程管理。
一. 内存管理
Dalvik拟虚机的内存大体上可以分为Java Object Heap、Bitmap Memory和Native Heap三种。
Java Object Heap是用来配分Java对象的,也就是我们在代码new出来的对象都是位于Java Object Heap上的。Dalvik拟虚机在启动的时候,可以通过-Xms和-Xmx选项来指定Java Object Heap的最小值和最大值。为了防止Dalvik拟虚机在运行的进程中对Java Object Heap的小大行进调整而影响能性,我们可以通过-Xms和-Xmx选项来将它的最小值和最大值设置为等相。
Java Object Heap的最小和最大默认值为2M和16M,但是手机在出厂时,厂商会根据手机的配置况情来对其行进调整,例如,G1、Droid、Nexus One和Xoom的Java Object Heap的最大值分离为16M、24M、32M 和48M。我们可以通过ActivityManager类的成员函数getMemoryClass来得获Dalvik拟虚机的Java Object Heap的最大值。
ActivityManager类的成员函数getMemoryClass的实现如下所示:
public class ActivityManager { ...... /** * Return the approximate per-application memory class of the current * device. This gives you an idea of how hard a memory limit you should * impose on your application to let the overall system work best. The * returned value is in megabytes; the baseline Android memory class is * 16 (which happens to be the Java heap limit of those devices); some * device with more memory may return 24 or even higher numbers. */ public int getMemoryClass() { return staticGetMemoryClass(); } /** @hide */ static public int staticGetMemoryClass() { // Really brain dead right now -- just take this from the configured // vm heap size, and assume it is in megabytes and thus ends with "m". String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m"); return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1)); } ...... }
这个函数定义在件文frameworks/base/core/java/android/app/ActivityManager.java中。
Dalvik拟虚机在启动的时候,就是通过读取统系属性dalvik.vm.heapsize的值来得获Java Object Heap的最大值的,而ActivityManager类的成员函数getMemoryClass终究也通过读取这个统系属性的值来得获Java Object Heap的最大值。
这个Java Object Heap的最大值也就是我们时平所说的Android用应序程进程可以用应的最大内存。这里必须要意注的是,Android用应序程进程可以用应的最大内存指的是可以用来配分Java Object的堆。
Bitmap Memory也称为External Memory,它是用来处置图像的。在HoneyComb之前,Bitmap Memory是在Native Heap中配分的,但是这部分内存一样计入Java Object Heap中,也就是说,Bitmap占用的内存和Java Object占用的内存加起来不能超过Java Object Heap的最大值。这就是为什么我们在调用BitmapFactory相干的口接来处置大图像时,会抛出一个OutOfMemoryError常异的原因:
java.lang.OutOfMemoryError: bitmap size exceeds VM budget
在HoneyComb以及更高的版本中,Bitmap Memory就直接是在Java Object Heap中配分了,这样以可就直接接受GC的管理。
Native Heap就是在Native Code中用应malloc等配分出来的内存,这部分内存是不受Java Object Heap的小大制限的,也就是它可以由自用应,当然它是会到受统系的制限。但是有一点须要意注的是,不要因为Native Heap可以由自用应就滥用,因为滥用Native Heap会致使统系可用内存急剧增加,从而激发统系取采保守的办法来Kill失落某些进程,用来充补可用内存,这样会影响统系休会。
此外,在HoneyComb以及更高的版本中,我们可以在AndroidManifest.xml的application标签中加增一个值即是“true”的android:largeHeap属性来通知Dalvik拟虚机用应序程须要用应大较的Java Object Heap。事实上,在内存受限的手机上,即使我们将一个用应序程的android:largeHeap属性设置为“true”,也是不能加增它可用的Java Object Heap的小大的,而即便是可以通过这个属性来增大Java Object Heap的小大,一般况情也不应该用应该属性。为了进步统系的体整休会,我们须要做的是致力于下降用应序程的内存需求,而不是加增加增用应序程的Java Object Heap的小大,毕竟统系统共可用的内存是定固的,一个用应序程用得多了,就味意意其它用应序程用得少了。
二. 圾垃搜集(GC)
Dalvik拟虚机可以动自收回那些不再用应了的Java Object,也就是那些不再被引用了的Java Object。圾垃动自搜集制机将发开者从内存题问中解放出来,极大地进步了发开效率,以及进步了序程的可维护性。
我们晓得,在C或者C++中,发开者须要手动地管理在堆中配分的内存,但是这常常致使很多题问。例如,内存配分后之记忘放释,形成内存泄漏。又如,法非拜访那些已放释了的内存,激发序程溃崩。如果没有一个好的C或者C++用应序程发开框架,一般的发开者基本法无驭驾内存题问,因为序程大了后之,很易容形成失控。最要命的是,内存被损坏的时候,并不一定就是序程溃崩的时候,它就是一颗不定时炸弹,说不准什么时候会被引爆,因此,查找原因是非常难困的。
从这里我们也可以推断出,Android为什么会择选Java而不是C/C++来作来用应序程发开语言,就是为了可以让发开阔别内存题问,而将力精会合在业务上,发开出更多更好的APP来,从而迎头赶超iOS。当然,Android统系内存也存在大批的C/C++代码,这只要虑考能性题问,毕竟C/C++序程的运行能性体整上还是优于运行在拟虚机之上的Java序程的。不过,为了防止涌现内存题问,在Android统系外部的C++代码码,大批地用应了智能针指来动自管理对象的生命周期。择选Java来作为Android用应序程的发开语言,可以说是技巧与业商之间一个调和,事实证明,这类调和是功成的。
回到正题,在GingerBread之前,Dalvik拟虚用应的圾垃搜集制机有以下特色:
1. Stop-the-word,也就是圾垃搜集线程在执行的时候,其它的线程都停止;
2. Full heap collection,也就是一次搜集全完体的圾垃;
3. 一次圾垃搜集形成的序程止中间时平日都大于100ms。
在GingerBread以及更高的版本中,Dalvik拟虚用应的圾垃搜集制机到得了改良,如下所示:
1. Cocurrent,也就是大多数况情下,圾垃搜集线程与其它线程是并发执行的;
2. Partial collection,也就是一次可能只搜集一部分圾垃;
3. 一次圾垃搜集形成的序程止中间时平日都小于5ms。
Dalvik拟虚机执行成完一次圾垃搜集后之,我们平日可以看到相似以下的日记出输:
D/dalvikvm(9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
在这一行日记中,GC_CONCURRENT示表GC原因,2049K示表统共收回的内存,3571K/9991K示表Java Object Heap统计,即在9991K的Java Object Heap中,有3571K是正在用应的,4703K/5261K示表External Memory统计,即在5261K的External Memory中,有4703K是正在用应的,2ms+2ms示表圾垃搜集形成的序程止中间时。
三. 即时译编(JIT)
后面提到,JIT是绝对AOT而言的,即JIT是在序程运行的进程中行进译编的,而AOT是在序程运行前行进译编的。在序程运行的进程中行进译编既有利益,也有处坏。利益在于可以利用序程的运行时信息来对译编出来的代码行进优化,而处坏在于占用序程的运行间时,也就是说不能花太多间时在代码译编和优化之上。
为了决解间时题问,JIT可能只会择选那些热门代码行进译编或者优化。根据2-8则原,一个序程80%的间时可能都是在重复执行20%的代码。因此,JIT以可就择选这20%经常执行的代码来行进译编和优化。
为了充分利地用好运行时信息来优化代码,JIT采取一种保守的法方。JIT在译编代码的时候,会对序程的运行况情行进假设,并且按照这类假设来对代码行进优化。随着序程的代码,如果后面的假设直一持保建立,那么JIT就什么也不用做,因此以可就进步序程的运行能性。一旦后面的假设不再建立了,那么JIT就须要对后面译编优化的代码行进调整,以便顺应新的况情。这类调整本钱多是很昂贵的,但是只要假设不建立的况情很少或者几乎不会产生,那么得获的利益还是大于处坏的。由于JIT在译编和优化代码的时候,对序程的运行况情行进了假设,因此,它所取采的保守优化办法又称为博赌,即Gambling。
我们以一个例子说来明这类Gambling。我们晓得,Java的同步原语涉及到Lock和Unlock作操。Lock和Unlock作操是非常耗时的,而且它们只有在多线程境环中才真的须要。但是一些同步函数或者同步代码,有序程运行的时候,有可能终始都是被单线程执行,也就是说,这些同步函数或者同步代码不会被多线程同时执行。这时候JIT以可就取采一种Lazy Unlocking制机。
当一个线程T1进入到一个同步代码C时,它还是按照常正的程流来获得一个轻量级锁L1,并且线程T1的ID会录记在轻量锁L1上。当经程T1分开同步函数或者同步代码时,它并不会放释后面得获的轻量级锁L1。当线程T1再次进入同步代码C时,它就会现发轻量级锁L的全体者是正自己,因此,它以可就直接执行同步代码C。这时候如果另外一个线程T2也要进入同步代码C,它就会现发轻量级锁L已被线程T1获得。在这类况情下,JIT就须要检查线程T1的调用堆栈,看看它否是还在执行同步代码C。如果是的话,那么就须要将轻量级锁L1转换成一个重量级锁L2,并且将重量级锁L2的状态设置为定锁,然后再让线程T2在重量级锁L2上眠睡。等线程T1执行成完同步代码C后之,它就会按照常正的程流来放释重量级锁L2,从而唤醒线程T2来执行同步代码C。另一方面,如果线程T2在进入同步代码C的时候,JIT通过检查线程T1的调用堆栈,现发它已分开同步代码C了,那么它就直接将轻量级锁L1的全体者录记为线程T2,并且让线程T2执行同步代码C。
通过上述的Lazy Unlocking制机,我们以可就充分利地用序程的运行时信息来进步序程的执行能性,这类优化对于静态译编的语言说来,是法无做到的。从这个角度来看,我们以可就说,静态译编语言(如C++)并不一定比在拟虚机上执行的语言(如Java)快,这是因为后者可以有一种壮大的器武叫做JIT。
Dalvik拟虚机从Android 2.2版本开始,才持支JIT,而且是可选的。在译编Dalvik拟虚机的时候,可以通过WITH_JIT宏来将JIT也译编进去,而在启动Dalvik拟虚机的时候,可以通过-Xint:jit选项来开启JIT功能。
关于拟虚机JIT的实现道理的要简分析,可以进一步参考这篇文章:http://blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html。
四. Java当地调用(JNI)
无论如何,拟虚机终究都是运行在目标呆板之上的,也就是说,它须要将自己的令指翻译成目标呆板令指来执行,并且有些功能,须要通过调用目标呆板运行的作操统系口接来成完。这样就须要有一个制机,使得函数调用可以从Java层穿越到Native层,也就是C/C++层。这类制机就称为Java当地调用,即JNI。当然,我们在执行Native代码的时候,有时候也是须要调用到Java函数的,这一样是可以通过JNI制机来实现。也就是说,JNI制机既持支在Java函数中调用C/C++函数,也持支在C/C++函数中调用Java函数。
事实上,Dalvik拟虚机供提的Java运行时库,大部分都是通过调用目标呆板作操统系口接来实现的,也就是通过调用Linux统系口接来实现的。例如,当我们调用android.os.Process类的成员函数start来创立一个进程的时候,终究会调用到Linux统系供提的fork统系调用来创立一个进程。
同时,为了便利发开者用应C/C++语言来发开用应序程,Android官方供提了NDK。通过NDK,我们以可就用应JNI制机来在Java函数中调用到C/C++函数。不过Android官方是不提倡用应NDK来发开用应序程的,这从它对NDK的持支远远不如SDK的持支以可就看得出来。
五. 进程和线程管理
一般说来,拟虚机的进程和线程都是与目标呆板当地作操统系的进程和线程一一对应的,这样做的利益是可以使当地作操统系来调度进程和线程。进程和线程调度是作操统系的心核模块,它的实现是非常复杂的,特别是虑考到多核的况情,因此,就全完没有必要在拟虚机中供提一个进程和线程库。
Dalvik拟虚机运行在Linux作操统系之上。我们晓得,Linux作操统系并没有纯粹的线程念概,只要两个进程享共统一个地址空间,那么以可就为认它们统一个进程的两个线程。Linux作操统系供提了两个fork和clone两个调用,其中,前者就是用来创立进程的,而后者就是用来创立线程的。关于Linux作操统系的进程和线程的实现,可以参考在后面Android学习启动篇一文中提到的经典Linux内核书籍。
关于Android用应序程进程,它有两个很大的特色,上面我们就要简分析一下。
第一个特色是每个Android用应序程进程都有一个Dalvik拟虚机实例。这样做的利益是Android用应序程进程之间不会相互影响,也就是说,一个Android用应序程进程的外意止中,不会影响到其它的Android用应序程进程的常正运行。
第二个特色是每个Android用应序程进程都是由一种称为Zygote的进程fork出来的。Zygote进程是由init进程启动起来的,也就是在统系启动的时候启动的。Zygote进程在启动的时候,会创立一个拟虚机实例,并且在这个拟虚机实例将全体的Java心核库都加载起来。每当Zygote进程须要创立一个Android用应序程进程的时候,它就通过制复自身来实现,也就是通过fork统系调用来实现。这些被fork出来的Android用应序程进程,一方面是制复了Zygote进程中的拟虚机实例,另一方面是与Zygote进程享共了统一套Java心核库。这样不仅Android用应序程进程的创立进程很快,而且由于全体的Android用应序程进程都享共统一套Java心核库而省节了内存空间。
关于Dalvik拟虚机的特性,我们就要简分析到这里。事实上,Dalvik拟虚机和Java拟虚机的实现是相似的,例如,Dalvik拟虚机也持支JDWP(Java Debug Wire Protocol)协议,这样我们以可就用应DDMS来调试运行在Dalvik拟虚机中的进程。对Dalvik拟虚机的其它特性或者实现道理有趣兴的,议建都可以参考Java拟虚机的实现,这里供提三本参考书:
1. Java Virtual Machine Specification (Java SE 7)
2. Inside the Java Virtual Machine, Second Edition
3. Oracle JRockit: The Definitive Guide
另外,关于Dalvik拟虚机的令指集和dex件文式格的分析,可以参考官方档文:http://source.android.com/tech/dalvik/index.html。如果对拟虚机的实现道理有趣兴的,还可以参考这个链接:http://www.weibo.com/1595248757/zvdusrg15。
在这里,我们学习Dalvik拟虚机的目标是通打Java层到C/C++层之间的函数调用,从而可以更好地解理Android用应序程是如在何Linux内核上面运行的。为了到达这个的目,在接下来的文章中,我们将注关以下三个景情:
1. Dalvik拟虚机的启动进程;
2. JNI函数的册注和调用进程;
3. Java进程和线程的创立进程。
把握了这三个景情后之,再结合后面的全体文章,我们以可就从上到下地通打整个Android统系了,敬请注关!
老罗的新浪微博:http://weibo.com/shengyangluo,迎欢注关!
文章结束给大家分享下程序员的一些笑话语录: 一位程序员去海边游泳,由于水性不佳,游不回岸了,于是他挥着手臂,大声求.救:“F1,F1!”