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指定的目录
运行时数据区域
通常我们将Java的内存分为堆内存(Heap)和栈内存(Stack),实际上这种分发并不准确,Java的内存区域划分实际上比这要复杂,Java虚拟机在执行的Java的过程中会把它所管理的内存划分为不同的数据区域,根据《Java虚拟机规范(Java SE7)》的规定,这些数据区分为程序计数器PC、Java虚拟机栈、本地方法栈、Java堆和方法区。下面一一对他们进行介绍。
- 1,程序计数器PC
当前线程所执行的字节码行号指示器,每个线程都有自己计数器,是私有内存空间,该区域是整个内存中较小的一块。当线程正在执行一个Java方法时,PC计数器记录的是正在执行的虚拟机字节码的地址;当线程正在执行的一个Native方法时,PC计数器则为空(Undefined)。
- 2,虚拟机栈
虚拟机栈,生命周期与线程相同,是Java方法执行的内存模型。每个方法(不包含native方法)执行的同时都会创建一个栈帧结构,方法执行过程,对应着虚拟机栈的入栈到出栈的过程。
栈帧(Stack Frame)结构
栈帧是用于支持虚拟机进行方法执行的数据结构,是属性运行时数据区的虚拟机站的栈元素。见上图, 栈帧包括:
- a, 局部变量表 (locals大小,编译期确定),一组变量存储空间, 容量以slot为最小单位。
- b, 操作栈(stack大小,编译期确定),操作栈元素的数据类型必须与字节码指令序列严格匹配
- c, 动态连接, 指向运行时常量池中该栈帧所属方法的引用,为了 动态连接使用。
- 1, 前面的解析过程其实是静态解析;
- 2, 对于运行期转化为直接引用,称为动态解析。
- d, 方法返回地址
- 1, 正常退出,执行引擎遇到方法返回的字节码,将返回值传递给调用者
- 2, 异常退出,遇到Exception,并且方法未捕捉异常,那么不会有任何返回值。
- e, 额外附加信息,虚拟机规范没有明确规定,由具体虚拟机实现。
Java虚拟机规范规定该区域有两种异常:
a,StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出
b,OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出
3,本地方法栈
本地方法栈则为虚拟机使用到的Native方法提供内存空间,而前面讲的虚拟机栈式为Java方法提供内存空间。有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,比如非常典型的Sun HotSpot虚拟机。
异常(Exception):Java虚拟机规范规定该区域可抛出StackOverFlowError和OutOfMemoryError。
- 4,java堆
Java堆,是Java虚拟机管理的最大的一块内存,也是GC的主战场,里面存放的是几乎所有的对象实例和数组数据。JIT编译器有栈上分配、标量替换等优化技术的实现导致部分对象实例数据不存在Java堆,而是栈内存。
- a, 从内存回收角度,Java堆被分为新生代和老年代;这样划分的好处是为了更快的回收内存;
- b, 从内存分配角度,Java堆可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB);这样划分的好处是为了更快的分配内存;
对于填充数据不是一定存在的,仅仅是为了字节对齐。HotSpot VM的自动内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例数据不是8的倍数,便需要填充数据来保证8字节的对齐。该功能类似于高速缓存行的对齐。
另外,关于在堆上内存分配是并发进行的,虚拟机采用CAS加失败重试保证原子操作,或者是采用每个线程预先分配TLAB内存。
异常(Exception):Java虚拟机规范规定该区域可抛出OutOfMemoryError。
- 5,方法区
方法区主要存放的是已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。GC在该区域出现的比较少。
异常(Exception):Java虚拟机规范规定该区域可抛出OutOfMemoryError。
- 6,运行时常量池
运行时常量池也是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。运行时常量池除了编译期产生的Class文件的常量池,还可以在运行期间,将新的常量加入常量池,比较常见的是String类的intern()方法。
- 1, 字面量:与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。
- 2, 符号引用:编译语言层面的概念,包括以下3类:
- a,类和接口的全限定名
- b,字段的名称和描述符
- c, 方法的名称和描述符
但是该区域不会抛出OutOfMemoryError异常。
以上运行时数据区域内容学习自gityuan博客,gityuan.com。
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区剩下的都是可回收对象。