jvm将class文读取到内存中,经过对class文件的校验、转换解析、初始化最终在jvm的heap和方法区分配内存形成可以被jvm直接使用的类型的过程
Java 虚拟机 一般有五部分构成:
- 1、一组指令集
- 2、一组寄存器
- 3、一个栈
- 4、一个垃圾收集堆
- 5、一个方法区域
指令:类似汇编语言,处理cpu的运算
寄存器:保存虚拟机运行状态。 包括4中(pc:java程序计数器 opTop 指向操作数栈顶指针 frame 指向方法环境的指针 vars指向局部变量第一个变量指针)
栈:java 虚拟机中的栈分3 个区 : 1、局部变量区 2、运行环境区(1、动态链接 2、正常返回值 3、异常返回 )3、操作数栈
垃圾回收堆: 运行时数据区,对象从这里分配空间
方法区:保存编译后的代码和符号表,class 和 static
二:JVM内存区域模型
1.方法区
也称”永久代” 、“非堆”, 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
运行时常量池:是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
2.虚拟机栈
描述的是Java 方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。
局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
3.本地方法栈
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。
4.堆
也叫做java 堆、GC堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组(所有new的对象)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。
新生代:
程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。
老年代:
用于存放经过多次新生代GC任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②.大的数组对象,切数组中无引用外部对象。
老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
5.程序计数器
是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
三:直接内存
直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。
1、一个Android 手机中开启N个app,那么 此时启动了N个虚拟机。每次运行一个 java main()方法的程序都会启动一个虚拟机。每个虚拟机都有一个 方法区 和 堆区 供该虚拟机中所有 线程共享。每次启动线程 得到两个东西: 一个是pc寄存器 ,一个是java栈 :
pc寄存器
java栈
指令1
指令2
...
操作数
局部变量
返回值
中间结果
java虚拟机和 Dalvik 虚拟机差别 :
1、java 虚拟机基于栈,dalvik虚拟机给予寄存器(理论上 dalvik要离cpu近,处理速度要快,少了一些读取栈数据的操作)
2、执行 的字节码文件不一样
java虚拟机 : java-->.class-->.jar
dalvik : java → .class-→.dex
jar 包里面包含了很多的class每个class 单独包含 头信息(如编译版本)、常量池、类信息、域、方法、属性。dex把所有的class里面信息整合在一起.
3、dalvik 会通过zygote preloadclass ,完成虚拟机初始化。
Dalvik运作流程
Dalvikvm :创建虚拟机,执行参数指定的java类
dvz: 从zygote孵化出一个虚拟机,和dalvikvm的区别是,dvz预装了framework的大部分类和资源
app_process:一个进程用于加载 ZygoteInit.java ,SystemServer.java
启动zygotte:
class文件加载:
1、mmap()--------→读取dex 。生成Dexfile(主要包含头部,索引,数据)
2、dexFileParse()------>分析,将结果存在Dexfile数据结构中
3、加载class 文件(需要用ClassObject保存加载类)
4、加载基本类库文件
5、加载用户类文件
Apk加载机制
派生了 两个类 DexClassLoader,PathClassLoader 集成 ClassLoader,重写了findClass,而不能使用defineClass。符合双亲委派原则。
DexClassLoader 和 pathClassLoader区别 :
DexClassLoader 可以加载 没有安装到app内部的jar 或者apk 文件,必须先把文件装近Application 目录下。DexClassLoader 传入了一个optimizedDirectory加载问类以后会生成新的dex文件;而 PathClassLoader 的optimizedDirectory=null,所以不会进行组装新的dex文件,故只能加载已经装载到app里面的文件。
ClasLoader 的 loaderClass 函数中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
Dalvik进程管理
1、linux 的启动脚本都是init.rc 在system/core/rootdir/init.rc
2、
java虚拟机内存分配策略
内存分配会涉及到 :
1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制.
- 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中。)
- 堆:存放所有new出来的对象。
- 静态域:存放静态成员(static定义的)
- 常量池:存放字符串常量和基本类型常量(public static final)。
- 非RAM存储:硬盘等永久存储空间
这里我们主要关心栈,堆和常量池,对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。
对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
堆内区 :分3种类型
1、永久存储区Permanent space(java 系统Class interferce 元数据,运行环境,不会被垃圾回收)
2、新生区 Young Generation Space(进行产生、分配、最后被垃圾回收收集,消亡)
3、养老区 Tenure Generation Space
堆区的数据有 new,newArray,anewArray,multianewarray等指令建立。
例如 :
String a=new String(“aaaaa”);存到堆里面
String b=”bbb”; //因为”bbb” ,先在栈里创建对象b,在常量池查找”bbb”,如果没有就将”bbb”放入常量池,另b→“bbb”.
所以
String a=”aaaa”;
String b=”aaaa”;
system.out.println(a==b); //true
直接内存:各种NIO 流
Dalvik内存分配
1、对象布局 :(1、clazz 2、lock 3、data)
堆: heapBase ,heapLimit ,dalvik.vm.heapsize=32M 一般
可以通过VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
垃圾回收
虚引用:任何时刻都可能被垃圾回收器回收,唯一作用是,在垃圾回收的时候能得到系统通知,phatomReference ,必须和ReferenceQueue 联合使用。
他的使用场景:1、用来跟踪对象何时被回收。2、避免析构函数后的一些问题。
Java垃圾收集:
常见的收集策略:
- Reference Count(引用计数)
在每个对象有引用的时候进行计数 +1,释放 -1 。当某一个对象引用值=0的时候,说明这个对象需要回收。(优势:简单,不需要暂停整个应用。 缺点:不能处理循环引用) - 跟踪收集器:(包含3种)
1、Mark-sweep collector: 扫描活跃的对象,作标记,标记完后清除没有标记的对象(优点?可以处理循环引用,缺点暂停扫描耗时)
2.copying collector(复制收集): 准备两块空间,保证同一时间只有一块空间处于活跃状态,每次等一块空间满了,扫描把活跃的对象放置另一块空间,留下的就是不活跃的对象。等把活跃对象复制到新的空间后,释放剩余的空间。
3.mark-compat-colletor: 综合了 mark-sweep 和copying collector,分两阶段,一、先进行mark-sweep 标记,清除。把活跃对象压栈底。(减少碎片)
java 采用了分代收集策略:
新生代采用Mark-compat 命名(minorGC),对老年代采用mark-sweep命名(FullGc). 注意:System.gc () 执行的是FullGc
引入 1、Serial Collector 串行程收集,只用于新生代收集 2、concurrent collector 并发收集器,缩短收集时间,用于老生代和 持久代收集。
Dalvik 垃圾回收:
垃圾回收机制: 智能指针(WP 和 SP) sp(Strong point) wp(weak Point)强指针和若指针 ,类似与java的强引用和 弱引用.
dalvik 下面 在 reBase 这个类中.
dalvik垃圾收集3种算法
1、引用计数
2、mark-sweep
3、SemiSpaceCopy