Java开发运行平台
JavaSE组成概念图
通过上图,可以看出,Java SE可以分成3个主要的部分
JVM(Java Virtual Machine, Java 虚拟机)
JVM可以理解为一个虚拟的机器,具备计算机基本运算方式。它主要负责将Java程序生成的和平台无关的字节码文件解释成能在具体平台上的机器指令。
JRE(Java Runtime Environment, Java 运行时环境)
JRE = JVM + 解释器 + Java核心类库
如果想要运行一个开发好的Java程序,只需在计算机中安装JRE即可。
JDK(Java Development Kit, Java 开发工具包)
JDK = JRE + 开发工具(编译器、调试器、其他工具…)+ 开发类库
JDK是提供给Java开发人员使用的,其中包含了java的开发工具。也包含了JRE。所以安装了JDK,就不用在单独安装JRE了。
简单的说就是JDK包含JRE,JRE包含JVM
Java程序执行过程
上图描述了Java程序执行的大致步骤。
编写source code,并将其存储在硬盘当中。
在命令行中使用javac命令启动Java Compiler对source code进行编译。并生成目标文件(即.class file)。
在命令行中使用java命令,启动JRE。JRE Class Loader会自动从硬盘中读取用户的.class File以及Java API中的.class File(有时候还包括用户导入的工具类jar包,其实也是一种字节码文件),并将他们全部载入系统分配的内存区域——Runtime Date Areas(运行数据区)。
执行引擎启动,完成对.class文件的解释或者编译,转化成特定平台的机器码。CPU执行机器码,完成底层调用等一系列工作。
类加载器(Class Loader)
层级结构
类加载器被组织成一种层次结构关系,也就是父子关系(本文中将其历史性的命名为汪狗宇-林禹模式)。
其中,Bootstrap是所有类加载器的父类。如下图所示:
- Bootstrap Class Loader:启动类加载器,当运行JVM时,这个类被创建,它加载一些基本的java API。
- Extension Class Loader:扩展类加载器,这个加载器加载除了基本API外的一些扩展类。
- System Class Loader(Application Class Loader):应用程序类加载器,它加载应用程序中的类,也就是在CLASSPATH中配置的类。(然鹅CLASSPATH早在5.0后就不必配置了,有缺省值)。该加载器为程序的默认加载器。
- User-Defined Class Loader(Custom Class Loader):用户自定义加载器,这是开发人员通过扩展ClassLoader类定义的自定义加载器,用于加载自定义的一些类。
委派模式(Delegation Mode)
当JVM加载一个类的时候,下层的加载器将任务委派给上一层类的加载器,上一层的加载器检查它的命名空间中是否已经加载这个类。如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。
检查完后,按照相反顺序进行加载,如果Bootstrap加载器找不到这个类,则往下委托,直到找到该类文件。
对于某个特定类加载器来说,一个Java类只能被载入一次,也就是说在JVM中,类的完整标识是(classloader,package,className)。一个类可以被不同的加载器加载。
在这里以我们学习的第一个Java程序HelloWorld.java为例来简单解释一下类加载器的工作机制。
现在我们有一个用户自己定义的类HelloWorld需要加载,如果不特殊指定的话,一般交由System去加载。接到任务后,System检查自己的库中是否已有这个类,如果没有(当然没有hahaha),则委托给上层的Extension进行检查。Extension发现自己的库中没有这个类,于是继续向上进行委托。最顶层的Bootstrap发现自己库中也没有,于是根据自己的路径(Java核心类库,如java.lang)尝试去加载,如果仍未找到,那么就交回给下层的Extension,Extension也根据自己的路径(Java_HOME/jre/lib/ext)去尝试加载这个类,若仍未找到,那么就继续交回下层去尝试加载。而System将会在CLASSPATH路径(缺省值为”/.”,即当前目录)去尝试加载,如果找到该类,那么就加载到JVM中。
每个类加载器都优先尝试用父类加载器加载,若父类加载器不能加载则自己尝试加载;
若加载成功则返回Class对象给子类,若失败抛出异常,让子类自己尝试进行加载。
类加载器工作机制
当类加载完毕后,JVM继续按照下图完成其它工作。
- Loading:加载。将文件系统中的Class文件加载到JVM运行数据区。
- Linking:链接。又可分为三个子步骤。
- Initializing:初始化。初始化类的局部变量,为静态域赋值,同时执行静态代码块。
类加载器限制
下层的加载器能够看到上层加载器中的类,反之则不行,也就是说委托只能自下而上。
这提升了软件的安全性。因为在父类委派模式下,用户自定义的加载器不可能加载本应由父类加载器加载的可靠类,从而有效的防止了不可靠甚至恶意的代码代替父类加载器加载可靠代码。
类加载器可以加载一个类,但它不能卸载一个类。(但是类加载器可以被删除或者被创建。)
之所以不由类加载器去卸载一个类,是因为每个类都有自己的生命周期,类加载器只负责类的加载,而一个类何时结束,取决于代表它的Class对象生命周期何时结束(对于用户创建的类,当代表类的Class对象不再被引用时,就会由垃圾回收器自动回收;而对于系统自带的加载器加载的类,也即Java核心类,在JVM的生命周期中,始终不会被卸载。因为JVM本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象)。
运行数据区
Runtime Data Areas:当运行一个JVM实例时候,系统将给它分配一块内存区域。这一内存区域由JVM自己来管理。JVM从这一块内存中分出一块,用来存储一些运行数据,例如:创建的对象,传递给方法的参数,局部变量,返回值等等。
- Java Stack:Java栈
- PC Register:程序计数寄存器
- Native Method Stack:本地方法栈
- Java Heap:Java堆
- Method Area:方法区
- Runtime Constant Pool:运行常量池。本属于方法区,但由于其重要性,JVM规范将其独立出来进行说明
其中,前面三个区域是每个线程独自拥有的,后三者则是整个JVM中所有线程所共同拥有的。
这6个区域如下图所示。
PC Register
每一个线程都拥有一个程序计数器,当线程启动时,程序计数器被创建,这个计数器存放当前正在被执行的字节码指令的地址。
Stack
同样的,Java栈也是每个线程单独拥有的,线程启动时创建。这个栈中存放着一系列栈帧(Stack Frame),JVM只能进行压栈和弹栈这两种操作。每当调用一个方法时,JVM就往栈中压入一个栈帧,方法结束返回时弹出栈帧。如果方法执行时出现异常,就可以使用printStackTrace等方法来查看栈的情况。
从示意图中很容易看出,每个栈帧包含三个部分:本地变量数组、操作数栈、方法所属类的常量池引用。在这里简单说明一下这三个部分。
- Local Variable Array:本地(局部)变量数组中,从0开始按顺序存放方法所属对象的引用、传递给方法参数、局部变量等。
- Operand Stack:操作数栈中存放方法执行时的一些中间变量,JVM在执行方法时压入或弹出这些变量。操作数栈是方法真正工作的地方。
- Reference to Constant Pool:除了局部变量数组和操作数栈之外,栈帧还需要一个常量池的引用。当JVM执行到需要常量池的数据时,就是通过这个引用来访问常量池的。
Native Method Stack
当程序通过JNI(Java Native Interface)调用本地方法(如C\C++代码)时,就根据本地方法的语言类型建立相应的栈。
Heap
堆中存放的是程序创建的对象和实例。这个区域对JVM的性能影响很大。垃圾回收机制处理的正是这一块内存区域。
Method Area
方法区是一个JVM实例中所有的线程共享的,当启动一个JVM实例时,方法区域被创建。它用于存放常量池、有关域和方法的信息、静态变量、类和方法的字节码。(字节码文件就保存在这一区域)
Runtime Constant Pool
运行常量池用于存放类和接口的常量,除此以外,它还存放方法和域的所有引用。当一个方法或者域被引用的时候。JVM就通过运行常量池中的这些引用来查找方法和域在内存中的实际地址。
执行引擎(Execution Engine)
类加载器将字节码载入内存之后,执行引擎以Java字节码指令为单元,读取Java字节码。然鹅此时的字节码是机器无法识别的,因此需要将字节码文件转化成平台相关的机器码。
这个过程可以由解释器来执行,也可以由即时编译器(JIT Compiler)来完成。
JVM执行引擎结构
这里通过一个框图来简单的说明JVM执行引擎的内部结构。
即时编译器与解释器
在学校学习的过程中,我们常把Java理解成一种先编译成字节码后解释执行的语言。在Java 1.0时代,这么理解或许没有问题。但当JIT出现后,这种局面出现了改观。
- 当程序需要迅速启动和执行的时候,解释器首先发挥作用,省去编译的时间,立即执行。然而随着时间的推移,即使编译器发挥作用,将越来越多的代码编译成本地代码,以获得更好的执行效率。
- 当机器内存限制比较大时,可以利用解释的方式节省内存,反之可以用编译提升效率。
- 解释器还可以作为编译器的“逃生门”。例如程序中加载了新类后,类型结构发生变化,可以采用逆优化,退回解释状态继续执行。
解释器一条一条的读取字节码文件,解释并且执行字节码指令。正因为它一条一条的解释和执行指令。所以它可以很快的解释字节码,但执行起来会比较慢。总的来说,字节码这种“语言”基本是解释执行的。
- 程序启动时首先发挥作用,解释执行Class字节码;
- 省去编译时间,加快启动速度;
- 执行效率低。
JIT(Just In Time)编译器
即使编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。
然后,执行引擎就没有必要再去解释执行该方法了,它可以直接通过本地代码去执行这段代码。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
- 程序解释运行后,JIT编译器逐渐发挥作用;
- 编译成本地代码,提高执行效率;
- 占用程序运行时间、内存等资源。
不过,用JIT编译器来编译代码所花的时间要比用解释器去一条条解释执行花的时间要多。因此,如果代码只被执行一次的话,那么最好还是解释执行而不是编译后再执行。
内置了JIT编译器的JVM都会检查方法的执行频率,如果一个方法执行频率超过了一个特定的值的话,JVM就认为这段代码是“热点代码”。那么这段代码就会被编译成本地代码,以提升程序执行效率。
JDK目录结构
安装包
本文以恩师wxx老师传给我的jdk-7u71-windows-x64环境为例来简单说明一下JDK的目录结构。
首先来说明下jdk-7u71-windows-x64这个jdk安装包命名结构。
jdk——说明这个安装包是用于安装Java开发工具包的
7u71——这代表该安装包对应的版本。u前是大版本号,即JDK7;u后则注明的是小版本号。每个版本的新特性可以登录Oracle官网查看。
windows——说明了该JDK所运行的平台
x64——说明了该安装包所需CPU的数据宽度,宽度不符可能出现问题。需要说明的是,有的安装包上标注的是x86,实际上也就是x64。可以安全安装。
目录结构
当我们完成JDK安装后,进入JDK目录,就会出现这样的界面。下面简单说明一下该目录的内容
- bin:存放java启动命令以及其他开发工具命令。该目录应该保存到系统全局变量PATH中
- db:用于支持JDK自带的一个小型数据库(没谁使),在JDK8.0以后版本中删除该目录
- include:包含C语言头文件,支持用Java本地接口和Java虚拟机接口来实现本机代码编程。比如JNI(JVM Native Interface),JVMTI(JVM Tool Interface)。
- jre:JDK自含的JRE,包含JVM,运行时的jar包和Java应用启动器。但不包含开发环境中的开发工具
- jre/bin :包含JVM执行所需的执行文件和dll动态链接库
- jre/bin/client:包含用Client模式的VM时需要的dll库
- jre/bin/server:包含用Server模式的VM时需要的dll库
- jre/lib:包含核心代码库(即Java API)
- jre/lib/ext:是Extension Class Loader装入jar类库的目录
- jre/lib/security:包含JVM安全需要的设置文件,JVM的信任的整数也在这里
- jre/lib/applets:用于applet需要的jar类库。可以通过本地加载减小大型Applet应用的启动时间
- jre/lib/fonts:字体文件
- lib:是JDK开发工具所用到的类库及其他文件(主要是.jar文件)
- COPYRIGHT:版权证书
- LICENSE:许可证书
- README:信息说明
- release:发布版本
- src.zip:Java所有核心类库的源代码(开源大法好)
- THIRDPARTYLICENSEREADME:第三方许可证信息
- THIRDPARTYLICENSEREADME-JAVAFX:JavaFX的第三方许可证信息