JVM架构
jvm有几个区域:方法区、堆、Java栈、程序计数器、本地方法栈
方法区
方法区被所有线程共享。存放 类信息、类的字段信息和方法信息、常量、static类变量Java栈
Java栈线程之间不共享。
Java栈是由很多的栈帧(stack frame)或者说帧(frame)组成的,一个栈帧包含一个Java方法调用状态。用来存放方法的局部变量、被调用时传进来的参数、返回值 以及运算的中间结果等等。Native栈
用来存放Native方法的调用状态堆
用于存放对象。 Java程序在运行时创建的所有类实例或数组(数组在Java虚拟机中是一个真正的对象)都放在同一个堆中。由于Java虚拟机实例只有一个堆空间,所以所有线程都将共享这个堆。程序计数器(PC寄存器)
每一个线程有一个程序计数器。当线程执行某个Java方法时,程序计数器的值总是下一条被执行指令的地址。类加载子系统
读取class文件,加载Java类到内存。
基于栈的jvm 和 基于寄存器的jvm
在虚拟机栈的每一个栈帧中(即每一个方法调用中),存在一个「操作数栈」,用于存放运算的临时数据。
基于栈 和 基于寄存器 的区别在于这个「操作数栈」
- 基于栈
对于基于栈的jvm,这个「操作数栈」的区域当然是一个栈,方法体运行过程中生成的临时数据,会压入栈中,需要的时候再从栈中弹出。
实现比较简单,只是单纯的压栈出栈
对于每一条字节码指令,无需指定操作数地址,所以单个指令长度较短
因为要频繁的压栈出栈,所以字节码指令会增加 - 基于寄存器
对于基于寄存器的jvm,这个「操作数栈】的区域变成了多个虚拟寄存器,方法体运行过程中的临时数据会存放到这些寄存器中。
避免了频繁的压栈出栈操作,所以字节码指令数量更少
因为要明确指定操作数地址,所以字节码单个指令长度会翻倍
为什么 dalvik 和 art 要基于寄存器
从上面的分析看,很明显,基于寄存器的jvm运行更快!
类加载器
类加载器负责动态加载Java类到Java虚拟机的内存空间中。
- 双亲委托机制
“双亲委托机制”是指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类 路径中查找并装载目标类。这一点是从安全角度考虑的
GC垃圾回收机制
如何判断对象是否可以被回收
- 引用计数法
- 根搜索算法:
- 从GC root出发查找引用链,无法触及到的对象可以被回收。
- GC-root包括:虚拟机栈中引用的对象、类静态属性引用的对象、常量引用的对象
强引用、弱引用、软引用、虚引用
GC算法
jvm将对象分为三个区域, 新生代(Young Generation) 、 老年代(Old Generation) 和 持久代(Permanent Generation)。
- 持久代:
- 存放java类的类信息,存放静态文件,如final常量,static常量,常量池等。几乎不回收
- 新生代:
- 新生代使用拷贝算法:新生代中存在一个Eden区和两个Survivor区,其中一个Survivor始终为空。 eden区gc时,存活的对象会复制到其中一个survivor区。 Survivor区gc时,会拷贝到另一个空闲的Survivor区。 当对象多次gc存活一定次数之后,会移动到老年代
- jvm中 eden区和Survivor区的大小为8:1
- 老年代:
- 使用标记整理算法。标记需要清除对象,将存活的对象往内存的一端移动,再将需要清除要回收的对象。这样的算法可以避免内存碎片的产生
- 大对象会一生成就会直接放到老年代。新生代容量较小,避免新生代频繁gc
GC Root
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
jar文件和dex文件的差异
一个jar文件包含多个class文件,不同的class文件之间没有关联。 如果有相同的常量信息等数据,会产生冗余,增大文件大小。
一个dex文件包括索引区和数据区,多个类共用一个索引区。这样相同的常量信息等可以避免重复冗余,可以大大减小文件大小。 同时,因为只有一个文件,也可以减少文件io的开销。
对象一定是在堆上的吗?
不一定,取决于不同jvm的设计优化。
在虚拟机栈的调用中,jvm会对方法体力创建的对象做逃逸分析。 通过逃逸分析,可以分析出这个对象的引用是否只存在于方法体内部。
如果发现某个对象并没有逃逸到方法体之外的话,就可能对其进行优化。不会将对象分配到堆上,可以减少gc的消耗。
具体的方法是,把这个对象拆解成若干个其中包含的若干个局部变量来代替。这个过程就是标量替换