1.java实现跨平台
java实现跨平台的主要原因在于jdk中的编译器将源代码编译成通用的字节码后,在不同的操作系统上,会有不同的jvm将字节码翻译成对应的机器码。
jvm运行在操作系统之上,没有直接与硬件进行交互。
2. JVM的整体结构
3.JAVA代码的执行流程(JVM的生命周期)
- 将java源代码通过编译器编译成字节码文件。
- 字节码校验并把java程序通过类加载器加载到JVM内存中。
- 针对每个类在JVM内存中创建Class对象。
- 字节码指令和数据初始化到内存中。
- 找到main方法,并创建栈帧。
- 初始化程序计数器中的值为main方法的内存地址。
- 程序计数器不断递增,逐条执行JAVA字节码指令,把指令执行过程的数据存放到操作数栈中(入栈),执行完成后从操作数栈取出后放到局部变量表中,遇到创建对象,则在堆内存中分配一段连续的空间存储对象,栈内存中的局部变量表存放指向堆内存的引用;遇到方法调用则再创建一个栈帧,压到当前栈帧的上面。
4.类的生命周期
加载、验证、准备、解析、初始化、使用、销毁,其中前五个为类的加载过程。
4.1加载
通过该类的完全限定名获取该类的二进制字节流。
将该字节流表示的静态存储转换成元空间中的运行时存储结构。
创建Class对象作为元空间中后续访问该类数据的入口。
4.2验证
确保 Class
文件的字节流中包含的信息符合当前虚拟机的要求。
4.3准备
为类变量分配内存并设置初始值。
4.4解析
将符号引用替换为直接引用。
4.5初始化
Java虚拟机执行类构造器<clinit>方法的过程。完成类变量的赋值。
<clinit>()
是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
5.类加载的时机
5.1主动引用
- 遇到new、putstatic、getstatic、invokestatic字节码指令时必须加载类
- 加载一个类,如果父类还未加载,先加载父类。
- main方法类
- 接口中定义了默认方法,该接口的实现类加载时会先加载该接口类。
- 使用反射包的方法对类进行反射调用。
5.2被动引用
- 通过子类引用父类的静态代码,不会触发类加载。
- 通过数组来定义引用类,不会触发类加载。
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。
6.类加载器的分类
从虚拟机角度可以分为启动类加载器和其他类加载器。
从开发1人员角度可以分为启动类加载器,扩展类加载器,应用程序类加载器。
启动类加载器:该类加载器负责将存放在 JRE_HOME\lib
目录中的虚拟机能识别的类库。(如果悉尼及不能识别,即使在lib目录下也不能被加载)
扩展类加载器:负责将 JRE_HOME/lib/ext中的类库。
应用程序加载器:加载第三方类库或自定义类。
7.双亲委派模型
类加载器之间的层级关系称为双亲委派模型。顶层为启动类加载器,其他加载器都要有父类加载器。它首先会将类加载器请求转发给父类,父类无法处理才会尝试自己解决。
双亲委派模型使Java类它的随着 加载器有了优先级,使得基础类得到统一,避免了冲突。
8.对象的创建过程
8.1类加载
虚拟机遇到一条 new
指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
8.2分配内存
分配内存的方式有两种,一种为指针碰撞,一种为空闲列表。堆内存规整的情况下使用指针碰撞。
8.3初始化零值
虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java
代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
8.4设置对象头
虚拟机对对象在对象头中进行必要的设计。
8.5执行init构造方法
从虚拟机的视角来看,一个新的对象已经产生了,但从Java
程序的视角来看,对象创建才刚开始,<init>
构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new
指令之后会接着执行 <init>
构造方法,把对象按照程序逻辑的意愿进行初始化。