java虚拟机
概述
我们常说的JDK(Java Development Kit)包含了Java语言、Java虚拟机和Java API类库三部分,这是java开发的最小环境,而JRE(Java Runtime Environment)包括了Java API中的Java SE API子集和Java虚拟机这两部分,是Java程序运行的标准环境。可以看出Java虚拟机的重要性,它是整个Java平台的基石,是Java语言编译代码的运行平台。你可以把Java虚拟机看作一个抽象的计算机,它有各种指令集和各种运行时数据区域。Java虚拟机不仅仅可以运行Java,还可以运行kotlin、Croovy、Scala、Jython等。
Java虚拟机家族
Java虚拟机不是一个,而是有很多实现。很多人认为一般说Java虚拟机就是指Oracle的HotSpot虚拟机,实际上不是,不过HotSpot虚拟机应用最广泛就是了,下面简单介绍几种主流的虚拟机实现。
1,HotSpot VM
Oracle JDK和OpenJDK中自带的虚拟机,是最主流和使用最广泛的Java虚拟机。介绍Java虚拟机的文章,如果不做特殊说明,一般都指HotSpot VN。HotSpot VM并非是Sun公司开发的,而是由Longview Technologies这家小公司设计的,它在1997年被Sun收购,Sun公司又在2009年被Oracle收购。2,J9 VM
J9 VM是IBM开发的,目前是其主力发展的Java虚拟机。J9 VM的市场定位和HotSpot VM接近,它是一款设计上从服务器到桌面应用再到嵌入式都考虑到的多用途虚拟机,目前J9 VM的性能水平大致与HotSpot是一个档次的。3,Zing VM
以Oracle的HotSpot VM为基础,改进了许多影响延迟的细节。最大的3个卖点如下:- a,低延迟,”无暂停”的C4 GC,GC带来的暂停可以控制在10ms以下的级别,支持的Java堆大小可以达到1TB。
- b,启动后快速预热功能
- c,可管理性:零开销、可在生产环境全时开启、整合在JVM内的监控工具Zing Vision。
需要注意的是,Android中的Dalvik和ART虚拟机并不属于Java虚拟机,因此这里没有列出他们,11章学习Dalvik和ART虚拟机。
Java虚拟机执行流程
执行一个Java程序时,它的执行流程是这样,如下图所示:
可以看出Java虚拟机执行流程分为两大部分:编译时环境和运行时环境,当一个Java文件经过Java编译器编译后会生成class文件,它会由Java虚拟机来处理。Java虚拟机与Java虚拟机语言没有什么必然的关系,它只与特定的二进制文件:class文件有关。因此无论任何语言只要能编译成class文件,就能被Java虚拟机识别并执行。如下图所示
Java虚拟机结构
如图所示,这里讲的体系结构,是指Java虚拟机的抽象行为,而不是具体的比如HotSpot VM的实现。按照Java虚拟机规范,抽象的java虚拟机如图3所示。
java虚拟机结构包括运行时数据区域、执行引擎、本地库接口和本地方法库,方法区和堆是所有线程共享的数据区域。
类的生命周期
一个Java文件加载到Java虚拟机内存中从内存中卸载的过程被称为类的生命周期。类的生命周期包括的阶段:加载、链接、初始化、使用和卸载。广义上类的加载分为:加载、链接(验证、准备和解析)、初始化。
接下来介绍各个阶段所做的工作,如下所示:
- 1,加载:查找并加载class文件
- 2,链接:包括验证、准备和解析
- a,验证:确保被导入类型的正确
- b,准备:为类的静态字段分配字段,并用默认值初始化这些字段
- c,解析:虚拟机将常量池内的符号引用替换为直接引用
- 3,初始化:将类变量初始化为正确初始值
根据《深入理解Java虚拟机》,加载阶段(并非类的加载)主要做了三件事: - 1,根据特定名称查找类或接口类型的二进制字节流
- 2,将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 3,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类加载子系统
Java虚拟机中有两种类加载器:系统加载器和自定义加载器。其中系统加载器包括以下三种:
- 1,Bootstrap ClassLoader (引导类加载器)
用c/c++实现的,用于加载指定的JDK核心类库,java.lang.,java.uti.等。它用来加载/jre/lib和-Xbootclasspath参数指定的目录。
JVM就是通过引导类加载器创建一个初始类来完成的,由于类加载器是C/C++实现的,所以该加载器不能被Java代码所访问,但是可以查询某个类是否被引导类加载器加载过。
- 2,Extensions ClassLoader (扩展类加载器)
Java实现,加载以下目录的类文件:
- a,/jre/lib/ext
- b,系统属性java.ext.dir所指定的目录
- 3,Application ClassLoader (应用程序加载器)
又称作System ClassLoader(系统类加载器),这是因为这个类加载器可以通过ClassLoader的getSystemClassLoader方法获取到,用于加载以下类库文件:
- a,当前应用程序ClassPath目录
- b,系统属性java.class.path指定的目录
运行时数据区域
GC算法
垃圾标记算法
1,引用计数法
每个对象有一个引用计数器,当对被引用时它的引用计数器就加1,引用失效就减1,引用计数器中的值为0时,则该对象不能被使用,变成了垃圾。这不是主流的垃圾标记算法,不选择引用计数法来判断垃圾主要是因为引用计数法没有解决对象之间相互引用的问题。
2,根搜索法
这是目前主流的垃圾标记算法
垃圾收集算法
垃圾被标记后,GC就会对垃圾进行收集,接下来介绍常用的垃圾回收算法
- 1,标记清除
标记阶段:标记出可以被回收的对象
清除阶段: 回收被标记的对象所占用的空间
标记–清除算法是基础的,是因为后面的几个算法都是在此基础上进行改造的。标记清除算法有两个缺点:
- a,标记的效率不高,
- b,容易产生大量的不连续的碎片,碎片太多可能会导致后续没有足够的连续内存分配给较大的对象,从而提前出发GC。
- 2,复制算法
复制算法是每次只使用一半的内存,GC时,遍历当前的区域,把存活的对象复制到另外一个区域中,最后将当前使用的区域的可回收对象进行回收,复制算法每次都对半个区域进行回收,不需要考虑碎片的问题,缺点是每次只能使用一半的内存。复制算法的效率和存活的对象数目有很大的关系,如果存活的对象很少,那么效率就高,由于绝大多数对象的生命周期很短,并且这些对象都在新生代中,所以复制算法被广泛应用于新生代中。
- 3,标记-压缩算法
新生代使用复制算法,老年代就不适合使用了,因为老年代的对象存活率高,若在老年代使用复制算法,这样就会产生较多的复制操作,导致效率变低。
与标记清除算法不同的是,在标记可回收的对象之后,将存活的对象压缩到内存的一端,使它们紧紧排列在一起,然后对边界以外的内存进行回收,回收后,已用的和未被使用的内存各占一边。
- 4,分代收集
Minor Collection:新生代垃圾收集
Full Collection:对老年代进行收集,又称Major Collection,Full Collection通常情况下会伴随至少一次的Minor Collection,它的收集频率低,耗时较长
执行一次Minor Collection时,Eden空间的存活对象会被复制到To Survivor空间,并且之前进过一次Minor Collection并在From Survivor空间的对象也会被复制到To Survivor空间。两种情况下不会复制到To Survivor空间而是晋升到老年代。一种是存活的对象的分代年龄超过-XX:MaxTenuringThreshold (用于控制对象经历多少次Minor GC才晋升到老年代)所指定的阈值;另一种是To Survivor空间满了,也是达到阈值。当所有存活对象都被复制到To Survivor空间,或者晋升到老年代,也就意味着Eden区和From Survivor区剩下的都是可回收对象。