虚拟机

1、 Java内存模型

java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。

程序计数器(Program Count Register):又叫程序寄存器。JVM支持多个线程同时运行,当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)。如果线程正在执行的是一个Java方法(非native),那么PC寄存器的值将总是指向下一条将被执行的指令,如果方法是 native的,程序计数器寄存器的值不会被定义。 JVM的程序计数器寄存器的宽度足够保证可以持有一个返回地址或者native的指针。

栈(Stack):又叫堆栈。JVM为每个新创建的线程都分配一个栈。也就是说,对于一个Java程序来说,它的运行就是通过对栈的操作来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。我们知道,某个线程正在执行的方法称为此线程的当前方法。我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧,这个帧自然成为了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其他数据。从Java的这种分配机制来看,堆栈又可以这样理解:栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

本地方法栈(Native Stack):存储本地方法的调用状态。

方法区/静态区(Method Area):当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后把这些类型信息(包括类信息、常量、静态变量等)放到方法区中,该内存区域被所有线程共享。本地方法区存在一块特殊的内存区域,叫常量池(Constant Pool),这块内存将与String类型的分析密切相关。

堆(Heap):Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在此区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存,但是这个对象的引用却是在栈(Stack)中分配。因此,执行String s = new String(“s”)时,需要从两个地方分配内存:在堆中为String对象分配内存,在栈中为引用(这个堆对象的内存地址,即指针)分配内存。
Java堆是垃圾收集器管理的主要区域,因此又称为“GC 堆”(Garbage Collectioned Heap)。现在的垃圾收集器基本都是采用的分代收集算法,所以Java堆还可以细分为:新生代(Young Generation)和老年代(Old Generation)。分代收集算法的思想:第一种说法,用较高的频率对年轻的对象(young generation)进行扫描和回收,这种叫做minor collection,而对老对象(old generation)的检查回收频率要低很多,称为major collection。这样就不需要每次GC都将内存中所有对象都检查一遍,以便让出更多的系统资源供应用系统使用;另一种说法,在分配对象遇到内存不足时,先对新生代进行GC(Young GC);当新生代GC之后仍无法满足内存空间分配需求时, 才会对整个堆空间以及方法区进行GC(Full GC)。

2 、Android虚拟机的特点

Google于2007年底正式发布了Android SDK,Dalvik虚拟机也第一次进入了人们的视野。它的作者是丹.伯恩斯坦(Dan Bornstein)。Dalvik虚拟机作为Android平台的核心组件,拥有如下几个特点:
• 体积小,占用内存空间小;
• 专有的DEX可执行文件格式,体积更小,执行速度更快;
• 常量池采用32位索引值,寻址类方法名,字段名,常量更快;
• 基于寄存器架构,并拥有一套完整的指令系统;
• 提供了对象生命周期管理,堆栈管理,线程管理,安全和异常管理以及垃圾回收等重要功能;
• 所有的Android程序都运行在Android系统进程里,每个进程对应着一个Dalvik虚拟机实例。

Dalvik虚拟机与Java虚拟机的区别

• Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码。传统的Java程序经过编译,生成Java字节码保存在class文件中,Java虚拟机通过解码class文件中的内容来运行程序。而Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中。Dalvik虚拟机通过解释DEX文件来执行这些字节码。

• Dalvik可执行文件体积小。Android SDK中有一个叫dx的工具负责将Java字节码转换为Dalvik字节码。dx工具对Java类文件重新排列,消除在类文件中出现的所有冗余信息,避免虚拟机在初始化时出现反复的文件加载与解析过程。一般情况下,Java类文件中包含多个不同的方法签名,如果其他的类文件引用该类文件中的方法,方法签名也会被复制到其类文件中,也就是说,多个不同的类会同时包含相同的方法签名,同样地,大量的字符串常量在多个类文件中也被重复使用。这些冗余信息会直接增加文件的体积,同时也会严重影响虚拟机解析文件的效率。消除其中的冗余信息,重新组合形成一个常量池,所有的类文件共享同一个常量池。由于dx工具对常量池的压缩,使得相同的字符串,常量在DEX文件中只出现一次,从而减小了文件的体积。

