目录
Java虚拟机:
1.概述:
我们经常说的JDK(Java DeveIopment Kit)包含了Java语言,Java虚拟机和Java API类库这三部分。这是Java程序开发的最小环境。而JRE(Java Runtime Environment)包含了Java API中的Java SE API子集和Java虚拟机这两部分,这是Java程序运行的标准环境。而JVM是整个Java平台的基石,是Java语言编译代码的运行平台。可以把JVM看成是一个抽象的计算机,它有各种的指令集和各种运行时数据区域,虽然叫Java虚拟机,但是在它之上运行的不仅仅是Java,还包括kotlin,Groovy,Jython等等。
1.1:Java虚拟机家族:
到现在为止出现了很多的虚拟机,也消亡了很多的虚拟机,这里写一些主流的Java虚拟机:
- HotSpot VM:Oracle JDK和Open JDK中自带的虚拟机,是目前最主流和使用范围最广泛的Java虚拟机。在介绍Java虚拟机时,如果不做特殊声明,都是介绍HotSpot VM的。
- J9 VM:J9 VM是IBM开发的虚拟机,目前是其主力发展的Java虚拟机,市场定位和1接近。
- Zing VM:它是以HotSpot VM为基础,改进了许多影响延迟的细节。卖点有:低延迟;启动之后快速预热功能;开管理性;零开销,可以在生产环境全时开启,整合在JVM内的监控工具Zing Vision。
注意:Android的Dalvik和ART虚拟机并不属于Java虚拟机。
1.2:Java虚拟机执行的流程:
当我们在执行一个Java程序时,它的执行流程为:
从图中可以知道Java虚拟机执行流程分为两部分,分别是编译时环境和运行时环境,一个Java文件经过Java编译器编译之后会生成class文件,这个class文件会由Java虚拟机来处理。所以Java虚拟机与Java语言没有什么必然的联系,它只和特定的二进制文件class文件有关联,因此无论任何的语言只要可以编译为class文件,就可以被Java虚拟机识别并且执行。
2.Java虚拟机结构:
这里的Java虚拟机结构,指的是Java虚拟机的抽象行为,而不是具体的,比如HotSpot VM的实现。按照Java虚拟机规范,抽象的Java虚拟机如图:
补充:
- 执行引擎(Execution Engine):是虚拟机最核心的组件之一,对加载的二进制字节码命令进行解析。
- 本地接口(Native Interface):融合不同开发语言的原生库为Java所用。
2.1:Class文件格式:
Java文件被编译之后生成了class文件,这种二进制文件不依赖于特定的硬件和操作系统。每一个class文件中都对应着唯一的类或者接口的定义信息,但是类或者接口并不一定定义在文件中。比如类和接口就可以通过类加载器来直接生成。class文件的格式内容具体参考网上内容。
2.2:类的生命周期:
一个Java文件被加载到Java虚拟机内存中到从内存中卸载的过程被称为类的生命周期。类的生命周期包括的阶段是:加载,链接,使用和卸载。其中链接又分为验证,准备和解析三个阶段。因此类的生命周期包括了7个阶段。广义来说类的加载包括了类的生命周期的5个阶段(不包括使用和卸载)。
大概介绍类的加载各个阶段的工作:
- 加载:查找并且加载Class文件。
- 链接:包括验证,准备和解析。
- 验证:确保被导入类型的正确性。
- 准备:为类的静态字段分配字段,并且使用默认值初始化这些字段。
- 初始化:将类的变量初始化为正确的初始值。
详细的看加载阶段1:
- 通过一个类的全限定名去找到其对应的.class文件,(它也是类加载子系统来完成的)
- 将这个.class文件内的二进制数据读取出来,转化成方法区的运行时数据结构,
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
2.3:类加载子系统:
类加载子系统通过多种类加载器来查找和加载class文件到Java虚拟机中,Java虚拟机有两种类加载器:系统加载器和自定义加载器。其中系统加载器包括以下三种:
- Bootstrap ClassLoader(引导类加载器):用C/C++代码实现的加载器,用于加载指定的JDK的核心类库。Java虚拟机的启动就是通过引导类加载器创建一个初始类来完成的。该加载器不能被Java代码访问到,但是可以查询某个类是否被引导类加载器加载过。
- Extensions ClassLoader(拓展类加载器):用于加载Java的拓展类,提供除了系统类之外的额外功能。
- Application ClassLoader(应用程序类加载器):也叫做系统类加载器(System ClassLoader),这个类加载器可以通过ClassLoader的getSystemClassLoader方法获取到。
2.4:运行时数据区域:
Java虚拟机在执行Java程序的过程中会把它所管理的内存区域划分为不同的数据区域。
2.4.1:程序计数器:
为了保证程序能够连续的执行下去,处理器必须具有某些手段来确定下一条指令的地址,而程序计数器就是起到这样的作用。程序计数器也叫做PC寄存器,是一块比较小的内存空间。在虚拟机概念模型中,字节码解释器工作的时候就是通过改变程序计数器来选取下一条需要执行的字节码指令的,Java虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,在一个确定的时刻只有一个处理器执行一条线到程中的指令,为了在线程切换后能恢复到正确的执行位置,每个线程都会有一个独立的程序计数器,因此,程序计数器是线程私有的。如果线程执行的方法不是Native方法,则程序计数器保存正在执行的字节码指令地址,如果是Native方法则程序计数器的值为空(Undefined)。程序计数器是Java虚拟机规范中唯一没有规定任何OutOfMemoryError情况的数据区域。
2.4.2:Java 虚拟机栈:
每一条Java 虚拟机线程都有一个线程私有的Java 虚拟机栈(Java Virtual MachineStacks)。它的生命周期与线程相同,与线程是同时创建的。Java虚拟机栈存储线程中Java方法调用的状态,包括局部变量、参数、返回值以及运算的中间结果等。一个Java虚拟机栈包含了多个栈帧,一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java虚拟机栈中,加在该方法执行完成后,这个栈帧就从Java虚拟机栈中弹出。我们平常所说的栈内存(Stack)指的是Java虚拟机栈。Java虚拟机规范中定义了种异常情况:
- 如果线程请求分配的栈容量超过Java虚拟机所允许的最大容量,Java虚拟机会抛出StackOverflowError。
- 如果Java虚拟机栈可以动态扩展(大部分Java虚拟机都可以动态扩展),但是扩展时无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的Java虚拟机栈,则会抛出OutOfMemoryError异常。
2.4.3:本地方法:
Java虚拟机实现可能要用到C Stacks来支持Native语言,这个C Stacks就是本地方法栈(Native Method Siack)。它与Java虚拟机栈类似,只不过本地方法栈是用来支持Native
方法的。如果Java虚拟机不支持Native方法,并且也不依赖于C Stacks,可以无须支持本地方法栈。在Java虚拟机规范中对本地方法栈的语言和数据结构等没有强制规定,因此具体的Java虚拟机可以自由实现它,比如HotSpot VM将本地方法栈和Java虚拟机栈合二为一。与Java虚拟机栈类似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
2.4.4:Java堆:
Java堆(Java Heap)是被所有线程共享的运行时内存区域。Java堆用来存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆存储的对象被垃圾收集器管理,这些管理的对象无法显式地销毁。从内存回收的角度来分,Java堆可以粗略地分为新生代和老年代,从内存分配的角度Java堆中可能划分出多个线程私有的分配缓冲区。不管如何划分,Java堆存储的内容是不变的,进行划分是为了能更快地回收或者分配内存。Java堆的容量可以是固定的,也可以动态扩展。Java堆所使用的内存在物理上不需要连续,逻辑上连续即可。Java虚拟机规范中定义了一种异常情况:如果在堆中没有足够的内存来完成实例分配,并且堆也无法进行扩展时,则会抛出OutOfMemoryError异常。
2.4.5:方法区:
方法区(Method Area)是被所有线程共享的运行时内存区域,用来存储已经被Jars虚拟机加载的类的结构信息,包括运行时常量池、字段和方法信息、静态变量等数据。方法区是Java堆的逻辑组成部分,它一样在物理上不需要连续,并且可以选择在方法区中不实现垃圾收集。方法区并不等同于永久代,只是因为HotSpot VM使用永久代来实现方法区,对于其他的Java虚拟机,比如J9和JRockit等,并不存在永久代概念。在Java虚拟机规范中定义了一种异常情况:如果方法区的内存空间不满足内存分配需求时,Java虚拟机会抛出OutOfMemoryError异常。
2.4.6:运行时常量池:
运行时常量池(Runtime Constant Pool)并不是运行时数据区域的其中一份子,而是方法区的一部分。在下一节中我们得知//①,Class文件不仅包含类的版本、接口、字段和方法等信息,还包含常量池,它用来存放编译时期生成的字面量和符号引用,这些内容会在类加载后存放在方法区的运行时常量池中。运行时常量池可以理解为是类或接口的常量池运行时表现形式。在Java虚拟机规范中定义了一种异常情况:当创建类或接口时,如果构造运行时常量地所需的内存超过了方法区所能提供的最大值,Java虚拟机会抛出OutOfMemoryError异常。
3.对象的创建:
对象的创建是我们经常要做的事情,通常是通过new来完成对一个对象的创建,当虚拟机接收到一个一个new时,它会:
3.1:判断对象对应的类是否加载、链接和初始化:
虚拟机接收到一条 new指令时,首先会去检查这个指定的参数是否能在常量池中定位一个类的符号引用,并且检查这个符号引用代表的类是否已被类加载器加载,链接和初始化过。
3.2:为对象分配内存:
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
- 指针碰撞:如果Java堆的内存是规整的,即所有用过的内存放在一边,而空闲的内在放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
- 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录哪些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
- Java堆的内存是否规整根据所采用的垃圾收集器是否带有压缩整理功能有关。
3.3:处理并发安全问题:
创建对象是一个非常频繁的操作,所以需要解决并发的问题,有两种方式:
- 对分配内存空间的动作进行同步处理,比如在虚拟机采用CAS算法并配上失败重试的方式保证更新操作的原子性。
- 每个线程在Java堆中预先分配一小块内存,这块内存称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),线程需要分配内存时,就在对应线程的TLAB上分配内存,当线程中的TLAB用完并且被分配到了新的TLAB时,这时候才需要同步锁定。通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。
3.4:初始化分配到的内存空间:
将分配到的内存,除了对象头外都初始化为零值。
3.5:设置对象的对象头:
将对象的所属类、对象的HashCode和对象的GC分代年龄等数据存储在对象的对象头。关于对象头下一节得知//②。
3.6:执行init方法进行初始化:
执行init方法,初始化对象的成员变量,调用类的构造方法,这样一个对象就被创建出来了。
4.对象的堆内存布局:
对象创建完毕,并且已经在Java堆中分配了内存,那么对象在堆内存是如何进行布局的呢?以HotSpot虚拟机为例,对象在堆内存的布局分为三个区域,分别是对象头(Healer) ,实例数据(Instance Data)、对齐填充(Padding)。下面分别来对这三个区域进行简单介绍:
- 对象头:对象头包括两部分信息,分别是Mark World和元数据指针,Mark Watd 用于存储对象运行时的数据,比如HashCode、锁状态标志、GC分代年龄、线程持有的锁等。而元数据指针用于指向方法区中的目标类的元数据,通过元数据可以确定对象的具体类型,具体是如何实现的请看下一节//③。
- 实例数据:用于存储对象中的各种类型的字段信息(包括从父类继承来的)。
- 对齐填充:对齐填充不一定存在,起到了占位符的作用,没有特别的含义。
其中:Mark Word 在 HotSpot中的实现类为markOop.hpp, markOop被设计成一个非固定的数据结构,这是为了在极小的空间中存储尽量多的数据。
5.oop-Klass模型:
oop-Klass模型是用来描述Java对象实例的一种模型,它分为两个部分:OOP指的是普通对象指针,用来表示对象的实例信息。klass用来描述元数据。在HotSpot中就采用了oop-Klass模型,oop实际上是一个家族,在Java虚拟机内部会定义很多的oop类型。
6.垃圾标记算法:
垃圾收集器(Garbage Collection),通常被称作GC。提到GC,很多人认为它是伴随Java而出现的,其实GC出现的时间要比Java早太多了,它是1960年诞生于MIT的Lisg,GC主要做了两个工作,一个是内存的划分和分配,另一个是对垃圾进行回收。关于内存的划分和分配,目前Java虚拟机内存的划分是依赖于GC设计的,比如现在GC都是采用了分代收集算法来回收垃圾的,Java堆作为GC主要管理的区域,被细分为新生代和老年代,再细致一点新生代又可以划分为Eden空间、From Survivor空间、To Survivor空间等,这样划分是为了更快地进行内存分配和回收。空间划分后,GC就可以为新对象分配内存空间。关于对垃圾进行回收,被引用的对象是存活的对象,而不被引用的对象是死亡的对象(也就是垃圾),GC要区分出存活的对象和死亡的对象(也就是垃圾标记),并对垃圾进行回收。在对垃圾进行回收前,GC要先标记出垃圾,那么如何标记呢?目前有两种垃圾标记算法,分别是引用计数算法和根搜索算法,这两个算法都和引用有些关联,因此讲垃圾标记算法之前,我们先回顾一下引用的知识点。
6.1:Java中的引用:
在JDK1.2之后,Java将引用分为强引用 ,软引用,弱引用和虚引用 。
- 强引用:当我们新建了一个对象时就创建了一个具有强引用的对象,如果一个对象具有强引用,垃圾收集器就绝不会回收它。Java虚拟机宁愿抛出OutOfMemoryError异常,使程序异常终止,也不会回收具有强引用的对象来解决内存不足的问题。
- 软引用:如果一个对象只具有软引用,当内存不够时,会回收这些对象的内存,回收后如果还是没有足够的内存,就会抛出OutOfMemoryError异常。Java提供了SoftReference 类来实现软引用。
- 弱引用:弱引用比起软引用具有更短的生命周期,垃圾收集器一旦发现了只具有弱引用的对象,不管当前内存是否足够,都会回收它的内存。Java提供了WeakReference类来实现弱引用。
- 虚引用:虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,这就和没有任何引用一样,在任何时候都可能被垃圾收集器回收,一个具有虚引用的对象,被垃圾收集器回收时会收到一个系统通知,这也是虚引用的主要作用。Java提供了PhantomReference类实现虚引用。
6.2:垃圾标记算法:
- 引用计数算法:引用计数算法的基本思想就是每个对象都有一个引用计数器,当对象在某处被引用的,它的引用计数器就加1,引用失效时就减1。当引用计数器中的值变为0,则该对象不能被使用,变成了垃圾。目前主流的Java虚拟机没有选择引用计数算法来为垃圾标记,主要原因是引用计数算法没有解决对象之间相互循环引用的问题。
- 根搜索算法:根搜索算法是基本思想就是选定一些对象作为GC Roots,并且组成根对象集合,然后再以这些GC Roots的对象为起始点,向下搜索,如果目标对象到GC Roots是连接着的,我们则称该目标对象是可达的,如果目标对象不可达则说明目标对象是可以被回收的。这样根搜索算法就解决了引用计数算法的问题:已经死亡的对象因为相互引用而不能被回收。
在Java中,可以作为GC Roots的对象主要有:
- Java栈中引用的对象。
- 本地方法栈中JNI引用的对象。
- 方法区中运行时常量池引用的对象。
- 方法区中静态属性引用的对象。
- 运行中的线程。
- 由引导类加载器加载的对象。
- GC控制的对象。
7.Java对象在虚拟机中的生命周期:
在Java对象被类加载器加载到虚拟机中后,Java对象在Java虚拟机中有7个阶段:
7.1:创建阶段:
创建阶段的具体步骤为:
- 为对象分配存储空间。
- 构造对象。
- 从超类到子类对static成员进行初始化。
- 递归调用超类的构造方法。
- 调用子类的构造方法。
7.2:应用阶段:
当对象被创建,并分配给变量赋值时,状态就切换到了应用阶段。这一阶段的对象至少要具有一个强引用,或者显式地使用软引用、弱引用或者虚引用。
7.3:不可见阶段:
在程序中找不到对象的任何强引用,比如程序的执行已经超出了该对象的作用 域。在不可见阶段,对象仍可能被特殊的强引用GC Roots持有,比如对象被本地方法栈中JNI引用或被运行中的线程引用等。
7.4:不可达阶段:
在程序中找不到对象的任何强引用,并且在垃圾收集器发现对象不可达。
7.5:收集阶段:
垃圾收集器已经发现对象不可达,并且垃圾收集器已经准备好要对该对象的内存空间进行重新分配,这个时候如果该对象重写了finalize方法,则会调用该方法。
7.6:终结阶段:
在对象执行完finalize方法之后仍然还处于不可达状态,或者对象没有重写finalize方法,则该对象进入到终结阶段,并且等待垃圾收集器回收该对象空间。
7.7:对象空间重新分配阶段:
当垃圾收集器对对象的内存空间进行回收或者在分配时,这个对象就会彻底消失。
8.垃圾收集算法:
垃圾收集算法有很多,常用的有:
8.1:标记-清除算法:
标记-清除算法是一种常见的基础垃圾收集算法,它将垃圾收集分为:标记阶段,清除阶段。之所以它是基础的,因为后面的垃圾收集算法都是在它的基础之上的。但是它主要有两个缺点:一是标记和清除的效率不高,另一个是容易产生大量不连续的内存碎片。
8.2:复制算法:
为了解决标记-清除算法效率不高的问题,产生了复制算法。它把内存空间划分为两个相等的空间,每次只使用其中的一个区域。在垃圾收集时,遍历当前使用的区域,把存活的对象复制到另外一个区域中去,最后将当前区域的可回收的对象进行回收。这种算法因为每次都对整个半区进行内存回收,不需要考虑内存碎片的问题,代价是使用内存为原来的一半。复制算法的效率与存活对象的数目多少有很大的关系,如果存活对象少,则算法效率高。由于绝大多数对象的生命周期短,并且这些对象存在于新生代中,因此复制算法被广泛应用在新生代中。
8.3:标记-压缩算法:
新生代可以使用复制算法,老年代因为存活率高,所以可以使用标记-压缩算法。与标记-清除算法不同的是,在标记可以回收的对象之后,将所有存活的对象压缩到内存的一端,使它们紧凑的排列的在一起,然后对边界以外的内存进行回收,回收之后,已经使用的和没有使用的内存都各自一边。
8.4:分代收集算法:
分代收集算法会结合不同的收集算法来处理不同的空间,因此在学习分代收集算法之前我们首先要了解Java堆区的空间划分。Java堆区的空间划分在Java虚拟机中,各种对象的生命周期会有着较大的差别,大部分对象生命周期很短暂,少部分对象生命周期很长有的甚至与应用程序以及Java虚拟机的运行周期一样长。因此,应该对不同生命周期的的对象采取不同的收集策略,根据生命周期长短将它们分别放到不同的区域,并在不同的区域采用不同的收集算法,这就是分代的概念。现在主流的Java虚拟机的垃圾收集器都采用分代收集算法(Generational Collection)。Java堆区基于分代的概念,分为新生代(YogGeneration)和老年代(Tenured Generation),其中新生代再细分为Eden空间、From Surniat空间和To Survivor空间。因为Eden空间中的大多数对象生命周期很短,所以新生代的空间划分并不是均分的,HotSpot虚拟机默认Eden空间和两个Survivor空间的所占的比例为8:1.
8.4.1:分代收集:
根据Java堆区的空间划分,垃圾收集的类型分为两种,它们分别如下:
- Minor Collection:新生代垃圾收集。
- Full Collection:对老年代进行收集,又可以称作Major Collection, Ful Colletion常情况下会伴随至少一次的Minor Collection,它的收集频率较低,耗时较长。
Dalvik虚拟机:
Dalvik虚拟机(Dalvik Virtual Machine),简称Dalvik VM 或者DVM。DVM是Google专门为Android平台开发的虚拟机,它运行在Android运行时库中。
1.DVM和JVM的区别:
DVM之所以不是一个JVM,主要原因是DVM没有遵循JVM的规范来实现,DVM和JVM的区别主要有:
- 基础的架构不同:JVM基于栈则意味着需要从栈中读写数据,所需要的指令会更多,这样会导致速度变慢,对于性能有限的移动设备,显然不是很适合的。DVM是基于寄存器的,它没有基于栈的虚拟机在复制数据时而使用的大量的出入栈指令,同时指令更紧凑、更简洁。但是由于显式指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少,总的代码数不会增加多少。
- 执行的字节码不同:在Java SE 程序中,Java类被编译成一个或多个.class文件,并打包成jar文件,而后JVM会通过相应的.class文件和jar文件获取相应的字节码。执行顺序为java文件一.class文件一jar文件,而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM会从该dex文件读取指令和数据。执行顺序为.java文件一.class文件一.dex文件。
jar文件里面包含多个.class文件,每个.class文件里面包含了该类的常量池、类信息、属性等。当JVM加载该jar文件的时候,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。而在.apk文件中只包含了一个dex文件,这个.dex文件将所有的.class里面所包含的信息全部整合在一起了,这样就加快了速度。.class文件存在很多的冗余信息,dex工具会去除余信息,并把所有的class文件整合到.dex文件中,减少了I/O操作,加快了类的查找速度。 - DVM允许在有限的内存中同时运行多个进程:DVM经过优化,允许在有限的内存中同时运行多个进程。在Andoid中的每一个应用的都运行在一个 DVM实例中,每一个 DVM实例都运行在一个独立的进程空间中,独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
- DVM由Zygote 创建和初始化:Zygote进程,它是一个DVM进程,同时也用来创建和初始化DVM实例。每当系统需要创建一个应用程序时,Zygote就会fock自身,快速地创建和初始化一 个DVM实例,用于应用程序的运行。对于一些只读的系统库,所有的DVM实例都会和Zygote共享一块内存区域,节省了内存开销。
- DVM有共享机制:DVM拥有预加载一共享的机制,不同应用之间在运行时可以共享相同的类,拥有更高的效率。而JVM机制不存在这种共享机制,不同的程序,打包以后的程序都是彼此独立的,即便它们在包里使用了同样的类,运行时也都是单独加载和运行的,无法进行共享。
- DVM 早期没有使用JIT编译器:JVM使用了JIT 编译器(Just In Time Compiler,即时编译器),而DVM早期没有使用JIT编译器。早期的DVM每次执行代码,都需要通过解释器将dex代码编译成机器码,后交给系统处理,效率不是很高。为了解决这一问题,从Android2.2版本开始DVM使了JIT编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器码(Native Code),这样在下次执行到相同逻辑的时候,直接使用编译之后的本地机器码,而不是每次都需要编译。需要注意的是,应用程序每一次重新运行的时候,都要重做这个编译工作,因此每次重新打开应用程序,都需要JIT编译。
2.DVM架构:
DVM的源码位于dalvik/目录下,Android8.0中的DVM源码的部分目录说明如下所示:
目录/文件 | 说明 |
---|---|
dexdump | 生成dex文件的反编译查看工具,主要用于查看编译出来的代码的正确性和结构 |
dexgen | dex代码生成器项目 |
docs | DVM相关帮助文档 |
dx | Java字节码转换为DVM机器码的工具 |
libdex | 生成主机和设备处理dex文件的库 |
tools | 一些编译和运行相关的工具 |
Android.mk | 虚拟机配置的makefile配置文件 |
MODULE_LICENSE_APACHE2 | APACHE2版权声明文件 |
NOTICE | 虚拟机源码版权注意事项文件 |
其中,dalvik/libdex会被编译为libdex.a静态库,作为dex工具使用。dalvik/dexdump是dex文件的反编译工具。
从上图可知:首先Java编译器编译的class文件经过DX工具转化为dex文件,dex文件由类加载器处理,接着解释器根据指令集对Dalvik字节码进行解释,执行,最后交给Linux处理。
3.DVM的运行时堆:
DVM的运行时堆使用标记一清除(Mark-Sweep)算法进行GC,它由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space (Zygote Heap)和Allocation Space (Active Heap)。Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,在Zygote进程和应用程序进程之间会共享 Zygote Spase。在Zygote 进程 fork第一个子进程之前,会把Zygote Space分为两个部分,原来的已经被使用的那部分堆仍旧叫 Zygote Space,而未使用的那部分堆就叫Allocation Space,以后的对象都会在Allocation Space上进行分配和释放。Allocation Space不是进程间共享的,在每个进程中都独立拥有一份。除了这两个Space,还包含以下数据结构:
- Card Table:用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。
- Heap Bitmap:有两个Heap Bitmap,一个用来记录上次GC存活的对象,另一个用来记录这次GC存活的对象。
- Mark Stack: DVM的运行时堆使用标记一清除(Mark-Sweep)算法进行GC,MastStack就是在GC的标记阶段使用的,它用来遍历存活的对象。
4.DVM的GC日志:
DVM和ART的GC日志与Java虚拟机的日志有较大的区别。在DVM中每次垃圾收集都会将GC 日志打印到logcat中。具体的格式为:
D/dalvikvm:<GC_Reason> <Amount_freed>, <Heap_stats>,<External_memory_stats>, <Pause_time>
可以看到DVM的日志一共有5个信息,其中GC Reason有很多种,GC Reason也叫做引起GC的原因,有以下几种:
- GC_CONCURRENT:当堆开始填充时,并发GC可以释放内存。
- GC_FOR_MALLOC:当堆内存已经满了,app尝试分配内存而引起的GC,系统必须停止app并且回收内存。
- GC_HPROF_DUMP_HEAP:当你请求创建HPROF文件来分析堆内存时出现的GC。
- GC_EXPLICIT:显式的GC,例如调用 System.gc()(应该避免调用显式的GC,信任GC会在需要时运行)。
- GC_EXTERNAL_ALLOC:仅适用于API级别小于等于10,且用于外部分配内存的GC。
除了引起GC的原因以外,还有其他的信息如下:
- Amount_freed:本次GC释放内存的大小。
- Heap_stats:堆的空闲内存百分比(已用内存)/(堆的总内存)。
- Extermal_memory_stats: API小于等于级别10的内存分配(已分配的内存)/(引起GC的阀值)。
- Pause time:暂停时间,更大的堆会有更长的暂停时间。并发暂停时间会显示两个暂停时间,即一个出现在垃圾收集开始时,另一个出现在垃圾收集快要完成时。
ART虚拟机:
ART(Android Runtime)虚拟机是Android4.4发布的,用来替代DVM,Android4.4还是默认采用DVM,系统会提供一个选项来开启ART。在Android5.0开始默认采用ART。
1.ART和DVM的区别:
主要有4点:
- 从DVM这一节我们知道,DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这会使得应用程序的运行效率降低。而在ART中,系统在安装应用程序时会进行一次AOT (ahead of time compilation,预编译),将字节码预先编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提升,设备的耗电量也会降低。这就好比我们在线阅读漫画,DVM是我们阅读到哪就加载哪,ART则是接加载一章的漫画,虽然一开始加载速度有些慢,但是后续的阅读体验会很流畅。采用AOT也会有缺点,主要有两个:第一个是AOT会使得应用程序的安装时间变长,尤其是一些复杂的应用;第二个是字节码预先编译成机器码,机器码需要的存储空间会多一些。为了解决上面的缺点,Android 7.0版本中的ART加入了即时编译器JIT,作为AOT的一个补充在应用程序安装时并不会将字节码全部编译成机器码,而是在运行中将热点代码编译机器码,从而缩短应用程序的安装时间,并节省了存储空间。
- DVM是为32位CPU设计的,而ART支持64位并兼容32位CPU,这也是DVM被淘汰的主要原因之一。
- ART对垃圾回收机制进行了改进,比如更频繁地执行并行垃圾收集,将GC暂停由2次减少为1次等。
- ART的运行时堆空间划分和DVM不同。
2.ART的运行时堆:
与DVM的GC不同的是,ART采用了多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认是采用了CMS(Concurrent Mark-Sweep)方案,该方案主要使用了sticky-CMS和partial-CMS。根据不同的CMS方案,ART的运行时堆的空间也会有不同的划分,默认是由4个Space和多个辅助数据结构组成的,4个Space分别是Zygote Space、 AllocationSpace、 Image Space 和 Large Object Space。 Zygote Space、 Allocation Space和DVM中的作用是一样的,Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象(默认大小为12KB),其中Zygste Space和Image Space是进程间共享的。除了这四个 Space,ART的Java堆中还包括两个Mod Union Table,一个Cand Table.两个 Hoap Bimap,两个Object Map,以及三个Object Stack。
3.ART的GC日志:
ART的GC日志与DVM不同,ART会为那些主动请求的垃圾收集事件或者认为GC速度慢时才会打印GC日志。GC速度慢指的是GC暂停超过5ms或者GC持续时间超过100ms,如果App未处于可察觉的暂停进程状态,那么它的GC不会被认为是慢速的。ART的GC日志的具体格式:
I/art: <GC_Reason> <GC_Name> <Objects_freed> (<Size_freed>) AllocSpace Objects, <Large_objects_freed> (<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause time(s)>
3.1:GC日志组成部分介绍:
ART的引起GC原因(GC_Reason)要比DVM多一些,有以下几种:
- Concrrent:并发GC,不会使App的线程暂停,该GC是在后台线程运行的,并不会阻止内存分配。
- Alloc:当堆内存已满时,App尝试分配内存而引起的GC,这个GC会发生在正在分配内存的线程中。
- Explicit:App显示的请求垃圾收集,例如调用System.gc()。与DVM一样,最佳做法是应该信任GC并避免显式地请求GC,显式地请求GC会阻止分配线程并不必要地浪费CPU周期。如果显式地请求GC导致其他线程被抢占,那么有可能会导致jank(App同一帧画了多次)。
- NativeAlloc:Native 内存分配时,比如为 Bitmaps或者RenderScript分配对象,这会导致Native内存压力,从而触发GC。
- CollectorTransition:由堆转换引起的回收,这是运行时切换GC 而引起的。收集器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集器转换仅在以下情况下出现:在内存较小的设备上,App将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
- HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间,通常发生在当APP已经移动到可察觉的暂停进程状态时。这样做的主要原因是减少了内存使用并且对堆内存进行了碎片化整理。
- DisableMoveingGc:不是真正触发GC的原因,发生并发堆压缩时,由于使用了GetPrimitiveArrayCrtical,收集会被阻塞。在一般情况下,强烈建议不要使用GetPrimitiveArrayCrtical,因为它在移动收集器方面有限制。
- HeapTrim:不是触发GC的原因,但是收集会一直被阻塞,直到堆内存整理完毕。
3.2:垃圾收集器名称:
GC_Name指的是垃圾收集器名称,有以下几种:
- Concurrent Mark Sweep (CMS): CMS收集器是一种以获取最短收集暂停时间为目标的收集器,采用了标记一清除算法实现。它是完整的堆垃圾收集器,能释放除了 Image Space外的所有的空间。
- Concurrent Partial Mark Sweep:部分完整的堆垃圾收集器,能释放除了ImageSpace和Zygote Space外的所有空间。
- Concurent Sicky Mark Sweep:粘性收集器,基于分代的垃圾收集思想,它只能释放自上次GC以来分配的对象。这个垃圾收集器比一个完整的或部分完整的垃圾收集器扫描得更频繁,因为它更快并且有更短的暂停时间。
- Marksweep + Semispase:非并发的GC,复制GC用于堆转换以及齐性空间压缩(堆碎片整理)。
3.3:其他信息:
- Objects freed:本次GC从非Large Object Space中回收的对象的数量。
- Size_freed:本次GC从非 Large Object Space中回收的字节数。
- Large objects freed:本次GC从Large Object Space中回收的对象的数量。
- Large object size freed:本次GC从Large Object Space 中回收的字节数。
- Heap stats:堆的空闲内存百分比,即(已用内存)/(堆的总内存)。
- Pause times:暂停时间,暂停时间与在GC运行时修改的对象引用的数量成比例。目前,ART的CMS收集器仅有一次暂停,它出现在GC的结尾附近, 移动的垃圾收集器暂停时间会很长,会在大部分垃圾回收期间持续出现。
4.DVM和ART的诞生:
DVM是怎么来的?从init启动zygote进程时开始,它会调用app_main.cpp的main函数。该函数相关的有:如果当前程序运行在zygote进程中,就调用AppRuntime的start函数,start函数具体在AppRuntime的父类AndroidRuntime中实现。
frameworks/base/cmds/app_process/app_main.cpp
在start函数中:首先调用startVM函数来创建虚拟机,并且调用startReg函数为Java虚拟机注册JNI方法。而其中有一个jni_invocation的Init函数:它调用了GetLibrary函数和dlopen函数。前者用于返回libart.so(当前使用的是ART)或者libdvm.so(当前使用的是DVM);后者用于加载libart.so(当前使用的是ART)或者libdvm.so(当前使用的是DVM)。所以jni_invocation的Init函数用于初始化ART后者DVM的环境,初始化之后会调用startVM函数来创建虚拟机。