JVM系列—概述
1.前言
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构的计算机,是
通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM屏蔽了与具体操作系统平台相关的信息,使Java
程序只需生成在Java虚拟机上一次编译,多次运行,具有跨平台性。JVM在执行字节码时,实际上最终还是把字
节码解释成具体平台上的机器指令执行。
2.JDK、JRE和JVM对比
- JDK(Java Development Kit) 是 Java 语言的软件开发工具包(SDK)。JDK 物理存在,是 programming tools、JRE 和 JVM 的一个集合。
- JRE(Java Runtime Environment)Java 运行时环境,JRE 是物理存在的,主要由Java API 和 JVM 组成,提供了用于执行 java 应用程序最低要求的环境。
- JVM是一种用于计算设备的规范,它是一个虚构的计算机的软件实现,简单的说,JVM是运行字节码(byte code)程序的一个容器。
JVM的特点:
- 基于堆栈的虚拟机:最流行的计算机体系结构,如英特尔 X86 架构和 ARM 架构上运行基于寄存器, JVM 是基于栈结构的。
- 符号引用 :除了基本类型以外的数据 (类和接口) 都是通过符号来引用,而不是通过显式地使用内存地址来引用。
- 垃圾收集 :一个类的实例是由用户程序创建和垃圾回收自动销毁。
- 网络字节顺序 :Java class文件用网络字节码顺序来进行存储。
JVM字节码:
JVM使用Java字节码的方式,作为Java 用户语言 和 机器语言之间的中间语言。实现一个通用的、 机器无关
的执行平台。
3. JVM类加载
3.1 类加载过程
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区
创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对
象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
JVM加载.class文件的方式如下:
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
3.2 类的生命周期
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中,验证、准备和解析3个部分统称为连接。加载、验证、准备、初始化以及卸载这五个阶段的顺序是确定的,因为为了支持Java的动态绑定,解析阶段可能会发生在初始化之后。
3.2.1 动态绑定
一个Java程序的执行要经过编译和执行(解释)这两个步骤,同时Java又是面向对象的编程语言。当子类和父类
存在同一个方法,子类重写了父类的方法,程序在运行时调用方法是调用父类的方法还是子类的重写方法呢?首
先得确定,这种调用何种方法实现或者变量的操作叫做绑定。在Java中存在两种绑定方式,一种为静态绑定,又
称作早期绑定。另一种就是动态绑定,亦称为后期绑定。
静态绑定和动态绑定的区别:
- 发生时间不同:静态绑定发生在编译时期,动态绑定发生在运行时期
- 作用范围不同: private、static和final修饰的变量或者方法,使用静态绑定;而虚方法(可以被子类重写的方法)则会根据运行时的对象进行动态绑定。
- 使用信息不同:静态绑定使用类信息来完成,而动态绑定则需要使用对象信息来完成。
- 重载和重写:重载使用的是静态绑定,重写使用的是动态绑定。
重载示例:
public class Test{
public static void main(String[] args){
String str = new String("123");
Father father = new Father();
father.printMsg(str);
}
static class Father{
public void printMsg(String msg){
System.out.println("String msg");
}
public void printMsg(Object msg){
System.out.println("Object msg");
}
}
}
经过javac编译后,在通过javap -c反编译一下:可以看到20行这里,是确定调用了接受String对象作为参数的printMsg方法,发生了静态绑定。
重写示例:
public class TestB{
public static void main(String[] args){
String str = new String("123");
Father father = new Son();
father.printMsg(str);
}
static class Father{
public void printMsg(String msg){
System.out.println("Father String msg");
}
}
static class Son extends Father{
public void printMsg(String msg){
System.out.println("Son String msg");
}
}
}
运行结果和javap -c反编译之后的结果如下:使用javap不能直接验证动态链接,但是从运行接口可以看出运行的是子类Son中的printMsg方法,同时从20行可以看出仍然是TestB$ Father.printMsg,而不是TestB$Son.printMsg。这就证明在编译时无法确定调用子类还是父类的实现,所以只能丢给运行时的动态绑定来处理。
3.2.2 类加载阶段的具体作用
加载:加载是类加载过程的第一个阶段,在加载阶段,虚拟机主要完成3件事情。
-
通过一个类的全限定名来获取定义此类的二进制流;
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
-
在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)、是可控性 最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完 成加载。加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在 Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
验证:验证是连接的第一个阶段,确保被加载类的正确性。这个阶段保证Class文件的字节流包含的信息是符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式规范,能否被当前版本虚拟机处理。比如:是否以魔数0xCAFEBABE开头,主次版本号是否在当前虚拟机处理范围内,常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外;这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 字节码校验:这个校验是整个验证过程中最复杂的一个阶段,通过数据流和控制流分析,确定程序语义是否合法,符合逻辑。
- 符号引用验证:这个校验动作发生在虚拟机将符号引用转化为直接引用的时候,确保解析动作的正常进行。
准备:为类的静态变量分配内存并设置初始值的阶段。
- 这个阶段分配的内存仅仅包含静态变量,而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在java堆中。
- 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个静态变量:public static int value = 123;
变量value在准备阶段过后的初始值是0,而不是123。这是因为此时尚未执行任何java方法,而把value赋值为123的putstatic指令是在程序被编译后,存放在类构造器< clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
public static final int value = 123;
如上value变量的定义,编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。
解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。那符号引用又是啥呢?
符号引用:java代码在进行javac编译的时候,在Class文件中不会保存各个方法、字段的最终内存布局信息,因为这些字段和方法的符号引用不经过运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用。而符号引用则是包含三类常量:1.类和接口的全限定名;2.字段的名称和描述符;3.方法的名称和描述符。当虚拟机运行时,需要从常量池中获取相应的符号引用,再在类创建时或者运行时进行解析、翻译到具体的内存地址之中。
初始化:初始化阶段是类加载过程中的最后一步,到了初始化阶段才真正开始执行类中定义的java程序代码。其实在前面的准备阶段,变量已经被赋予了一次系统要求的初始值,在初始化阶段会为类的静态变量赋予正确的初始值。在Java中对类变量进行初始值设定有两种方式:
- 声明类变量时指定初始值
- 使用静态代码块为类变量指定初始值
JVM初始化步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类 ;
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类;
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用主要有6种方法:
- 创建类的实例,也就是new Test()这种方式方式;
- 访问某个类或接口的静态变量,或者对该静态变量赋值;
- 调用类的静态方法;
- 反射(如Class.forName(“com.xxx.Test”))
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
结束生命周期:在下面几种情况下,Java虚拟机将结束生命周期
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
4.类加载器
参考
https://juejin.im/post/5b4de8185188251af86be259
https://droidyue.com/blog/2014/12/28/static-biding-and-dynamic-binding-in-java/
http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html
作者:爱肖哥真是太好啦
来源链接:https://blog.csdn.net/qq_36500178/article/details/107165240
版权声明:
1、JavaClub(https://www.javaclub.cn)以学习交流为目的,由作者投稿、网友推荐和小编整理收藏优秀的IT技术及相关内容,包括但不限于文字、图片、音频、视频、软件、程序等,其均来自互联网,本站不享有版权,版权归原作者所有。
2、本站提供的内容仅用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯相关权利人及本网站的合法权利。
3、本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站(javaclubcn@163.com),我们将第一时间核实后及时予以删除。