Java虚拟机
关于Java跨平台
-
C/C++语言直接编译成对应平台机器码,如果跨平台,就需要相应的编译器重新编译
-
Java语言跨平台,需要先将Java源程序(.java)编译成与平台无关的字节码文件(.class),然后字节码文件再解释成机器码运行
-
虚拟机将字节码文件解释成对应平台机器码并执行
-
字节码文件只面向虚拟机
-
一次编译,到处运行 —— 编译后的.class文件可以跨平台运行,前提是该平台具有对应的Java虚拟机
-
Java的跨平台原理决定了它的性能没有C/C++高
概述
一.什么是Java虚拟机?
Java虚拟机是一个抽象化的计算机,通过在实际的计算机上通过软件模拟来实现。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
Java虚拟机是Java实现跨平台特性的核心。是Java平台的基础。
二.Java虚拟机的生命周期
-
每个Java程序会单独运行一个Java虚拟机。程序开始执行时,Java虚拟机开始运行(产生一个Java虚拟机实例),程序结束它就停止(该Java虚拟机实例随程序结束而消亡)
通过命令行启动Java虚拟机:java className(类名)
-
Java虚拟机总是开始于一个main()方法
固定格式:public static void main(String args[]){…}
-
main()方法是程序的起点,它被执行的线程初始化是程序的初始线程。程序中所有的线程都由它来启动。
Java种类的线程分为两种:
- 守护线程(daemon):Java虚拟机自己的线程,比如负责垃圾回收的线程;可以将自己的程序设置为守护线程。包含main()方法的初始线程不是守护线程。
- 普通线程
-
只要Java虚拟机中还有普通线程在执行,Java虚拟机就不会停止。如果权限允许,程序可以调用Runtime类或者System类的exit()方法来退出
三.Java虚拟机的体系结构
Java虚拟机的规范定义了内存区域、数据类型、使用指南以及一系列的子系统,这些组件构成了Java虚拟机的内部结构。它们不仅为Java虚拟机的实现提供了清晰的内部结构,更严格规定了Java虚拟机实现的外部行为。
每个Java虚拟机都有一个类加载子系统(class loader subsystem),负责加载程序中的类和接口,并赋予唯一的名字。
每个Java虚拟机都由一个执行引擎(execution engine)负责执行被加载类中包含的指令。
JVM的内部架构:
-
虚拟机栈:
我们也叫它Java栈,就是我们常说的栈内存,一般用来执行Java方法,存储方法参数、局部变量、中间运算结果,并且提供部分其它模块工作需要的数据
Java中的每一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError(栈溢出)异常;如果Java栈可以动态扩展(当前大部分栈都可以动态扩展,虚拟机规范中也允许有固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError(内存出界)异常
-
本地方法栈
主要用来执行本地方法,本地方法不一定是用Java语言编写的方法,JVM会运行在不同的操作系统和硬件上面
-
程序计数器寄存器:
程序计数器是用于存放下一条指令所在单元的地址的地方。
对于Java虚拟机来说,PC寄存器是当前线程执行某条字节码的行数的指示,JVM会根据计数器的值来选取需要执行的操作语句。属于线程私有,不可以共享,每个线程都有自己的PC寄存器。共享会导致计数混乱,无法准确执行当前线程需要执行的语句。
该区域一般不会出现任何OutOfMemoryError(内存出界)异常
-
堆
JVM中内存最大的一块区域,是线程共享的,用来存储对象实例(所有new出来的东西)。
垃圾回收器主要收集的区域
可以通过-Xmx(JVM最大可用内存)和-Xms(JVM初始内存)来调整堆内存。内存无法扩展时,会出现OutOfMemoryError(内存出界)异常
-
方法区
用来存储各种类信息、常量、静态变量、class文件。
垃圾回收器也会对这部分进行回收,比如常量池的清理和类型的卸载。
是线程共享的
四.Java虚拟机中使用的数据类型
-
所有Java虚拟机中使用的数据都有确定的数据类型,Java虚拟机规范严格定义了数据类型和操作。
-
Java中的数据类型分为原始数据类型(四类八种)和引用数据类型(类类型、接口类型、数组类型),在Java虚拟机存在一个Java语言中不能使用的原始数据类型—返回值类型(return value),被用来实现Java程序中的”finally classes“
JVM的类加载机制
类的生命周期
加载、验证、准备、解析、初始化、使用、卸载
-
加载
- 通过类的全限定名来获取此类的二进制字节码
- 将这个字节码所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口
总的来说,就是将源文件经过编译后的到的字节码文件读入内存,并为其创建一个Java.lang.class对象。类的加载由类加载器完成,类加载器通常由JVM提供(被称为系统类加载器),开发者也可以通过继承ClassLoader基类来创建自己的类加载器。
-
连接
-
验证
- 验证输入的字节流是否符合Class文件的存储格式,如果不符合,则抛出一个java.lang.VerifyError异常
- 验证输入的字节流(字节码)是否符合Class文件格式的规范,是否可以被当前版本的虚拟机处理。验证完后,字节流进入方法区进行存储。
- 对类的元数据信息进行语义校验,验证是否存在不符合Java语言规范的元数据信息
- 进行字节码验证,分析数据流和控制流,对类的方法体进行校验分析,保证被校验的类的方法在运行时不会危害虚拟机
- 进行符号引用验证,对常量池中的各种符号引用信息进行匹配性校验。
总结就是检验被加载的类是否有正确的内部结构,并和其他类协调一致。
-
准备
为类变量分配内存(在方法区中进行分配)并设置类变量初始值。
对类似 public static final int value = 85;的有常量值属性的类字段,会在准备阶段对它的变量和值在方法区中进行匹配。
-
解析
将常量池中的符号引用替换为直接引用
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了
-
-
初始化
主要对类变量进行初始化
如果该类的直接父类还没有被初始化,先初始化其父类
类中的初始化语句依次执行
注意
-
系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类
-
当调用java(java className)命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,同一个JVM的所有线程以及变量都处于同一个进程里,它们都使用该JVM进程的内存区。
当系统出现以下几种情况时,JVM进程被终止:
- 程序正常结束
- 程序运行到使用System.exit()或Runtime.getRuntime().exit()代码处
- 程序执行过程中遇到未捕获的异常或错误
- JVM 所在平台强制结束了JVM进程
-
JVM会连续完成类的加载、连接和初始化。所以将这三个统称为类的加载或类的初始化
类初始化的时机:
- 创建类的实例(new、反射、反序列化)
- 调用某个类的类方法
- 访问某个类或接口的类变量
- 使用反射方式强制创建某个类或者接口对应的Java.lang.class对象
- 初始化某个类的子类
- 直接使用java.exe命令来运行某个主类