JVM虚拟机
继续做知识点铺垫。
我这里再简单梳理下虚拟机相关知识点,当然只是热修复与插件化会涉及到的部分。
一、JVM整体结构
java文件先通过编译器生成虚拟机执行的字节码文件,该文件被ClassLoader加载到内存,由虚拟机分区域进行内存管理,然后子系统会执行相关的工作:包括将虚拟机字节码编译为机器码、针对堆内存进行GC等等。同时虚拟机通过本地库接口连接native方法与本地方法库。
1.1 编译流程
class文件生成是通过javac编译器程序执行得到的,整个编译流程简单总结如下:
源代码->词法分析器->Token流->语法分析器->语法树/抽象语法树->语义分析器->
注解抽象语法树->字节码生成器->jvm字节码
这个部分就不管了,了解下点到为止,感兴趣的可以自行研究。
1.2 类加载流程
类加载器介绍:
Bootstrap Loader
负责加载系统类;
Extension ClassLoader
负责加载扩展类(就是继承类和现类);
System ClassLoader
负责加载用户类。
(类加载器Andorid有比较大的区别,但是思想还是一样的)
这里牵涉到几个机制:
-
全盘负责
:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。 -
双亲委派
:先判断当前类加载器是否加载过,如果没有则传递到父类类加载器中,父类加载器没有搜到该Class无法完成该加载,子加载器才会尝试自己去加载该Class。 -
缓存机制
:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
如何判断加载的是同一个类:类名相同、包名相同、由相同的ClassLoader加载。
类加载的过程:
image.png-
Loading
:类的信息从文件中获取并加载到JVM内存中。 -
Verifying
:检查读入的结构是否符合JVM规范。 -
Preparing
:验证完正确,会分配一个结构来存储类信息。 -
Resolving
: 把这个类的常量池中的所有符号引用转变为直接引用。 -
Initializing
:执行静态初始化程序,把静态变量初始化成指定的值。
1.3 内存管理
1.3.1 内存区域划分
-
程序计数器
:一个指针,记录当前线程所执行到的字节码行号; -
虚拟机栈
:存放方法执行时的所有数据; -
本地方法栈
:专门为native方法服务的; -
方法区
:存储被虚拟机加载的类信息、常量、静态变量、及时编译器编译后等数据; -
java堆
: 所有new创建的对象的内存都在堆中分配。是虚拟机中最大的一块内存,是GC要回收的部分。
线程共享区包括:方法区 、java堆。
线程隔离区包括:虚拟机栈、本地方法栈、程序计数器。
1.3.2 虚拟机栈解析
以线程为单位由栈结构来进行管理。栈中对应的栈元素叫栈帧,它是用于支持虚拟机进行方法调用和方法执行的数据结构,每个方法从调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧包含的内容:局部变量表、栈操作数、动态链接、方法出口。
1.3.3 java堆区解析
java堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
简单看下区域的划分以及虚拟机默认分配的比例:
另外还有个永久代,这部分属于方法区,就捎带提一嘴。
那么,这个比例可以配置吗?当然可以,如果玩java服务器的话,肯定要玩玄学调参。参考如下JVM参数选项:
参数名 | 介绍 |
---|---|
-Xms | 初始堆大小。如:-Xms256m |
-Xmx | 最大堆大小。如:-Xmx512m |
-Xmn | 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% |
-Xss | JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。 |
-XX:NewRatio | 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio | 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
-XX:PermSize | 永久代(方法区)的初始大小 |
-XX:MaxPermSize | 永久代(方法区)的最大值 |
-XX:+PrintGCDetails | 打印 GC 信息 |
-XX:+HeapDumpOnOutOfMemoryError | 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用 |
jvm 可配置的参数选项可以参考 Oracle 官方网站给出的相关信息:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
堆内存管理规则:
新生代
:
新创建的对象先被放在Eden,Eden满了,执行Minor GC,同时把未被 GC 的对象 移动到 S0(from) 或 S1(to) 中。 最后使 S0(from) 、S1(to) 其中一个置为空,这是由GC算法决定的,因为Minor GC对应的是复制算法,因此需要内存交换区,所以多分了from和to这两块区域。
老年代
:
内存区存放了经过多次 Minor GC 后仍然不能被 GC 的对象。Old区满了,执行Major GC。
另外要补充一点的是:新生代和新生代都可以主动触发 stop-the-world 事件,挂起所有任务,执行 GC 操作。 被挂起的任务只有在 GC 执行完毕后,才会恢复执行。
1.3.4 垃圾回收
垃圾收集算法:
-
引用计算算法(jdk1.2之前)
:堆中的每个对象对应一个引用计数器,创建对象置为1,每次引用到此对象+1,其中一个引用销毁-1,变为0即满足回收。致命缺点:循环引用的对象无法进行回收。 -
可达性算法(jdk1.2之后)
:确定GC root,寻找路径可达的引用节点,形成可达性树,不在树上的节点即满足回收条件。
在Java语言里,可作为GC Roots对象的包括如下几种:- 虚拟机栈(栈桢中的本地变量表)中的引用的对象;
- 方法区中的类静态属性引用的对象;
- 方法区中的常量引用的对象;
- 本地方法栈中JNI的引用的对象;
引用类型:
-
强引用(StrongReference)
:JVM 宁可抛出 OOM ,也不会GC; -
软引用(SoftReference)
:只有在内存空间不足时,才会被回收; -
弱引用(WeakReference)
:在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存; -
虚引用(PhantomReference)
:主要用来判断对象是否将要被回收,属于GC回收标志。
垃圾回收算法:
标记-清除算法(Tracing Collector)
:遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。对没标记的对象全部清除。
优点:对不存活对象进行处理,在存活对象高的情况下非常高效。
缺点:清除对象不会整理,造成内存碎片,这部分内存碎片属于内部碎片。
复制算法(Coping Collector)
: 遍历所有的GC Roots,将可达的对象复制到另一块内存空间,遍历完后清空原来的内存空间(剩下的都是不可达对象)。
优点:对可达对象进行复制,在存活的对象比较少时极为高效。
缺点:需要额外的内存空间。
标记-整理算法(Compacting Collector)
:在标记-清除算法基础上,增加存活对象内存整理。
优点:不造成内存碎片,也不需要额外内存空间
缺点:整理过程耗时,效率不高。
因此内前面咱们提到的两个GC:
Minor GC
: 是发生在新生代中的垃圾收集动作,通常对象存活时间较短,因此采用的是复制算法。
Full GC
: 是发生在老年代的垃圾收集动作,通常对象存活时间较长,因此采用的是标记-清除算法(考虑执行效率)/标记-整理算法(考虑内存利用率)。
GC触发时机:
- Java虚拟机无法再为新的对象分配内存空间了;
- 手动调用System.gc()方法(强烈不推荐:即使手动调了也不会立马回收,还会加大虚拟机压力);
- 低优先级的GC线程,被调度时。
二、JVM与DVM不同
- 执行文件不同,JVM对应class,DVM对应dex。dex文件更精简。
- 虚拟机架构不同,JVM基于栈,DVM基于寄存器。寄存器比是内存更快的存储介质,因此DVM运行更快。
- 类加载系统区别,JVM基于Bootstrap Loader、Extension ClassLoader、App ClassLoader,DVM基于ClassLoader、BaseDexClassLoader、PathClassLoader、DexClassLoader。
- 项目中虚拟机数目不同,JVM只能同时存在1个,DVM存在多个一个进程对应一个。
三、ART与DVM相比较的优势
编译优化
DVM使用JIT来将字节码编译为机器码,ART引入AOT预编译。JIT生成的机器码缓存在内存中,优化解释模式的执行,属于运行时优化。而OAT生成的机器码缓存为文件,属于持久化优化,因此下次执行肯定是AOT的方式更快,不用重新生成机器码,直接拿来用。
但是OAT的缺点是每次编译dex2oat CPU占用率非常高,可能造成部分任务抢占不到CPU。另外ART安装时间更长,存储空间占用更大。
垃圾回收优化
改善了Dalvik GC流程,将其非并发过程改变成了部分并发。缩短了任务挂起时间,据官方测试数据说gc效率提高2倍。
内存优化
ART对比DVM 提高了内存使用率,减少了内存碎片化。
之前写的相关文章:
虚拟机(一)-JVM执行java代码流程浅析
虚拟机(二)-Dalvik执行java代码流程浅析
虚拟机(三)-JVM 、DVM 、ART简单对比
虚拟机(四)-JVM垃圾回收