• Java虚拟机与Dalvik虚拟机架构不同。Java虚拟机基于栈架构,程序在运行时虚拟机需要频繁的从栈上读取或写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费不少CPU时间,对于像手机设备资源有限的设备来说,这是相当大的一笔开销。Dalvik虚拟机基于寄存器架构。数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式要快很多。

Android系统启动加载完内核后,第一个执行的是init进程,init进程首先要做的是设备的初始化工作,然后读取inic.rc文件并启动系统中的重要外部程序 Zygote。Zygote进程是Android所有进程的孵化器进程,它启动后会首先初始化Dalvik虚拟机,然后启动system_server并进入Zygote模式,通过socket等候命令。当执行一个Android应用程序时,system_server进程通过Binder IPC方式发送命令给Zygote,Zygote收到命令后通过fork自身创建一个Dalvik虚拟机的实例来执行应用程序的入口函数,这样一个程序就启动完成了。

一个应用,一个虚拟机实例,一个进程!!!
每一个Android应用都运行在一个Dalvik虚拟机实例里,而每一个虚拟机实例都是一个独立的进程空间。每个进程之间可以通信(IPC,Binder机制实现)。虚拟机的线程机制,内存分配和管理,Mutex等等都是依赖底层操作系统而实现的。

不同的应用在不同的进程空间里运行,当一个虚拟机关闭或意外中止时不会对其它 虚拟机造成影响,可以最大程度的保护应用的安全和独立运行。

3、 Dalvik和Art的区别

什么是Dalvik?

Dalvik是Google公司自己设计用于Android平台的虚拟机。
Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。
它可以支持已转换为 .dex格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。
Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
很长时间以来,Dalvik虚拟机一直被用户指责为拖慢安卓系统运行速度不如IOS的根源。
2014年6月25日,Android L 正式亮相于召开的谷歌I/O大会,Android L 改动幅度较大,谷歌将直接删除Dalvik,代替它的是传闻已久的ART。

什么是ART?
即Android Runtime
ART 的机制与 Dalvik 不同。在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率,而在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。这个过程叫做预编译(AOT,Ahead-Of-Time)。这样的话,应用的启动(首次)和执行都会变得更加快速。

Dalvik和ART的几点区别
dalvik是执行的时候编译+运行,安装比较快,开启应用比较慢,应用占用空间小;
ART是安装的时候就编译好了,执行的时候直接就可以运行的,安装慢,开启应用快,占用空间大。
用个比喻来说就是,骑自行车:
dalvik 是已经折叠起来的自行车,每次骑都要先组装自行车才能骑;
ART 是已经组装好的自行车,每次骑直接上车就能走人。

4、 熟悉垃圾回收的实现机制,了解虚拟机的GC类型

GC全称是Garbage Collection,也就是所谓的垃圾回收。Android系统会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收。那么哪些对象会被认为是不再使用,并且可以被回收的呢?我们来看下面一张图:

这里写图片描述

上图当中,每个蓝色的圆圈就代表一个内存当中的对象,而圆圈之间的箭头就是它们的引用关系。这些对象有些是处于活动状态的,而有些就已经不再被使用了。那么GC操作会从一个叫作Roots的对象开始检查,所有它可以访问到的对象就说明还在使用当中,应该进行保留,而其它的对象就表示已经不再被使用了,如下图所示:

这里写图片描述

可以看到,目前所有黄色的对象仍然会被系统继续保留,而蓝色的对象就会在GC操作当中被系统回收掉了,这大概就是Android系统一次简单的GC流程。

垃圾收集GC(Garbage Collection)是Java语言的核心技术之一,虽然Java 7上新增垃圾回收器G1有新特性,但在JVM的内部运行机制上看,Java的垃圾回收原理与机制并未改变。垃圾收集的目的在于清除不再使用的对象。GC通过确定对象是否被活动对象引用来确定是否收集该对象。GC首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数和对象引用遍历。

引用计数收集器
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象(不是引用)都有一个引用计数。当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象+1),但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
缺点: 无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.

跟踪收集器
早期的JVM使用引用计数,现在大多数JVM采用对象引用遍历。对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集。在对象遍历阶段,GC必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。

