JVM之体系结构和类加载子系统
一,JVM的整体架构
HotSpot VM是目前市面上高性能虚拟机的代表作之一。采用解释器与即时编译器并存的架构
JVM的架构图:
java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构
两者之间的区别:
基于栈式架构的特点:
- 设计和实现更加简单,适用于资源受限的系统
- 避开了寄存器的分配难题,使用零地址指令方式分配
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈,指令集更小,编译器容易实现
- 不需要硬件支持,可移植性更好,更好实现跨平台
基于寄存器架构的特点:
- 典型的应用是X86的二进制指令集,如传统的pc以及Android的Davlik虚拟机
- 指令集架构则完全依赖硬件,可移植性差
- 性能优秀和执行效率更高
- 花费更少的指令去完成一项操作
- 在大部分情况下,基于寄存器架构的指令集往往都是以一地址指令,二地址指令和三地址指令为主,而基于栈式架构指令集却是以零地址指令主
总结:java的指令根据栈来设计,是由于其跨平台性的设计,不同平台cpu架构不同,所以设计不能基于寄存器。优点:跨平台性,指令集小,指令多。缺点:性能下降,实现同样的功能需要更多的指令
JVM的生命周期
虚拟机的启动:
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类由虚拟机的具体实现指定的
虚拟机的执行:
程序开始执行时他才运行,程序结束时他就停止
执行一个所谓的Java程序的时候,真正在执行的是一个叫做Java虚拟机的进程
虚拟机的退出:
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机的进程终止
某线程调用Runtime类或System类的exit方法或halt方法
二,类加载子系统
类加载子系统作用:
1.类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识(在二进制字节码文件中,每个class文件的开头都有:CA FE BA BE这四个字节)
2.ClassLoader只负责class文件的加载,至于它是否可以运行,则有ExecutionEngine决定
3.加载的类信息存放于一块称为方法区的内存空间,除了类信息外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器ClassLoader角色:
classfile存在于本地硬盘上,相当于一个模板,这个模板在执行的时候加载到JVM中,根据这个文件实例化出n个一模一样的实例
class file 加载到JVM中,被称为DNA元数据模板,放在方法区
类加载器 ClassLoader相当于一个快递员,进行.class文件–>JVM–>最终成为元数据模板
一,类加载的过程:
一,加载:
-
通过一个类的全限定名获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
-
在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载.class文件的方式:
- 从本地系统中直接加载
- 通过网络获取,例:Web Applet
- 从zip压缩包中读取,成为日后jar,war格式的基础
- 运行时计算生成。例:动态代理技术
- 由其他文件生成,例:JSP应用
- 从专有数据库中提取
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
二,链接:
验证(Verify):
目的:确保Class文件的字节流包含信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全
四种验证方式:文件格式验证,元数据验证,字节码验证,符号引用验证
准备(Prepare):
为类变量分配内存并且设置该类变量的默认初始值,即零值
不会给用final修饰的static初始化,因为final修饰的是常量,在编译的时候就已经分配了内存, 在准备阶段会显示初始化
不会为实例变量分配初始化,因为类变量会分配在方法区中,而实例变量会随着对象一起分配到java堆中
解析(Resolve):
将常量池内的符号引用转换为直接引用的过程
解析操作往往会伴随着JVM在执行完初始化之后再执行
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中
解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等
三,初始化
- 初始化阶段就是执行**类构造器方法< clinit>()**的过程
- < clinit>()不同于类的构造器(没有静态的变量,就不会出现clinit方法),构造器是虚拟机视角下的< init>(),任何一个类,只要声明后就至少存在一个类的构造器 init方法
- 此方法不需定义,是javac编译器自动收集类中的所有变量的赋值动作和静态代码块中的语句合并而来.
- 构造方法中的指令按语句在源文件中出现的顺序执行
- 若该类具有父类,JVM会保证子类的< clinit>()执行前,父类的已经执行完毕(会层层向上调用,然后从上再向下执行)
- 虚拟机必须保证一个类的< clinit>()方法在多线程下被同步加锁
二,类加载器
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader,不是由Java语言所写)和自定义加载器(所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器)。
主要常见的加载器有三种:引导类加载器/启动类(Bootstrap Class Loader) 扩展类加载器( Extension Class Loader) 系统类加载器/应用类(SystemClass Loader)
ClassLoader的扩展类图:
获取类加载器的编码:
这三种类加载器不是子父类之间的继承关系,类似于多级文件目录的结构
三种加载器详解:
一,启动类加载器
- 这个类加载使用C/C++语言实现,嵌套在JVM内部
- 用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar resource.jar sun.boot.class.path 路径下的内容)用于提供JVM自身需要的类
- 不继承自java.lang.ClassLoader,没有父加载器
- 出于安全考虑,只加载包名为java,javax,sun等开头的类
二,扩展类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库,如果用户创建的jar放在此目录下,也会自动扩展类加载器加载
三,应用类加载器
负责加载环境变量classpath 或系统属性java.class.path指定路径下的类库
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成
通过ClassLoader getSystemClassLoader()方法可以获取到该加载器
获取ClassLoader的途径:
编码:
三,双亲委托机制
根据上面提到的三种类加载器加载的包,进行选择是否该自己加载
优势:
避免类的重复加载
保护程序安全,防止核心API被随意篡改
--例:如果自定义一个java.lang.String这个类,然后引用String,如果不是双亲委托机制,就会直接调用自定义的这个String类,如果这个类中被加入了恶意代码,会对系统造成非常大的危险
注意:在JVM中表示两个class对象是否为同一个类存在两个必要条件
1.类的完整类名必须一致,包括包名
2.加载这个类的ClassLoader(ClassLoader实例对象)必须相同
对类加载器的引用
Jvm必须知道一个类型是由启动类加载器加载还是由用户类加载器加载的。如果一个类是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
类的主动使用和被动使用
主动使用:
-
创建类的实例
-
访问某个类或接口的静态变量
-
调用类的静态方法
-
反射
-
初始化一个类的子类
-
Java虚拟机启动时被标明为启动类的类
-
JDK7 开始提供的动态语言支持:
- java.lang.invoke.MethodHandle实例的解析结果
除了以上七种情况,其他使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化
JVM运行数据区之堆空间