1.前言
由于后期学习需要用到大量的JVM底层的东西,所有本人调整了一下学习计划,打算先从JVM入手,了解整个JAVA的运行机制,内存模型,编译原理等等一些底层的东西,这样在学习 后面的东西,会有一种豁然开朗的感觉。后期的内容有从网上直接复制粘贴的内容,但是大部分的内容都是经过自己整理后的,我觉得参照别人写的东西,未尝不可。如果是转载的文章,最后我列出转载的地址。虽然我做不了技术的创造者,但是争取做一个好的传播者。
2.什么是Java
经过了多年的发展,Java早已由一门单纯的计算机编程语言,演变为了一套强大的技术体系。是的,什么是Java,我想技术体系四个字应该是最好的概括了吧。Java设计者们将Java划分为3种结构独立但却彼此依赖的技术体系分支,它们分别对应着不同的规范集合和组件:
1、Java SE(标准版),主要活跃在桌面领域,主要包含了Java API组件。
2、Java EE(企业版),活跃在企业级领域,除了包含Java API组件外,还扩充有Web组件、事务组件、分布式组件、EJB组件、消息组件等,综合这些技术,开发人员完全可以构建出一个具备高性能、结构严谨的企业级应用,并且Java EE也是用于构建SOA(面向服务架构)的首选平台。
3、Java ME(精简版),活跃在嵌入式领域,称之为精简版的原因是,它仅保留了Java API中的部分组件,以及适应设备的一些特有组件。
上面讲到Java技术体系的分支,那既然Java是一种技术体系,我们来看一下组成这种技术体系的技术:
1、Java编程语言
2、字节码
3、Java API,包括Java API类库和来自商业机构以及开源社区的第三方类库
4、Java虚拟机
很多时候我们只关注了第一点,因为第一点才是和工作切实相关的。Java技术体系所包含的内容实际上Java官方有提供给我们一张图.
3.Java的优点
Java能获得如此广泛的认可,除了它拥有一门结构严谨、面向对象的编程语言之外,还有许多不可忽视的优点:
1、它摆脱了硬件平台的束缚,实现了“一次编写、到处运行”
2、它提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄露和指针越界问题
3、它实现了热点代码检测和运行时编译及优化,这使得Java应用能随着运行时间的增加而获得更高的性能
4、它有一套完整的应用程序接口,还有无数来自商业机构和开源社区的第三方类库来帮助它实现各种各样的功能
5、它与身俱来对分布式技术的支持就比较完善
但是,Java最大的优势和财富还不是以上这些,就像高翔龙老师在《Java虚拟机精讲》中写的,Java真正强大的地方是因为拥有全世界最多的技术拥护者和开源社区支持,他们无时无刻都保持着最充沛的体力与思维,一步一步地驱动着Java技术的走向。
4.JDK和JRE
两个常见的重要概念。其实上面的图中已经划分出了JDK和JRE的范围了。我们对这张图做一个归纳,用我们的语言简单地总结一下什么是JDK和JRE:
1、JRE(Java Runtime Enviroment),是支持Java程序运行的标准环境,包含:java虚拟机、JAVA SE API、运行java应用程序所必须的文件等。
2、JDK(Java Development Kit),是用于支持Java程序开发的最小环境,包含:JRE的部分,以及编译器和调试器等。
总结:
如果只是要运行JAVA程序,只需要JRE就可以。 JRE通常非常小,也包含了JVM。
如果要开发JAVA程序,就需要安装JDK。
OpenJDK
前面有讲过,“Java真正强大的地方是因为拥有全世界最多的技术拥护者和开源社区支持,他们无时无刻都保持着最充沛的体力与思维,一步一步地驱动着Java技术的走向”。其实JDK在一开始并不是开源的,但是随着开源运动的蓬勃发展,2006年Sun公司宣布将对Java开放源代码,开源的Java平台开发主要集中在OpenJDK项目上。2009年4月15日,Sun公司正式发布OpenJDK,JDK 7则是Java开源后发布的第一个版本,任何组织和个人都可以为Java的发展做出贡献。当然OpenJDK和真正的Oracle JDK(因为Sun公司被Oracle公司在2010年收购了嘛,所以就叫做Oracle JDK了)还是有区别的:
OpenJDK中的代码基本上都来自于Oracle JDK,属于Oracle JDK的一个分支,但是其中去除了一些非开源的组件和代码,替换成了开源的组件和代码,主要是加密和图形的部分。因此用OpenJDK代替Oracle JDK可能会有一些的不兼容。
对于OpenJDK感兴趣的,可以在OpenJDK官网http://download.java.net/openjdk/jdk7/下载OpenJDK的源代码。像Java虚拟机HotSpot、Java编译器Javac、JNI等等,源代码都在里面。
5.Java虚拟机(JVM)
5.1 概述
JVM是JRE的一部分,实际上它是一个虚构出来的小型计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JAVA语言最大的特点就是跨平台运行,即所谓的“一次编写,处处运行”,它屏蔽了与具体操作系统平台相关的信息,类似一个程序代码和操作系统之间的一个中间件。
Java程序的跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为:libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。
5.2 JVM的主要功能
下面我们先看一张图,来了解JVM的主要功能和运行流程,如果看不懂没关系,后期的文章会系列的讨论。
三项主要功能:
- 加载代码:通过类加载器(class loader)加载类文件的过程,将class文件动态的加载到内存中。
- 运行时数据区:JVM加载class文件和运行class文件的过程中,分配的空间,具体指上图的jvm memory区域。
- 执行代码:负责执行class文件中包含的字节码指令。
5.3 加载
5.3.1 类加载的过程
下面简单介绍一下类加载的过程,类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示:
[图片上传失败...(image-1617ab-1530860129287)]
其中**类加载的过程包括了加载、验证、准备、解析、初始化五个阶段**。其中验证,准备,解析又可以合在一起。
所以也可以分为三个大的步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤,如下图所示:
[图片上传失败...(image-992da7-1530860129287)]
1) 装载:查找并加载类的二进制数据;
2) 链接:
验证:确保被加载类的正确性,就是确保.class字节码符合虚拟机的要求;
准备:为类的静态变量分配内存,并将其初始化为默认值,这些变量所使用的内存都将在方法区中分配;
解析:虚拟机将常量池内的符号引用替换为直接引用的过程;
3)初始化:初始化过程是一个执行类构造器<clinit>()方法的过程,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。其实就是执行类构造器方法的内容,包括给static变量赋予用户指定的值以及执行静态代码块;
注意:
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
这里简要说明下Java中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定:
- 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对java,简单的可以理解为程序编译期的绑定。java当中的方法只有final,static,private和构造方法是前期绑定的。
- 动态绑定:即后期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在java中,几乎所有的方法都是后期绑定的。
5.3.2 类加载器
(1): 类加载器的结构
说到加载,不得不提到类加载器,下面就具体讲述下类加载器。下面看一张图了解一下类加载器的层次结构。
下面是类加载器的执行的各自不同的目录下的 .jar 包:
看上图我们知道类加载器有以下几类:
- 启动类加载器:Bootstrap ClassLoader,在JVM启动的时候加载,使用native code实现(Hotspot虚拟机(C++)),是虚拟机自身的一部分。它负责加载java的核心库,即存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。此类加载器并不继承java.lang.ClassLoader,不能被java程序直接调用。
- 扩展类加载器:Extension ClassLoader,该类由sun.misc.Launcher$ExtClassLoader实现,它负责用于加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量指定所指定的路径中所有类库,就是除了基本的Java API以外的扩展类,比如 security的安全扩展功能,开发者可以直接使用扩展类加载器。
- 应用程序类加载器:Application ClassLoader,一般也被称为系统类加载器(System class loader),该类加载器由sun.misc.LauncherAppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类(即bin目录下的.class文件),开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 用户自定义类加载器(User-defined class loader):这是应用程序开发者用直接用代码实现的类装载器。也可以用来加载应用类,使用自定义的类加载器有很多特殊的原因:运行时重新加载类或者把加载的类分隔为不同的组,典型的用法比如 web 服务器 Tomcat。
(2):类加载器的双亲委托模型
上面层次关系称为类加载器的**双亲委派模型**。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是**使用组合关系**来复用父加载器中的代码。该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。
**双亲委派模型的工作流程是**:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,依次向上,层层递进,最终所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
优点:
使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。
(3): 类加载器的特点
Java提供了动态的加载特性;它会在运行时的第一次引用到一个class的时候对它进行加载和链接,而不是在编译期进行。JVM的类装载器负责动态加载。
Java类装载器有如下几个特点:
- 层级结构:Java里的类装载器被组织成了有父子关系的层级结构。Bootstrap类装载器是所有装载器的父亲。
- 代理模式:基于层级结构,类的装载可以在装载器之间进行代理。当装载器装载一个类时,首先会检查它是否在父装载器中进行装载了。如果上层的装载器已经装载了这个类,这个类会被直接使用。反之,类装载器会请求装载这个类。
- 可见性限制:一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。
- 不允许卸载:类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器。
5.4 运行时数据区
运行时数据区是在JVM运行的时候操作系统所分配的内存区。它可以划分为几个区域:方法区,堆,java栈,pc寄存器,本地方法栈等。
5.5 执行引擎(****Execution Engine****)
5.5.1 执行引擎的定义
通过类装载器装载的,被分配到JVM的运行时数据区的字节码会被执行引擎执行。执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。在执行引擎执行时,有一个任务是必须把字节码转换成可以直接被JVM执行的语言,也就是机器码。
5.5.2 字节码的执行技术
主要的执行技术有:解释,即时编译,AOT编译等几种。
其中编译和解释执行的技术有时候是混合在一起的,如果单独从编译技术的角度来看,又分为:前端编译、即时编译(JIT编译)、静态提前编译(AOT编译)三种。
- 解释器:在解释执行前,java编译器已经将java源码文件(.java)编译成class二进制字节码文件。解释器一条一条的读取二进制字节码文件,解释并且执行字节码指令。缺点是执行速度比较慢.
- 即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后通过在运行时通过收集监控信息,检查方法的执行频率,如果一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码,放到缓存里,下次执行引擎就没有必要再去解释执行这个方法了,直接通过本地代码去执行它,因为本地代码是保存在缓存里的。这种方式也被称为“热点”编译。它之所以被称作”热点“是因为热点编译器通过分析找到最需要编译的“热点”代码,然后把热点代码编译成本地代码。如果已经被编译成本地代码的字节码不再被频繁调用了,换句话说,这个方法不再是热点了,那么Hotspot VM会把编译过的本地代码从cache里移除,并且重新按照解释的方式来执行它。优点是执行效率大大增加,缺点就是编译代码所花的时间要比用解释器去一条条解释执行花的时间要多。
- AOT(Ahead-Of-Time)编译器。它使得多个JVM可以通过共享缓存来共享编译过的本地代码,程序运行前,直接把Java源码文件(.java)编译成本地机器码的过程;
目前主要还是采用解释器+JIT编译这种混合的方式,如JDK中的HotSpot虚拟机。 AOT的编译器在IBM JDK1.6的时候被引入进去,不过主流的jdk用的还是比较少。另外JIT编译速度及编译结果的优劣,是衡量一个JVM性能的很重要的指标,****所以对程序运行性能优化集中到这个阶段;****也就是说可以对这个阶段进行JVM调优;
HotSpot JVM 内置了两个不同的即时编译器,分别称为Client Compiler(C1)和Server Compiler(C2),HotSpot默认采用解释器和其中一个编译器直接配合的方式工作,使用哪个编译器取决于虚拟机运行的模式,HotSpot会根据自身版本和宿主机器硬件性能自动选择模式C1还是C2,用户也可以使用“-client”或”-server”参数去指定。
6.后记
研究jvm,并不是需要我们能写一个jvm,而是要求我们最起码对代码的执行有一个比较清晰的认识,当以后遇到程序比较复杂的场景,可以根据我们的业务需求定制自己的虚拟机,对虚拟机进行调优。
目前我们现在说的Java虚拟机基本上都是JDK自带的虚拟机HotSpot,这款虚拟机也是目前商用虚拟中市场份额最大的一款虚拟机,可以通过在命令行程序中输入“java -version”来查看:
那其实市面上还有很多别的优秀的虚拟机。Sun公司除了有大名鼎鼎的HotSpot外,还有KVM、Squawk VM、Maxine VM,BEA公司有JRockit VM、IBM公司有J9 VM等等。