我爱编程

有关JVM

2018-02-23  本文已影响12人  34sir

JVM是什么?

一种软件实现,执行物理机程序
特点:

作用

提供通用的机器无关的执行平台:

定义了:

生命周期

整体架构

执行过程:


jvm process.png

Class Loader

类加载器,负责加载程序中的类型(类和接口),并赋予唯一的名字
三种 ClassLoader:


classloader.png

三种classloader关系

Bootstrp loader 是在Java虚拟机启动后初始化的

Class Loader 实现 负责加载
Bootstrp loader C++ %JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类
ExtClassLoader Java %JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库
AppClassLoader Java classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器

双亲委托模型

ClassLoader的加载采用了双亲委托机制,有以下几个步骤:

步骤如图:


classloader 加载策略.png

为什么使用双亲委托模型?
这里涉及到ClassLoader的隔离问题
每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名进行搜索来检测这个类是否已经被加载了
那么,一个运行程序中有没有可能同时存在两个包名和类名完全一致的类?
答案是,有可能
JVMDalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个”类”不是由一个ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是ClassLoader 隔离
双亲委托是ClassLoader问题的一种解决方案,也是 Android 差件化开发和热修复的基础

类装载器的特点

Java提供了动态加载特性:他会在运行时的第一次引用到一个class的时候对它进行加载、链接和初始化,而不是在编译时进行
类装载器的特点:

过程分析

上文提到的三个过程:

一一分析:

加载:找到代表这个类的class文件或根据特定的名字找到接口类型,然后读取到一个字节数组中。这些字节会被解析检验它们是否代表一个class对象并包含正确的majorminor版本信息。直接父类的类和接口也会被加载进来。这些操作一旦完成,类或者接口对象就从二进制表示中创建出来了

链接:检验类或接口并准备类型和父类接口的过程。包含三个步骤:

初始化:把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值

load process.png

执行引擎

通过类装载器装载的,被分配到JVM的运行时数据区的字节码会被执行引擎执行。执行引擎以指令为单位读取 Java 字节码。它就像一个 CPU 一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。
不过 Java 字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被 JVM 执行的语言。
字节码可以通过以下两种方式转换成合适的语言:

补充说明:
Java字节码是解释执行的,没有在JVM中执行原生代码快。
为了提到性能,Oracle Hotspot虚拟机会找到执行最频繁的字节码片段并把它们编译成原生机器码,编译出的原生代码保存在非堆内存的代码缓存中。基于这种方法(JIT),Hotspot 虚拟机将权衡下面两种时间消耗:

Dalvik 是依靠一个 Just-In-Time (JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART 则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

运行时数据区

JVM运行时结构图:


runtime_data.png

PC寄存器(PC Register)

也叫程序计数器,是一块较小的内存空间,作用可以看做是当前线程所执行的字节码的信号指示器
每一条JVM线程都有自己的PC寄存器
任意时刻,一条 JVM线程只会执行一个方法的代码,该方法称为该线程的当前方法(Current Method

此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域

JVM 栈

同PC寄存器一样,JVM 栈也是线程私有,每一个JVM线程都有自己的JVM栈,这个栈与线程同时创立,与线程生命周期相同,用来保存栈帧。JVM只会在JVM栈上执行pushpop操作

JVM 栈异常情况

栈帧:随着方法被调用而创建,随着方法结束而销毁(方法出现异常也视为方法结束)
每一个栈帧都包含下列三样:

局部变量数组(Local variable array)
每一个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表。栈帧中局部变量表的长度由编译期决定
Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从 0 开始的连续的局部变量表位置上
当一个实例方法被调用的时候,第 0 个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即 Java 语言中的“this”关键字)

操作数栈(Operand stack)
每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出(Last-In-First-OutLIFO)栈
Java 虚拟机提供一些字节码指令来 从局部变量表或者对象实例的字段中 复制常量或变量值到操作数栈中,也提供了一些指令用于 从操作数栈取走数据、操作数据和把操作结果重新入栈。
作用:
在方法调用的时候,操作数栈用来准备调用方法的参数以及接收方法返回结果

动态链接(Dynamic Linking
每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking
在 Java中,链接阶段是运行时动态完成的
Java类文件编译时,所有变量和方法的引用都被当做符号引用存储在此类的常量池中
符号引用是常量引用,实际上并不指向物理内存地址
JVM可以选择符号引用解析的时机:

JVM必须在第一次使用符号引用时完成解析,并抛出可能发生的解析错误
绑定是将对象域、方法、类的符号引用替换为直接引用的过程,绑定只会发生一次
如果一个类的符号引用还没被解析,就会载入这个类
每一个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联)的偏移量

方法正常调用完成
在这种场景下,当前栈帧承担着 回复调用者状态 的责任,其状态包括 调用者的局部变量表操作数栈和被正确增加过来表示执行了该方法调用指令的程序计数器等。使得调用者的代码能在被调用的方法返回并且返回值被推入调用者栈帧的操作数栈后继续正常地执行

方法异常调用完成
方法执行的过程中,某些执行导致了Java虚拟机的异常,并且此异常在本方法中没法处理或者执行过程中遇到了 throw 字节码指令显式地抛出异常,并且在该方法内部没有把异常捕获住
此种场景下,一定不会有返回值返回给它的调用者

本地方法栈(Native method stack
Java虚拟机可能会使用到传统的栈来支持native方法(使用Java语言以外的其它语言编写的方法)的执行,这个栈就是本地方法栈

方法区
被加载类的信息都保存在方法区中,包括:

方法区是线程共享的,所以访问方法区线程的方法必须是线程安全的
方法区是在JVM启动的时候创建的
方法区中存储了每一个类的结构信息:

运行时常量池
每一个接口或者类的常量池的运行时表现形式
包括了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用
简而言之,当一个方法或者变量被引用时,JVM通过运行时常量池来查找方法和变量在内存中的实际地址
运行时常量池是方法区的一部分,每一个运行时常量池都分配在JVM的方法区中,在类和接口被加载到JVM中时,对应的运行时常量池就会被创建


JVM中,堆是所有线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域
堆在虚拟机启动的时候就被创建,其中存储了各种对象,这些对象被垃圾回收器所管理,无需也无法显式的被销毁
对比栈和方法区:

上一篇 下一篇

猜你喜欢

热点阅读