1、前言
作为一名 Java 程序开发,几乎每天都在和虚拟机打交道,今天就说说我对于虚拟机的理解。
我们平常接触到的虚拟机有 jvm 、dvm 、art 虚拟机。 dvm 是基于 jvm 优化而用于移动端,art 针对 dvm 又进行了优化。 本质都是 jvm 优化版,所以本章以讲解 jvm 为主。
2、class 文件(jvm 的执行文件)
什么是 class 文件?
能被 jvm 识别、加载并执行的文件格式,是一种 8 位字节的二进制流文件,它记录了一个类文件的所有信息并远远多于 java 源文件的内容(如 this、super 等这些在 class 文件中都会被编译器赋值)
如何生成 class 文件?
编码生成.java 文件 通过 jdk 的 javac 命令生成 .java -> .class , 每个 java 类或接口都会编译生成一个对应的 class 文件(也因为此当虚拟机查找类文件中的内容时会有大量的 io 操作),
编译流程:
这里我们只做了解即可,编译原理已经忘的差不多了,很尴尬。
缺点?
每个类文件记录了大量的信息,占用内存较大(class 的文件结构可以通过二进制阅读软件去查看,有兴趣可以去详细了解其结构);
基于堆栈的加载模式,加载速度慢;
文件 io 操作多,类查找慢。
因此它并不适合移动端,由此产生了 dvm、art 这类移动端的虚拟机
3、jvm 的结构
class 文件由类加载器载入内存,加载时会先对 class 文件进行校验、解析等操作,如图:
4、内存管理
内存空间分为: 栈区、方法区、本地方法栈区、堆区
Java 栈区:
存放 Java 方法执行时的所有数据,由栈帧组成
Java 栈帧:
包含:局部变量表、栈操作数、动态链接、方法出口
每个方法从调用到执行完成对应了一个栈帧在虚拟机栈中的入栈和出栈,当超过栈所允许的最大深度时就会抛出 stackoverflow 异常(比如方法中死循环)
本地方法栈:
专门为 native 方法服务的栈区
方法区:
存放加载的类信息、常量、静态变量、即时编译器编译后的数据
该区会一直占用内存
堆区:
所有创建的对象都存放于该区,是虚拟机中最大的一块内存,也是 GC 要回收的部分
在堆区内存中又划分了几块: 新生代区(young generation)、老生代区(old generation)、永久区(permanent generation)
新生代区: 存放新生成的对象
老生代区: 当新生代区内存不足时,根据算法将新生代的某些对象移入该区,为新生代区提供内存存放新对象,当新生代区和老生代区都无可用内存时就会出现 OOM
为什么要在堆区区分新生代区和老生代区?有什么好处?
如此区分,可以允许开发者去动态调整新生代区和老生代区的大小,便于内存分配以适应不同场景,
如一些大型服务类场景 并不需要频繁创建对象 就可以让老生代内存大一些 方便这些服务常驻 提高服务稳定性
如即时通讯 临时消息对象多 就可以把新生代调整的大一些 老生带小一些 加快内存分配
5、内存垃圾回收 (GC)
虚拟机要去回收垃圾,首先得知道哪些对象是垃圾对象,然后才能去回收。这里就涉及到垃圾收集算法和垃圾回收算法
垃圾收集算法
1、引用计数算法
这是 jvm 早期使用的算法,jdk1.2 之前所用
创建对象时会为其产生引用计数器并加 1,都有新引用引用该对象时计数器 +1,引用该对象的引用销毁时计数器-1,当为 0 时即为垃圾对象,可以被回收
缺陷: 两个对象相互引用时(环形引用),计数器都为 1,但是两者都不可达,却导致无法回收
代码举例:
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null;
b = null;
这个时候 a,b 引用被置空,但其 ab 两个对象还在堆中且相互引用,我们也没法通过引用找到这两个对象,他们也无法被回收。
2、可达性算法 (根搜索算法)
jdk1.2 之后对垃圾收集算法进行了改进
将所有引用关系视作一张图
从 GcRoot 节点开始寻找对应的所有引用的节点,找到节点后继续寻找它的引用节点,当所有引用节点寻找完毕后,没有被引用的节点不可达节点,就是垃圾对象,这样也解决了环引用对象回收问题
说到引用,这里简单说一下,引用有几种类型:
强引用 Object obj = new Object(); 不回收
软引用 内存不足时回收
弱引用 WeakReference wf = new WeakReference(obj); gc 时回收
虚引用 gc 时回收
垃圾回收算法
通过垃圾收集算法找到了要回收的垃圾如何进行回收呢?
1、标记-清除算法
将未被引用的对象(不可达对象)标记为可回收对象,垃圾回收时将其清除
优点: 不需要对对象进行移动,仅对不存活的对象进行处理,在存活对象多时会极为高效
缺点: 直接清除对象置空,容易造成内存碎片,不利于后续内存分配
2、复制算法
将可达对象复制到空闲内存中,不可达的直接跳过,最后将原来的内存清空
优势:存活对象少时高效
缺点:需要更多内存作交换空间 (需要内存大)
3、标记-整理算法
清除不可达对象后,将后续可达对象移动到该清除后的内存区域并更新引用的位置
在标记-清除算法的基础上进行了移动,成本更高,但解决了内存碎片问题
总结:
这三种算法各有优劣,在虚拟机中会动态根据情况采用不同的算法,而不是只用一种算法
6、垃圾回收的触发
1、jvm 无法再为新对象分配内存空间时触发
2、手动调用 System.gc() (不推荐使用) 不会立马去执行垃圾回收,会加大虚拟机压力
3、低优先级的 GC 线程被启动时会触发
看到这里,相信对 jvm 已经有了比较深的了解了,下面再将 jvm 、dvm、art 进行对比
7、jvm、dvm、art 之间的比较
** jvm 与 dvm**
1、执行文件格式不同,class / dex(将多个.class 文件通过命令一个生成 dex 文件)
2、dvm 类加载系统与 jvm 区别较大
3、dvm 可以同时存在多个(某一个挂掉的话不会影响其他 dvm 的运行,确保稳定性),jvm 只能同时存在 1 个
4、dalvik 基于寄存器的,jvm 基于栈,寄存器被内存更快。
ART
dvm 使用 JIT 动态将字节码转换成机器码效率低。 (JIT : Just In Time ,每次运行时转码)
ART 采用了 AOT(ahead of time 安装时就进行转码)预编译技术,执行速度快
ART 会占用更多应用安装时间和存储空间(空间换时间)
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于