JVM虚拟机详解

JVM的平台无关性与性能

问题:Compile once,Run anywhere如何实现

Java源码首先被编译成字节码,再由不同平台的JVM进行解析,JAVA语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。

b03

为什么JVM不直接将源码进行编译成机器码去执行

(1)准备工作太过繁琐

JVM每次进行编译的时候都会对源代码进行各种检查,纠错

(2)兼容性

JVM不仅仅可以给java语言编译成的class文件进行解释,还可以对任何语言,只要是解释为.class字节码都可以解释

java与C++的区别

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java不提供指针来直接访问内存,程序内存更加安全,C++程序中存在指针的运算,可能会导致,指针指向不存在的地址值,而导致错误。
  • Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。
  • Java有自动内存管理机制,不需要程序员手动释放无用内存
  • java编译出字节码文件,在虚拟机上运行;C编译出二进制码文件,直接在操作系统上运行。

java运行机制对性能的影响

java确实是编译型语言,但也有解释部分,通过JVM保证数据的一致性,也确保Java的平台无关性;java中有JIT编译器,将我们编写好的源码编译成字节码,然后再装载到JRE,JRE再对字节码进行解析。

b04

以下我们来比较以下C代码与java代码的执行效率

#include<stdio.h>

int main() {
    printf("Hello World!");
}

b04a

再来看看Java代码

  1.    public class Main {
         public static void main(String[] args) {
                  System.out.prinln("Hello World!");
         }
       }
    

    b04b

python的性能比java更慢

python特性, 强类型、动态类型检查的语言。所谓动态类型,是指在定义变量时,我们无需指定变量的类型,Python 解释器会在运行时自动检查。 (2017年发布的python 3.5以上加入类型检查机制,通过特定注解完成,但是还是跟原生强类型检查的java、C没法比,最新版phthon 是 10月发布的3.7.5和8月发布的3.6.9)

没有;作为语句结尾标识,用换行符作为语句结束标志,很长的语句需要用\来把下一行作为本行的续行

total = item_one + \
        item_two + \
        item_three

没有{}作为语句块的标识,全凭严格的缩进来组织。

flag = False
name = 'luren'
if name == 'python':         # 判断变量是否为 python
    flag = True              # 条件成立时设置标志为真
    print 'welcome boss'     # 并输出欢迎信息
else:
    print name               # 条件不成立时输出变量名称 

类加载机制

java虚拟机结构

问题:JVM由哪几块组成?

由类加载器、内存空间、执行引擎,垃圾收集、本地方法接口组成。

b04c

img

英文名称: 类加载器 Class Loader

内存空间或运行时内存空间 Runtime Data Area

栈:Stack

堆:Heap

Java的内存分为两类,一类是栈内存,一类是堆内存

栈内存是指程序进入一个方法时,会为这个方法单独分配一块私属存储空间,用于存储这个方法内部的局部变量,当这个方法结束时,分配给这个方法的栈会释放,这个栈中的变量也将随之释放。

堆是与栈作用不同的内存,一般用于存放,不放在当前方法栈中的那些数据,例如,使用new创建的对象都放在堆里,所以,它不会随方法的结束而消失。方法中的局部变量使用final修饰后,放在堆中,而不是栈中。

  1. 栈是用来存放基本类型的变量和引用类型的变量名(用来指向在堆内存中的引用变量),堆用来存放new出来的对象和数组(引用变量)。

  2. 栈的存取速度快,但不灵活。堆的存取速度慢,但是存取灵活,空间动态分配。

  3. 栈在建立在连续的物理位置上,而堆只需要逻辑位置连续即可。

  4. 堆是JVM运行时最大的内存区域。

stack(栈)的空间由操作系统自动分配和释放,heap(堆)的空间是手动申请和释放的,heap常用new关键字来分配。

stack空间有限,heap的空间是很大的自由区。

在Java中,

若只是声明一个对象,则先在栈内存中为其分配地址空间,

若再new一下,实例化它,则在堆内存中为其分配地址。

方法区:Method Area

本地方法栈:Native Method Stack

执行引擎:Execution Engine

本地方法接口:Native Interface

Java类的生命周期

问题:java类的生命周期由几个阶段组成

b04d

类加载机制主要就是加载、验证、准备、解析、初始化这些过程

1、类加载过程

问题:类加载(Loading)的过程中做了哪些处理

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将获取到的二进制字节流转化成一种数据结构并放进方法区
  • 在内存中生成一个代表此类的java.lang.Class对象,作为访问方法区中各种数据的接口

被加载的类的信息存储在方法区中,可以被线程所共享,也就是说,加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在了方法区之中。 Class对象虽然是在内存中,但并未明确规定是在Java堆中,对于HotSpot来说,Class对象存储在方法区中。它作为程序访问方法区中二进制字节流中所存储各种数据的接口。

2、验证阶段

