JVM基本原理
一、简介
dalvik是google在其android智能手机操作系统中用的java虚拟机。借此讲一下我对虚拟机的基本理解吧。一切编程语言要想在计算机上运行必须翻译成机器码(这是废话)。java是一种半编译半解释型语言。半编译是指:java源代码,会经过javac命令变成 .class文件。半解释是指: .class文件被jvm解释的过程。也就是因为jvm的半解释才有了java的动态语言特性:反射和annotation。
二、类文件预处理
在半编译阶段,java源代码被编译,在.class文件中会有类信息和虚拟机指令。dalvik有自己的libdex库负责对.class进行处理。libdex主要对.class进行处理生成自己的dex文件。主要做的工作是,对虚拟机指令进行转换(dalvik是基于寄存器的,sun虚拟机是基于栈的),对类的静态数据进行归类、压缩。
三、类加载
在虚拟机启动时,根据输入的参数(一般有入口类(main函数所在的类)和jar包的路径),在classath路径中加载入口类,类加载过程是:虚拟机根据入口类的全称,去遍历classpath下的dex文件(dalvik第一次加载后会生成cache文件,供下次快速加载,所以第一次会很慢),获取入口类的信息,构建入口类的Class结构体,Class结构主要包含类的field、method、(anntation 和内部类dalvik放在native处理,没在Class结构体,这样为了节省内存)。另外在加载入口类之前,可能要加载,他所依赖的类,比如父类、Class类等,类被加载后一般会在classloader中保存,下次用可以直接取到。
- filed主要信息,filed的类型、变量名对static fiedl 还有可能有初始值(非static field在构造函数中赋初值,不保证每个类都有静态初始化块<clinit>方法,所以staitc filed初始值要单独处理)。
- method 主要有:方法签名、method Code(虚拟机指令)、exception等信息
四、类的初始化及resolve
在一个类的metod code被虚拟机解释器执行之前,一般要进行类的初始化,类初始化主要做:static变量的赋初值(如上所说)、及method code的预处理,因为在method code内都是静态的信息,比如:你要调一个类的某个方法,指令中可能只有类名和方法名的字符串信息,而不是这个这个method结构体的指针,如果要掉的类没有初始化,你可能还要初始化这个类。以及异常错误的处理,比如你调的方法不存在,要抛NoSuchMethodError。这步叫resolve。resolve过程也可以在解释器内做,因为你有可能初始化一个类,而不调用它的方法。dalvik是在第一次调一个方法时resolve的。
五、类的解释执行
在加载完入口类时,虚拟机会调用JNI(对虚拟机对外暴露的函数的封装,可以让java和C互相调用)的方法去执行,入口类的main方法,解释器首先获取此类的method结构体,获得method code一条一条解释执行。
常见的指令:
- const、const-string等:一般是把一个常量(int double short boolean char string ……),放入一个寄存器。
- add sub div xor or and ……:加减乘除、与或非等
- getfield putfield 、getstatic putstaic:在堆上获取相应对象的field值,对static可能在Class结构体内存放
- invokeXXX:调用某个方法,要在栈上保存当前frame信息(包括IP 及各寄存器值),push一个frame去解释另一个方法。方法调用是,要将这个方法的参数copy到新的frame 寄存器上。在被调用的方法结束时,在栈上pop出该frame,把方法返回值copy到调用frame的寄存器上。(这个可以有多种实现,dalvik是把返回值copy到全局的ret变量中,在用move_result指令来做到这一点)
- return :告诉解释器方法调用结束
- if if_eqz等:逻辑判断、还有swith case等也有专门指令
- new指令,new一个对象放在堆上
六、异常处理
首先明确一个概念,就是一个java的线程在dalvik内对应一个C线程,每个java线程都会有自己的执行栈。Exception的处理时,当解释器遇到Excepiton时,把这个异常对象放入Thread结构体保存,在解释某些有可能抛异常的指令时,去check,check到,就结束执行下一条指令,往下找catch,如果没有就一直往上抛,只到栈上没frame,虚拟机结束。
七、堆结构及GC
堆是虚拟机为了预存对象,而事先分配的一块大内存。对象在堆上一般要保存,其Class信息(属于某个class)和对象每个field的值。GC时,虚拟机遍历所有线程栈及全局变量(JNI全局reference、Stringpool等),对有引用对象进行mark,然后遍历堆进行swap。另外还牵扯finalize的执行及java reference机制等。
堆设计最大的麻烦在于,频繁的分配和释放对象可能导致内存碎片。一般做法是在GC后要进行一次堆整理,但对整理会改变对象的指针地址,可能导致其他对象引用一个无效的指针,所以一般要另外为每个对象对应一个定长的map,在引用对象上保存一个不变的映射值,在根据这个值找到对象。堆整理时,改变引用值。这种做法的坏处是浪费了内存,对JNI的getArrayElements方法不得不采用copy模式等。另一种做法是不进行堆整理,这样要用好的算法控制不至于产生过多的内存碎片。
优化Dalvik虚拟机的堆内存分配
对于Android平台来说,其托管层使用的
Dalvik Java VM
从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用
dalvik.system.VMRuntime
类提供的
setTargetHeapUtilization
方法可以增强程序堆内存的处理效率。当然具体原理我们可以参考开源工程,这里我们仅说下使用方法:
private final static float TARGET_HEAP_UTILIZATION = 0.75f;
在程序
onCreate
时就可以调用
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);
即可。
Android堆内存也可自己定义大小
对于一些Android项目,影响性能瓶颈的主要是Android自己内存管理机制问题,目前手机厂商对RAM都比较吝啬,对于软件的流畅性来说RAM对性能的影响十分敏感,除了 优化Dalvik虚拟机的堆内存分配外,我们还可以强制定义自己软件的堆内存大小,我们使用Dalvik提供的
dalvik.system.VMRuntime
类来设置最小堆内存为例:
private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
//设置最小heap内存为6MB大小。当然对于内存吃紧来说还可以通过手动干涉GC去处理