JVM
JVM运行机制
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
JVM组成
Class loader(类装载)
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最
终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载
(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化
(Initialization)、 使用(Using)和 卸载(Unloading) 七个阶段。
《Java虚拟机规范》中并没有进行
强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》
则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之
前开始)
- 使用new关键字实例化对象的时候
- 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
的时候。 - 调用类的静态方法的时候
- 对类进行反射调用的时候
- 类初始化如果父类没有初始化,先初始化父类
- 执行启动类(包含main)的时候,先初始化启动类。
- 如果一个包含默认方法的接口被实例化的类初始化时,先初始化这个接口(JDK8)
类加载的过程
加载
加载(Loading)过程需要完成以下3个步骤
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入
口。
二进制字节流不一定是从Class文件中获取。可以通过使用不同类加载器从不同的源加载二进制字节流。
- 从ZIP压缩包中读取
- 从网络中获取
- 由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件
- 运行时计算生成(动态代理等)
…
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚
拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
验证主要分为下面4个阶段:
- 文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
例如:是否以魔数0xCAFEBABE开头、主次版本号是否在当前Java虚拟机接受范围之内、常量池的常量中是否有不被支持的常量类型… - 元数据验证
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要
求。
例如:这个类的父类是否继承了不允许被继承的类(被final修饰的类)、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法… - 字节码验证
最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现 - 符号引用验证
主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题 - 符号引用验证
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初
始值的阶段
解析
将二进制数据中的符号引用替换成直接引用。
- 符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
- 直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
初始化
类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)
类加载器
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节
流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动
作的代码被称为“类加载器”(Class Loader)。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。
主要存在的类加载器:
- 启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中。 - 扩展(Extension)类加载器
它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null - 系统(System)类加载器
它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径
双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载
器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用
组合(Composition)关系来复用父加载器的代码.
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加
载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的
加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请
求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载.
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类
加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一
个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类
在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个
类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的
ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应
用程序将会变得一片混乱