一、初识JVM
本章讲述JVM概念、发展历史等。
1、虚拟机(Virtual Machine)概念
JRE是由Java API和JVM组成的。JVM的主要作用是通过Class Loader来加载Java程序,并且按照Java API来执行加载的程序。
虚拟机是通过软件的方式来模拟实现的机器(比如说计算机),它可以像物理机一样运行程序。设计虚拟机的初衷是让Java能够通过它来实现WORA(Write Once Run Anywhere 一次编译,到处运行),尽管这个目标现在已经被大多数人忽略了。因此,JVM可以在不修改Java代码的情况下,在所有的硬件环境上运行Java字节码。
Java虚拟机的特点如下:
基于栈的虚拟机:Intel x86和ARM这两种最常见的计算机体系的机构都是基于寄存器的。不同的是,JVM是基于栈的。
符号引用:除了基本类型以外的数据(类和接口)都是通过符号来引用,而不是通过显式地使用内存地址来引用。
垃圾回收机制:类的实例都是通过用户代码进行创建,并且自动被垃圾回收机制进行回收。
通过对基本类型的清晰定义来保证平台独立性:传统的编程语言,例如C/C++,int类型的大小取决于不同的平台。JVM通过对基本类型的清晰定义来保证它的兼容性以及平台独立性。
网络字节码顺序:Java class文件用网络字节码顺序来进行存储:为了保证和小端的Intel x86架构以及大端的RISC系列的架构保持无关性,JVM使用用于网络传输的网络字节顺序,也就是大端。
虽然是Sun公司开发了Java,但是所有的开发商都可以开发并且提供遵循Java虚拟机规范的JVM。正是由于这个原因,使得Oracle HotSpot和IBM JVM等不同的JVM能够并存。Google的Android系统里的Dalvik VM也是一种JVM,虽然它并不遵循Java虚拟机规范。和基于栈的Java虚拟机不同,Dalvik VM是基于寄存器的架构,因此它的Java字节码也被转化成基于寄存器的指令集。
2、JVM发展历史
1. Java发展史
1996年:SUN JDK 1.0 Classic VM
纯解释运行,使用外挂进行JIT
1997年:JDK1.1 发布
AWT、内部类、JDBC、RMI、反射
1998年:JDK1.2 Solaris Exact VM
JIT 解释器混合
Accurate Memory Management 精确内存管理,数据类型敏感
提升了GC性能
注:JDK1.2开始,称为Java 2,于是有了J2SE J2EE J2ME 的出现,同时加入Swing Collections。
2000年:JDK 1.3,Hotspot 作为默认虚拟机发布
加入JavaSound
2002年:JDK 1.4【Java真正走向成熟的一个版本】,Classic VM退出历史舞台
加入:Assert 正则表达式 NIO IPV6 日志API 加密类库
2004年:JDK 1.5【至关重要的版本】。即 JDK5 、J2SE 5 、Java 5
泛型、注解、枚举、自动装箱、可变长参数、Foreach循环
2006年:JDK 1.6发布,JDK6
脚本语言支持、JDBC 4.0、Java编译器 API
2011年:JDK7发布
G1(全新的GC收集器)
动态语言增强
64位系统中的压缩指针
NIO 2.0
2014年:JDK8发布【重要的版本】
Lambda表达式
语法增强 Java类型注解
2016年:计划发布JDK9
模块化
2. 重大历史事件
使用最为广泛的JVM为HotSpot:HotSpot 为Longview Technologies开发,被SUN收购
2006年:Java开源,并建立OpenJDK,
HotSpot 成为Sun JDK和OpenJDK中所带的虚拟机。
2008年:Oracle收购BEA
得到JRockit VM
2009年:Oracle公司正式宣布以74亿美金的价格收购Sun公司。
得到Hotspot
Oracle宣布在JDK8时整合JRockit和Hotspot,将这两款优秀的虚拟机取长补短,最终合二为一。
在Hotspot基础上,移植JRockit优秀特性
3、JVM种类
1. KVM:
SUN发布
IOS Android兴起之前,广泛用于手机系统
2. CDC/CLDC HotSpot:
J2ME的重要组成部分
手机、电子书、PDA等设备上建立统一的Java编程接口
3. JRockit:
BEA
4. IBM J9 VM:
IBM内部
5. Apache Harmony:
兼容于JDK 1.5和JDK 1.6的Java程序运行平台
与Oracle关系恶劣,退出JCP ,Java社区的分裂
OpenJDK出现后,受到挑战,2011年退役
没有大规模商用经历
对Android的发展有积极作用
4、JVM规范
JVM语言规范主要体现在以下几点:
Class文件类型
运行时数据
帧栈
虚拟机的启动
虚拟机的指令集
此外,JVM需要对Java Library 提供以下支持:(因为这些东西没有办法通过java语言本身来实现)
反射 java.lang.reflect
ClassLoader
初始化class和interface
安全相关 java.security
多线程
弱引用
二、Java字节码(Java bytecode) 和 Class文件
为了保证WORA,JVM使用Java字节码这种介于Java和机器语言之间的中间语言。字节码是部署Java代码的最小单位。
参见:http://www.importnew.com/1486.html
参见:https://blog.jamesdbloom.com/JVMInternals.html#class_file_structure
参见:http://localhost:9500/zoa/collect/#?id=z449aa
三、JVM结构
JVM本身是一个规范,所以可以有多种实现,除了Hotspot外,还有诸如Oracle的JRockit、IBM J9也都是非常有名的JVM。通常我们基于Hotspot虚拟机来讲。
JVM的结构 主要由类加载器、运行时数据区(也叫做内存区)、执行引擎、本地方法接口等4部分组成,有点类似于操作系统的结构。如下图所示:
主要包括4部分:
1. 类加载器:在JVM启动时或者在类运行时将需要的class加载到JVM中。
2. 执行引擎:负责执行class文件中包含的字节码指令;
3. 本地方法接口:主要是调用C或C++实现的本地方法及返回结果。
4. 内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。
生动易懂的说明,参见:http://blog.csdn.net/yfqnihao/article/details/8289363
四、运行时数据区(Runtime Data Areas)
运行时内存区包含如下 5个部分:
The pc Register(程序计数器,Program Counter Register)
JVM Stacks(虚拟机栈,包含很多 “栈帧Frame”)
—— 在JVM Spec v1.0版本中 叫做 “Java Stack”
Heap(堆)
Method Area(方法区)
方法区包括:Run-Time Constant Pool(运行时常量池)
Native Method Stacks(本地方法栈)
由于这部分内容比较多,单独开了一篇,见《JVM运行时数据区》。
五、其他
1、对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。
Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
2、对象的访问定位
建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:
1)句柄
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
即:reference(句柄地址)--->JVM堆 ( 句柄池--->对象实例数据 )
2)直接指针
如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
即:reference(数据指针地址)--->JVM堆 ( 对象实例数据 )
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
对HotSpot而言,它主要使用第二种方式进行对象访问。