理解Java虚拟机

声明:学习刘望舒《Android晋级解密》第10章Java虚拟机总结

一、Java虚拟机的执行流程

Java虚拟机的运行流程包括两部分,编译时和运行时。其中编译时是将.java文件编译为.class文件;运行时是将.class文件交给Java虚拟机,进行程序的执行。由于最终Java虚拟机执行的是.class文件,因此在编译时是通过何种语言生成的.class文件,Java虚拟机并不关心,因此任何语言只要能编译成.class文件,都可以在Java虚拟机中运行。
例如JDK环境中,通过执行javac语句生成.class文件。
在这里插入图片描述

二、Java虚拟机的结构

在这里插入图片描述

1、类加载:

这是由于特定的语法决定,在编写时,必须遵守相应的规则,这就是为何一次编译可以在不同虚拟机中运行的原因。
.class文件的执行生命周期包括:加载、链接、初始化、使用和卸载,其中初始化包括验证、准备和解析。
那么.class文件是如何加载到系统中的?其是通过类的加载器实现,Java虚拟机中的类加载器 包括系统加载器和自定义加载器。其中系统加载器包括:Bootstrap ClassLoader(引导类加载器)、Extension ClassLoader(拓展类加载器)和Application ClassLoader(应用程序类加载)。

2、运行时数据区域

运行时数据区域包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区。

  • 程序计算器:确定下一条指令的地址,确保程序能够连续执行。在Java虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,因此程序计算器与线程是相绑定且对于该线程是私有的。其对应的是PC寄存器中一块较小的内存空间,且规定该空间不会出现OutOfMemoryError。
  • Java虚拟机栈:平常所说的栈内存,其与Java虚拟机线程相绑定,记录Java方法调用的状态:局部变量、参数、返回值和运算的中间结果等。在线程请求超过分配的栈内存超过Java虚拟机所允许的最大容量时,抛出StackOverflowError;动态扩展时,无法获取到足够的内存时,抛出OutOfMemoryError。
  • 本地方法栈:需要支持Native时,需要本地方法栈,例如C Stacks,如果不需要或者不支持则只包含Java虚拟机栈。
  • Java堆:该区域是线程共享的,用于存储对象的实现。由于该部分无法显示的销毁,该部分的内存区域受垃圾回收管理,也就是通常所说的内存回收机制的作用区域,栈中的内存由其自己管理。为便于垃圾回收管理划分为新生代和老年代,其最终目的是在堆内存容量固定的情况下,能够及时有效提供连续的、足够大的内存空间,否则会抛出OutOfMemoryError。
  • 方法区:该区域也是线程共享,用于存储已加载的类的结构信息,包括运行时常量池、字段和方法信息等。同样如果该区域的内存不满足分配需求时,也会抛出OutOfMemoryError。
    运行时常量:用于存储编译时生成的字面量和符号引用,在类加载后存储在方法区的运行常量池中,因此运行时常量是属于方法区中的一部分,同样在不满足分配需求时,会抛出OutOfMemoryError。
3、对象的创建过程

在Java中新建一个对象时,只需通过执行new指令即可,但对于Java虚拟机却需要执行很对步骤,其中包括如下:

  • 判断:该对象对应的类是否在常量池中定位到一个类的符号引用,并且该符号的引用对应的类,已经被类加载器执行加载、链接和初始化过程,等待使用的状态;
  • 分配内存:确认对象所对应的类加载完成后,Java堆中会分配一块内存,用于存储该对象,其根据Java堆是否完整划分两种方式:
    指针碰撞:Java堆完整,在分配前包括用过和空闲两部分,分配时通过指针指示器从用过区域向空闲区域划出该对象所需的内存空间。
    空闲列表:如Java堆不完整,通过Java虚拟机的列表维护,分配时从列表中查询出该对象所需的内存空间,分配完成后,更新列表。
    以上两种方式,如果在请求分配内存时,没有足够的内存空间,则都会抛出OutOfMemoryError。
  • 并发问题:在频繁的创建对象时,很容易出现并发问题,导致同一个Java堆内存分配给不同的Java对象,为防止该问题的出现,主要采用两种方式:在分配内存时执行同步处理,例如采用CAS算法处理并加上失败重试,保证其原子性,但是如果每次都执行同步处理比较影响性能,因此提出了本地缓冲(Thread Local Allocation Buffer TLAB)方式,线程会预先分配出一块小的内存(TLAB)用于存储新创建的对象,只有当该线程中的TLAB用完且分配到新的TLAB时,才需要同步锁定。
  • 对象标识:在存储对象时,需对对象进行初始化操作,包括对象成员变量、调用类的构造方法,以及对象头的赋值,该操作主要是便于对象生命周期管理,方便内存回收。
