浅析Java虚拟机体系结构

1、什么是Java虚拟机

            大家都知道Java语言被称为是跨平台语言,那么它为什么具有跨平台性,而什么叫做跨平台性呢?跨平台性的意思就是:Java程序的执行与底层操作系统无关,一次编译,到处执行,这种跨平台就得益于JVM(Java虚拟机)。在了解Java虚拟机前,我们先了解Java技术体系:JRE,JDK,JVM

      JRE(Java Runtime Entertainment) Java运行环境:也称为Java运行平台,所有的Java程序在JRE上才能运行。

     JDK(Java Development Kit) Java开发工具:程序开发者用来编译,调试Java程序的工具包,因为JDK也是Java程序,所以也需要JRE才能运行(为了保持JDK的独立性和完整性,在JDK安装过程中,JRE也是安装的一部分)

      JVM(Java Virtual Machine)Java虚拟机:它是JRE的一部分,是一个虚拟出来的机器,通过真实的机器上仿真模拟各种计算机功能实现。JVM有自己的硬件架构,包括处理器,堆栈,寄存器以及相应的指令系统。

        首先,我们可以先了解以下Java程序的执行过程:

       由上图:Java程序的执行依赖于编译环境和运行环境,通过上图所示的过程,Java代码编程字节码文件再编程机器码文件Java虚拟机的。Java的核心就是Java虚拟机,因为所有的Java程序需要在Java虚拟机上运行,Java程序的运行需要Java API ,Java虚拟机和Java class文件的配合。Java的虚拟机实例负责一个Java程序的运行,当一个Java程序启动,一个Java虚拟机实例就产生了,当这个程序执行完成,这个Java虚拟机实例也就消亡了。也就是说,JVM的生命周期与Java程序的启动与完成有关。

2、JVM的体系结构    

     Java虚拟机的主要任务就是装载class文件并执行其中的字节码文件,其工作体系结构如下图:

      主要是由三部分组成:类加载子系统,运行时数据区,执行引擎(执行字节码或执行本地方法)

2.1 类加载系统(类加载器作用就是:将字节码文件加载到内存)

     Java的动态类的加载由类加载系统完成,它可以装载、连接、初始化类文件(第一次引用类是需要)加载:功能就是加载类,共有三个类加载器:

       BootStrap ClassLoader(启动类加载器)

       Extension class loader(扩展类加载器)

      application class loader(应用程序加载器)

各个加载器的工作责任:

(1)Bootstrap ClassLoader:(启动类加载器)

          负责加载JAVA_HOME中jre/lib/rt.jar里所有的class,(该目录下的所有jar包都是运行JVM所必需的jar包)由C++实现,不是ClassLoader子类

(2)Extension ClassLoader:

       负责加载java平台中扩展功能的一些jar包,包括JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

(3)App ClassLoader:

    负责加载classpath中指定的jar包及目录中class

注意类加载器其实自身也是一个 Java 类, 因此,自身类加载器需要被其他类加载器进行加载后方可使用, 显然必须有一个类加载器的顶级父类(也就是 Bootstrap ClassLoader, 该类加载器是由 C 语言代码进行开发的)是其他类加载器的父类。 关键点在于, 如果一个类的类加载器是 Bootstrap ClassLoader, 那么该类的 getClassLoader()方法返回 null。

   类加载过程涉及一个模型:双亲委派模型

工作过程:

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载

5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

        这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。

双亲委派模型的好处: 

1、防止内存中出现多份同样的字节码(安全性角度)

特别说明: 类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。

2、可见性机制

子类加载器可以看到父类加载器加载的类。

3、单一性机制

因委托机制的关系,一个类(唯一的全限定名)只能被一个类加载器加载一次。

类加载过程

              加载 -》验证-》 准备-》 解析-》 初始化-》 使用 -》卸载

注意:其他阶段必须按照这个顺序进行,而解析阶段不一定按照这个顺序,它在某些情况下也可以在初始化之后进行,这些阶段一般是互相交叉混合式进行,不是按部就班地执行。

加载:类加载机制-》类加载器;查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象;

验证:字节码验证器:验证字节码,符号引用,元数据,文件格式等,验证失败则 无法进行;

准备:内存空间分配(只包括静态变量,不包括实例变量),将静态变量初始化(通常情况下,设置初始值一般是数据类型的零值;特殊情况下,如final修饰的静态变量会设置为给定的值);

解析:将所有符号引用替换为方法区域的原始引用;

初始化:基本属性(非静态)的初始化以及为静态变量赋正确的值;

       明白了类的整个加载过程,那么新的问题又来了,类什么时候加载呢?主动初始化:(主动引用)

  1.     创建对象 new类对象
  2.     反射 class.forName()
  3.    调用类的静态属性或静态属性赋值
  4.    调用类的静态方法
  5.    初始化一个类的父类的子类,使用子类是先初始化父类
  6.    JVM启动时标记为启动类的类(main)

除了以上几种情况外的方式,都不会触发初始化,称为被动引用。

被动引用举例说明:

(1)通过子类引用父类的静态字段,不会导致子类初始化(对于静态字段,只有直接定义这个字段的类才会被初始化 ,因此这种情况下,只会出发父类的初始化而不会触发子类的初始化)