这一阶段的目的主要是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,从而不会危害虚拟机自身安全。也就是说,当加载阶段将字节流加载进方法区之后,JVM需要做的第一件事就是对字节流进行安全校验,以保证格式正确,使自己之后能正确的解析到数据并保证这些数据不会对自身造成危害。

验证阶段主要分成四个子阶段:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

3、准备阶段

1.准备阶段的目的:正式为静态变量变量分配内存并设置静态变量初始值的阶段,这些变量所使用的内存将在方法区中分配。

2.这里的静态变量初始值通常是指数据类型的初始值。比如int的零值为0,long为0L,boolean为false… …真正的初始化赋值是在初始化阶段进行的。

额外一点,如果你设置的类变量还具有final字段,如下:

public static final int value = 123;

那么在准备阶段变量的初始值就会被直接初始化为123,具体原因是由于拥有final字段的变量在它的字段属性表中会出现ConstantValue属性。

4、解析阶段

解析阶段的目的:虚拟机将常量池内的符号引用替换为直接引用

常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用:直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。

重新解读:虚拟机将运行时常量池中那些仅代表其他信息的符号引用解析为直接指向所需信息所在地址的指针。

  • 在解析阶段主要有以下不同的动作

    类或接口的解析(注意数组类和非数组类)
    字段(简单名称+字段描述符)解析(注意递归搜索)
    类方法解析(注意递归搜索)
    接口方法解析(注意递归搜索)

动态连接

大部分JVM的实现都是延迟加载或者叫做动态连接。它的意思就是JVM装载某个类A时,如果类A中有引用其他类B,虚拟机并不会将这个类B也同时装载进JVM内存,而是等到执行的时候才去装载。

而这个被引用的B类在引用它的类A中的表现形式主要被登记在了符号表中,而解析的过程就是当需要用到被引用类B的时候,将引用类B在引用类A的符号引用名改为内存里的直接引用。

5、对象初始化阶段

问题:给出 … 代码,会输出什么?

对象初始化过程,将按照类文件中初始化代码,依次执行。并创建出类文件的实例

虚拟机规范定义了5种情况,会触发类的初始化阶段,也正是这个阶段,JVM才真正开始执行类中定义的Java程序代码:

  • new一个对象、读取一个类静态字段、调用一个类的静态方法的时候
  • 对类进行反射调用的时候
  • 初始化一个类,发现父类还没有初始化,则先初始化父类
  • main方法开始执行时所在的类
  • 用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例后解析结果REF_putStatic,REF_getStatic,REF_invokeStatic的方法句柄时,当该方法句柄对应的类没有初始化时,需要初始化该类 。

另外有三种引用类的方式不会触发初始化(也就是类的加载),为以下三种:

  • 通过子类(类名)引用父类的静态字段,不会导致子类初始化(会引发父类的初始化、不会引发子类的初始化)
  • 通过数组定义来引用类,不会触发此类的初始化
  • 引用另一个类中的常量不会触发另一个类的初始化,原因在于“常量传播优化

常量传播优化的例子

public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }
	public static final String HELLOWORLD = "hello world";
}

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}

常量“hello world”已经被存储到了NotInitialization类的常量池中,以后NotInitialization对常量ConstClass.HELLOWORLD的引用实际上都被转化为NotInitialization对自身常量池的引用。

通过数组定义来引用类的子类

 public class SuperClass {
     static {
         System.out.println("SuperClass init!");
     }
     public static int value = 123;
 } 

 public class NotInitialization {
     public static void main(String[] args) {
         SuperClass[] sca = new SuperClass[10];
     }
 }  

类加载器

需要了解、掌握的知识只有两点:

1.类加载器的命名空间
2.双亲委派模型

问题:java有几种类加载器

问题:类加载的双亲委派机制是什么

类加载器的命名空间:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。也就是说,你现在要比较两个类是否相等,只有在这两个类是同一个类加载器加载的前提下才有意义。

双亲委派模型:首先你得知道在JVM中有三种系统提供的类加载器:启动类加载器,扩展类加载器、应用程序类加载器

java的三种类加载器存在父子关系,子加载器保存着附加在其的引用,当一个类加载器需要加载一个目标类时,会先委托父加载器去加载,然后父加载器会在自己的加载路径中搜索目标类,父加载器在自己的加载范围中找不到时,才会交给子加载器加载目标类。

采用双亲委托模式可以避免类加载混乱,而且还将类分层次了,例如java中lang包下的类在jvm启动时就被启动类加载器加载了,而用户一些代码类则由应用程序类加载器(AppClassLoader)加载,基于双亲委托模式,就算用户定义了与lang包中一样的类,最终还是由应用程序类加载器委托给启动类加载器去加载,这个时候启动类加载器发现已经加载过了lang包下的类了,所以两者都不会再重新加载。当然,如果使用者通过自定义的类加载器可以强行打破这种双亲委托模型,但也不会成功的,java安全管理器抛出将会抛出java.lang.SecurityException异常。

b04e

双亲委派模型的破坏者-线程上下文类加载器
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例
b04f

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值