4、对象的内存布局

对象在内存中的布局包括对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),其含义包括:

  • 对象头:包括Mark World和元数据指针,Mark World用于存储对象运行时指针的数据,比如HashCode、锁状态标志、GC分代年龄和线程持有锁等。元数据指针用于确定对象的具体类型;
  • 实例数据:存储对象中各种类型的字段信息(包括父类中继续的);
  • 对齐填充:用于占位作用,不一定存在。

三、 Java垃圾回收机制

1、垃圾标记算法概述

垃圾回收机器主要是针对上文中提到的Java堆中的内存处理,快速回收和整理堆内存,便于其他对象的复用,无论何种回收机制的算法处理,其根本原理还是基于对象引用的处理,在Java中按照回收的优先级分为强、软、弱和虚引用。

  • 强引用:在默认通过new创建对象时,就是强引用,在Java堆中即使抛出OutOfMemoryError也不会主动销毁强引用;
  • 软引用:在创建对象时通过SoftReference标识,当内存不足时,会主动回收该类型的对象,如果该类型对象回收后,还是无法满足其他对象的内存申请 ,则抛出OutOfMemoryError,其使用方式如下:
SoftReference<String> softReference = new SoftReference<>("SoftReference");
String softReferenceTest = softReference.get();
  • 弱引用:创建对象时通过WeakReference标识,垃圾回收器一旦发现只具有该类型标识的对象(无其他对象引用),即使内存足够,也会回收该对象,其使用如下:
WeakReference<String> weakReference = new WeakReference<>("WeakReference");
String weakReferenceTest = weakReference.get();
  • 虚引用:创建对象时通过PhantomReference标识,如果一个对象仅持有虚引用,在任何时刻都可能会被垃圾回收器回收,但在回收时,会收到系统通知,这也是虚引用的主要作用,其用法如下:
PhantomReference<String> phantomReference = new PhantomReference<>("PhantomReference",null);
String phantomReferenceTest = phantomReference.get();

了解Java的四种引用方式,便于了解垃圾回收器的标记算法处理,在上文中提到堆对象内存分配中,对象头信息中就包括GC回收相关的数据信息。对象的标识包括引用计数法和根搜索法。

  • 引用计数法
    引用计数法的基本思想是每个对象都有一个引用计数器,当该对象被引用时,该计数器加1,引用失效时 该计数器减1,当该计算器 的值为0时,则该对象就无法被使用,等待垃圾回收器回收。
    但当两个对象相互引用时,此时其对应的计数器的值都不为0,但这两个对象除此之外再无其他引用,就导致这两个对象无法被垃圾回收器回收,造成内存浪费。
	    Test test1 = new Test();
        Test test2 = new Test();

        test1.instance = test2;
        test2.instance = test1;
       
        test1 = null;
        test2 = null;

如上所述,对象test1和对象test2分别持有对方的引用,即使对象赋值为null,但是对象引用计数器值不为0,导致相互循环引用的问题,因此目前该标记的算法基本不被使用。

  • 根搜索法
    其原理是选用一些对象作为GC Roots,作为根对象集合,然后以该GC Roots对象作为起始点,向下搜索,在查找对象时,如果该对象与GC Roots直接或者间接相连接,则说明对象时可达的,否则表示该对象不可达,可以被回收。
    在这里插入图片描述
    如上图所示,Object1、Object2、Object3和Object4与GC Roots均具有直接或间接的连接,表明对象可达,但是Object5和Object6虽然相互引用,但是与GC Roots无直接或间接连接,表明对象不可达。
    在Java中可以作为GC Roots的对象主要包括如下:
  • Java栈中引用的对象;
  • 本地方法栈中JNI引用的对象;
  • 方法区中运行时常量池引用的对象;
  • 方法区中静态属性引用的对象;
  • 运行中的线程;
  • 由引导类加载器加载的对象;
  • GC控制的对象。
    以上说明,能作为GC Roots的对象,其一般是正在使用或其生命周期较长的对象。由此我们想到对象在Java虚拟机中的生命周期包含那几个阶段?
    Java对象在虚拟机中的生命周期包括:创建、应用、不可见、不可达、收集、终结和对象空间重新分配。