(2)通过数组定义来引用类,不会触发此类的初始化

(3)常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义的类的初始化。

      Java类加载是动态过程,并不是一次加载完所有类,按需加载(只要保证程序运行的基础类完全加载到内存中,至于其他类,需要的时候再加载,目的是节省内存开销)

类加载顺序:

没有继承关系时:

静态成员变量-->静态代码块-->实例变量-->实例代码块-->构造方法

在有继承关系存在时:

父类静态成员变量-->父类静态代码块-->子类静态成员变量-->子类静态代码块-->父类普通实例变量-->父类实例代码块-->父类构造方法-->子类普通实例变量-->子类实例代码块-->子类构造方法

JIT即时编辑器

一般我们可能会想:JVM在加载了这些class文件以后,针对这些字节码,逐条取出,逐条执行-->解析器解析。

但如果是这样的话,那就太慢了!

我们的JVM是这样实现的:

  • 就是把这些Java字节码重新编译优化,生成机器码,让CPU直接执行。这样编出来的代码效率会更高。
  • 编译也是要花费时间的,我们一般对热点代码做编译,非热点代码直接解析就好了。

热点代码解释:一、多次调用的方法。二、多次执行的循环体

使用热点探测来检测是否为热点代码,热点探测有两种方式:

  • 采样
  • 计数器

目前HotSpot使用的是计数器的方式,它为每个方法准备了两类计数器:

  • 方法调用计数器(Invocation Counter)
  • 回边计数器(Back EdgeCounter)。
  • 在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译

2.2 运行时数据区(JMM Java内存模型)

    Java虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,如下图所示:

   首先,Java内存模型是如何进行划分的呢,每个部分存储什么呢?

  1. 方法区: 静态变量
  2. 堆区:对象,垃圾回收主要作用于堆区
  3. 虚拟机栈:变量表(堆内存中的对象地址引用),局部变量,方法返回
  4. 本地方法栈:native方法相关存储信息
  5. 程序计数器:当前字节码整的位置指示器

程序计数器(线程私有)    

      这是一个较小的内存空间,是当前线程正在执行的哪一条字节码指令的地址,若当前线程正在执行的是一个本地方法,那么此时程序计数器为Undefined。

作用:

  • 字节码解释器通过改变程序计数器来依次获取指令,从而实现代码的流程的控制
  • 在在多线程情况下,程序计数器记录的是当前线程执行的执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了

特点:

  • 是一块较小的内存空间
  • 线程私有,每个线程都有自己的程序计数器
  • 生命周期:随着线程的创建而创建,随着线程的销毁而销毁
  • 是一个唯一不会出现的OutOfMemoryError的内存区域

虚拟机栈(线程私有)

      虚拟机栈的生命周期随着线程的生命周期开始和结束,虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行时都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用直至执行完成的过程,就对应一个栈帧在虚拟机栈从入栈到出栈的过程。

存储内容:

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
  • 栈操作(Operand Stack):记录出栈、入栈的操作;
  • 栈帧数据(Frame Data):包括类文件、方法等等。

Java虚拟机栈会出现的异常

StackOverFlowError

  • 若Java虚拟机栈的大小不允许动态扩展,那么当前线程请求的栈的深度超过当前的Java虚拟机栈的最大深度时,就会抛出此异常

  • 解决:查看是否开启动态扩容,如果已经开启还出现异常,则要查看是否部分内存未及时释放

OutOfMemoryError

  • OutOFMemoryError,若允许动态扩展,那么当前线程的请求的栈内存用完了,无法再动态扩展时,抛出此异常

  • 解决:当线程请求到达栈的线程上限,则无法解决,未到达可以进行调整,修改上限等

      这两种异常都是在先到达上限后,根据动态扩容来判断抛出哪种异常(若不允许动态扩展,抛出StackOverFlowError,若是允许动态扩容,抛出OutOfMemoryError)

本地方法栈(线程私有)

      和Java虚拟机栈的作用的类似

与Java虚拟栈的区别:

  • 虚拟机栈为字节码服务
  • 本地方法栈为虚拟机使用到的native方法服务,也会抛出两种异常(StackOverFlowError和OutOfMemoryError)

堆(线程共享)

特点:

  • JVM中管理的最大的内存空间;
  • 堆是所有线程共享的内存区域,在虚拟机启动时创建;
  • 抛出异常:OutOfMemoryError 当堆中没有内存完成实例分配,且堆也无法再扩展,抛出异常

存储内容:

  • 几乎所有的对象和数组都会存放在堆中;
  • 垃圾回收机制主要作用于堆中;

Java堆可以处于物理上不连续的地址空间,只要逻辑上连续即可。

方法区(线程共享)

方法区和堆是线程共享区域,

存储:已被虚拟机加载的类信息,静态变量,常量

Hotspot虚拟机将方法区称为永久代:方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,把方法区称为"永久代"

常量池:存放类的版本,字段,方法,接口等描述信息(在类加载后存放到方法区的运行时常量池中)

抛异常:OutOfMemoryError(方法区无法满足内存分配要求时)

参考:

深入理解Java虚拟机http://wiki.jikexueyuan.com/project/java-vm/

理解Java虚拟机体系结构https://www.cnblogs.com/lao-liang/p/5110710.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值