下一步,GC要删除不可到达的对象。删除时,有些GC只是简单的扫描堆栈,删除未标记的未标记的对象,并释放它们的内存以生成新的对象,这叫做清除(sweeping)。这种方法的问题在于内存会分成好多小段,而它们不足以用于新的对象,但是组合起来却很大。因此,许多GC可以重新组织内存中的对象,并进行压缩(compact),形成可利用的空间。
为此,GC需要停止其他的活动活动。这种方法意味着所有与应用程序相关的工作停止,只有GC运行。结果,在响应期间增减了许多混杂请求。另外,更复杂的 GC不断增加或同时运行以减少或者清除应用程序的中断。有的GC使用单线程完成这项工作,有的则采用多线程以增加效率。

一些常用的垃圾收集器
(1)标记-清除收集器
这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。并且,由于它只是清除了那些未标记的对象,而并没有对标记对象进行压缩,导致会产生大量内存碎片,从而浪费内存。
(2)标记-压缩收集器
有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。
(3)复制收集器
这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,JVM生成的新对象则放在另一半空间中。GC运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。并且对于指定大小堆来说,需要两倍大小的内存,因为任何时候都只使用其中的一半。
(4) 增量收集器
增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾,也可理解为把堆栈分成一小块一小块,每次仅对某一个块进行垃圾收集。这会造成较小的应用程序中断时间,使得用户一般不能觉察到垃圾收集器正在工作。
(5)分代收集器
复制收集器的缺点是:每次收集时,所有的标记对象都要被拷贝,从而导致一些生命周期很长的对象被来回拷贝多次,消耗大量的时间。而分代收集器则可解决这个问题,分代收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。JVM生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象(非短命对象)将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。

基本回收算法
• 引用计数( Reference Counting )
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃 圾回收时,只用收集计数为 0 的对象。 此算法最致命的是无法处理循环引用的问题。
• 标记 - 清除( Mark-Sweep )
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整 个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
• 复制( Copying )
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把 正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内 存整理,不过出现 ” 碎片 ” 问题。当然,此算法的缺点也是很明显的,就是需要两 倍内存空间。
• 标记 - 整理( Mark-Compact )
此算法结合了 ” 标 记 - 清除 ” 和 ” 复制 ” 两个算法的优点。也是分两阶段,第一阶段从根节点开始 标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象 ” 压缩 ” 到堆 的其中一块,按顺序排放。此算法避免了 ” 标 记 - 清除 ” 的碎片问题,同时也避免了 ” 复制 ” 算法的空间问题。
• 增量收集( Incremental Collecting )
实施垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因 JDK5.0 中的收集器没有使用这种算法的。
• 分代( Generational Collecting )
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对 不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从 J2SE1.2 开始)都是使用此算法的。

分代垃圾回收详述

• Young (年轻代)
年轻代分三个区。一个 Eden 区,两个 Survivor 区。大部分对象在 Eden 区中生成。当 Eden 区满时,还存活的对象将被复制到 Survivor 区(两个中的一个),当这个 Survivor 区满时,此区的存活对象将被复制到另 外一个 Survivor区,当这个 Survivor 去也满了的时候,从第一个 Survivor 区复制过来的并且此时还存活的对象, 将被复制 ” 年老区(Tenured)” 。需要注意, Survivor 的两个区是对称的,没先后关系,所以 同一个区中可能同时存在从 Eden 复制 过来对象,和从前一个 Survivor 复 制过来的对象,而复制到年老区的只有从第一个 Survivor 区过来的对象。而且,Survivor区总有一个是空的。

• Tenured (年 老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

• Perm (持 久代)
用于 存放静态文件,如今 Java 类、方法 等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class ,例如 Hibernate 等,在这种时候需要设置一 个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过 -XX:MaxPermSize= 进行设置。

GC 类型

GC 有 两种类型: Scavenge GC 和 Full GC 。
• Scavenge GC
一般情 况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC , 堆 Eden 区域进行 GC ,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区。
• Full GC
对整个堆进行整 理,包括 Young 、 Tenured 和 Perm 。 Full GC 比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 。有如下原因可能导致 Full GC :
Tenured 被写满;
Perm 域被写满;
System.gc() 被显示调用;
上一次 GC 之 后 Heap 的各域分配策略动态变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值