2、垃圾收集算法概述

在完成垃圾标记后,Java虚拟机就开始垃圾收集处理,因此垃圾回收器的最终目的是将堆中的内存回收处理,释放出内存,便于其他对象的使用。
垃圾收集常用的算法包括:

  • 标记-清除算法(Mark-Sweep):其方法包括标记和清除两个阶段,先标记可以回收的内存对象,然后回收标记的对象。实现对内存的回收处理,但是其有两个比较严重的缺点:
    效率低:通过对内存中的每个区域去逐一标识,查看该对象是否可以回收,然后再逐一回收标记对象,其效率很低;
    效果差:由于堆内存的不连续性,回收后的内存块不完整,无法适配大内存的申请,导致提前触发一次垃圾收集动作。

  • 复制算法:由于标记清除算法效率和效果都比较差,其根本原因是内存的不连续性导致,而复制算法针对其问题,提出在使用内存时,每次只分配出一半内存使用,在回收时,把正在使用的内存整体复制到另一半未使用的内存中在复制过程中,只复制存活的对象,然后针对已使用的另一半进行整体回收,其解决了效率较低的问题,不需要考虑内存碎片化的问题,但也存在以下问题:
    浪费资源:可使用的内存仅占总体内存的一半,使用效率较低;
    适用面窄:复制算法对于内存中对象存活数量较少的情况效率较高,但是对于存活数量较多的情况,复制到新的另一半后,需要针对该存活的对象进行重写分配处理。
    但是绝大多数对象生命周期都比较短,因此复制算法目前广泛用于新生代中。

  • 标记-压缩算法(Mark-Compact):对于老年代较多的情况,复制算法就不适合,其过多复制操作(创建存活的对象),导致效率较低,标记-清除可以用于老年代较多的情况,但是其效率低,综合这两者后想出标记压缩算法。在标记对象时,把存活的对象统一压缩至内存的一端,使其紧凑排列,剩余内存的均是可以统一整体回收的,不需要再一次逐一复制操作。

  • 分代收集算法:在上述讨论中了解复制算法适用于新生代较多(对象生命周期较短)的情况,标记-压缩算法适用于老年代(对象生命周期较长)较多情况,因此结合这两种方式推出了分代收集的算法处理。其实现思路为:
    划分内存:划分内存为新生代和老年代两个区域;
    新生代划分:新生代中采用复制算法,由于该部分是对象活跃度较高,生命周期较短,同时为了提高内存的使用效率,不可能统一将新生代的内存一分为二,因此提出了划分为Eden空间(正在使用的内存区域)、From Survivor空间(复制转接内存区域)和To Survivor 空间(空的内存区域)三部分,并且三部分的内存比例不同,例如HostSpot虚拟机中大概是Eden空间比两个Survivor为8:1。
    新生代复制:将Eden空间中存活的直接复制到To Survivor中,同时From Survivor存活的对象也复制至To Survivor中。只有两种情况存活的对象不复制值To Survivor中,一种是存活的对象分代年龄超过最大阈值(-XX:MaxTenurinngThreshold),还有一种是To Survivor内存区域已满,这两种情况直接将存活的对象放置至老年代中。
    新生代收集:经过复制后的新生代复制后,目前Eden空间和From Survivor空间已经是完全可回收状态,此时GC执行一次Minor Collection,这两个空间就都可以清空,而To Survivor内存保存着清空前这两部分存活的内存对象。
    老年代处理:老年代中,依然使用标记-压缩算法。
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值