1. 对象
1.1 对象的创建方式
创建过程
一.虚拟机遇到一条new指令时, 首先将去检查这个指令的参数是否能在常量池中定位到一
个类的符号引用, 并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。 如果没有, 那必须先执行相应的类加载过程.二.在类加载检查通过后, 接下来虚拟机将为新生对象分配内存。
三.内存分配完成后, 虚拟机需要将分配到的内存空间都初始化为零值( 不包括对象头).
四.接下来, 虚拟机要对对象进行必要的设置 ,也就是设置对象头
五.在上面工作都完成之后, 从虚拟机的视角来看, 一个新的对象已经产生了
六.执行new指令之后会接着执行< init> 方法,即构造函数
内存分配
内存分配:两种方式分别为 内存规整的指针碰撞,和空闲列表,并且为了保证安全性采用cas且配上失败重试的方式保证更新操作的原子性,另一种安全方式,为每个线程预先分配一小块内存。
1.2对象的内存布局
对象包含对象头,示例数据,对象填充
对象头
HotSpot虚拟机的对象头包括两部分信息, 第一部分用于存储对象自身的运行时数据,如哈希码( HashCode) 、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等, 这部分数据的长度在32位和64位的虚拟机( 未开启压缩指针) 中分别为32bit和64bit, 官方称它为“Mark Word”.如果对象处于未被锁定的状态下, 那么Mark Word的32bit空间中的25bit于存储对象哈希码, 4bit用于存储对象分代年龄, 2bit用于存储锁标志位, 1bit固定为0, 而在其他状态( 轻量级锁定、 重量级锁定、 GC标记、 可偏向) 下对象的存储内容见下图。
在即将进入同步代码块时,如果此同步对象没有被锁定,虚拟机会首先在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的的markword的拷贝,这时的线程堆栈与对象头的状态如下图:
对象头的另外一部分是类型指针, 即对象指向它的类元数据的指针, 虚拟机通过这个指针来确定这个对象是哪个类的实例.如果对象是一个Java数组, 那在对象头中还必须有一块用于记录数组长度的数据, 因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小, 但是从数组的元数据中却无法确定数组的大小。
实例数据
实例数据部分是对象真正存储的有效信息, 也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的, 还是在子类中定义的, 都需要记录起来。 存储顺序受分配策略参数Java源码中定义顺序影响,相同宽度的字段总是被分配到一起。
对齐填充
第三部分对齐填充并不是必然存在的, 也没有特别的含义, 它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍, 换句话说,就是对象的大小必须是8字节的整数倍。 而对象头部分正好是8字节的倍数( 1倍或者2倍) ,因此, 当对象实例数据部分没有对齐时, 就需要通过对齐填充来补全。
1.3 栈、堆、方法区
2.执行引擎
执行引擎是 Java 虚拟机最核心的组成部分之一。「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力,区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机执行引擎是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。java是半解释半编译型语言指的是jvm中两种方式并存。
有两种引擎,解释器和JIT,二者不能同时使用,后者执行效率过高。
2.1 java代码编译和执行过程
传统解释执行
Java 语言常被人们定义成「解释执行」的语言,但随着 JIT 以及可直接将 Java 代码编译成本地代码的编译器的出现,这种说法就不对了。只有确定了谈论对象是某种具体的 Java 实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。
无论是解释执行还是编译执行,无论是物理机还是虚拟机,对于应用程序,机器都不可能像人一样阅读、理解,然后获得执行能力。大部分的程序代码到物理机的目标代码或者虚拟机执行的指令之前,都需要经过下图中的各个步骤。下图中最下面的那条分支,就是传统编译原理中程序代码到目标机器代码的生成过程;中间那条分支,则是解释执行的过程。
如今,基于物理机、Java 虚拟机或者非 Java 的其它高级语言虚拟机的语言,大多都会遵循这种基于现代编译原理的思路,在执行前先对程序源代码进行词法分析和语法分析处理,把源代码转化为抽象语法树。对于一门具体语言的实现来说,词法分析、语法分析以至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是 C/C++。也可以为一个半独立的编译器,这类代表是 Java。又或者把这些步骤和执行全部封装在一个封闭的黑匣子中,如大多数的 JavaScript 执行器。
Java 语言中,Javac 编译器完成了程序代码经过词法分析、语法分析到抽象语法树、再遍历语法树生成字节码指令流的过程。因为这一部分动作是在 Java 虚拟机之外进行的,而解释器在虚拟机的内部,所以 Java 程序的编译就是半独立的实现。
许多 Java 虚拟机的执行引擎在执行 Java 代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。而对于最新的 Android 版本的执行模式则是 AOT + JIT + 解释执行,关于这方面我们后面有机会再聊。
javac编译流程
JVM虚拟机执行引擎的执行过程
- 解释器:当java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行
- JIT编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
3.垃圾回收
3.1 判断对象存活算法
引用计数法
较为简单,对每一个对象保存一个完整的引用计数属性。用于记录对象被引用的情况。
优点:实现简单、垃圾对象便于辨别;判断效率高,回收没有延迟性。
缺点:增加存储开销,增加时间开销,有循环引用问题。
可达性分析算法
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链( Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说, 就是从GC Roots到这个对象不可达时,则证明此对象是不可用的。
固定可作为GC Roots的对象包括以下几种:
1.虚拟机栈( 栈帧中的本地变量表) 中引用的对象。比如各线程被调用的方法堆栈中使用到的参数/局部变量/临时变量.
2.方法区中类静态属性引用的对象。比如java类的引用类型静态变量
3.方法区中常量引用的对象。 比如字符串常量池里的引用.
4.本地方法栈中JNI( 即一般说的Native方法)引用的对象。
5.java虚拟机 内部引用,如基本数据类型对应的class对象,一些常驻的异常对象(比如 nullpointexception,outofmemoryerror)等,还有系统加载器。
6.所有被同步锁(synchronized)持有的对象。
7.反应java虚拟机内部情况的jmxbea jvmtl中注册的回调 本地代码缓存。
**注:**真正宣告一个对象的死亡至少需要两次标记: 在进行可达性分析后确定没有引用链中会进行第一次标记,随后会判断对这些对象进行筛选,条件是是否有必要执行finalize()方法. 执行这个方法时若再一次挂接到引用连上则移出垃圾回收的集合,一个对象的finalize()方法只会被系统自动调用一次,不建议使用。
3.2 java引用
1.强引用: 是指代码之间的普遍存在的引用赋值,无论什么情况下,只要强引用关系还在,垃圾收集器就永远不会回收对象
2.软引用: 用来描述一些还有用但是非必须的对象.在系统将要发生内存溢出异常之前, 将会把这些对象列进回收范围之中进行第二次回收。 如果这次回收还没有足够的内存, 才会抛出内存溢出异常。
比如高速缓存
3.弱引用: 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
4.虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
3.3 垃圾回收算法
分代收集理论
java7逻辑上分为新生代,老年代,永久代
java8逻辑上分为新生,老年,元空间
及建立在两个假说之上
- 弱分代假说:绝大数对象都是朝生夕灭的.
- 强分代假说:熬过越多次垃圾收集过程的对象就难以消亡.
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数
垃圾收集机器的原则–收集器应该讲java堆划分出不同的区域,然后将对象依旧据年龄分配到不同的区域之中存储.
目前几乎所有的GC都采用分代收集算法执行垃圾回收。
标记清除算法
算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象, 在标记完成后统一回收所有被标记的对象, 它的标记过程其实在前一节讲述对象标记判定时已经介绍过了。 示例图如下:
主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记复制算法
为了解决效率问题, 一种称为“复制”( Copying) 的收集算法出现了,它将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。 这样使得每次都是对整个半区进行内存回收, 内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。适合存活对象比较小的情况。
缺点:需要两倍的内存空间,对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维持region之间的对象引用关系,不管是内存占用和事件开销都不小。
标记整理算法
根据老年代的特点, 有人提出了另外一种“标记-整理”( Mark-Compact) 算法, 标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向一端移动, 然后直接清理掉端边界以外的内存, “标记-整理”算法的示意图.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zoRXolRJ-1616318279983)(http://qny.madlandduke.cn/1616318029052image-20200621114021160.png)]
优点:笑出了标记-清除算法当中,内存区域分案的缺点,分配新对象时只需持有一个内存的起始地址即可。消除了复制算法当中,内存减半的高额代价;
缺点:从效率上来说,标记整理算法要低于复制算法,移动对象的同时如果对象被其他对象应用,则还需要调整引用地址。移动过程中,需要用户暂停应用程序。即:STW