「读书分享」- 走进JVM,深入理解JVM

---云开方见日,潮尽炉峰出。揭开JVM的神秘面纱,探寻底层实现原理

Java Vitural Machine

Java 的源代码是怎么被机器识别并执行的呢?答案是Java虚拟机,即 Java Vitural Machine, 简称 JVM.

字节码

字节码是一种中间状态(中间码)的二进制代码(文件),Java中所有指令有200个左右,一个字节(8位)可以存储256种不同指令信息,一个这样的字节称为字节码(ByteCode)。在代码的执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖,JVM 也可以将字节码编译执行,如果是热点代码,会通过JIT 动态地编译为机器码,提高执行效率。

程序的执行方式分为静态编译执行、动态编译执行和动态解释执行。 注意:此处所说的编译指的是编译成可让操作系统直接执行的机器码。

 

java执行

字节码和机器码的区别

机器码是电脑CPU直接读取运行的机器指令,运行速度最快,但是非常晦涩难懂,也比较难编写,一般从 业人员接触不到。

字节码是一种中间状态(中间码)的二进制代码(文件)。需要直译器转译后才能成为机器码。

加载或存储指令

在某个栈帧中,通过指令操作数据在虚拟机的局部变量表与操作栈之间来回传输,常见指令如下

(1)将局部变量表的变量加载到操作栈中:如ILOAD(将int类型的局部变量压入操作栈)和ALOAD(将对象引用的局部变量压入栈)等。

(2)从操作栈顶将变量存储到局部变量表中:如ISTORE、ASTORE等

(3)将常量加载到操作栈顶,这是极为高频使用的指令。如ICONST、BIPUSH、SIPUSH、LDC等。

ICONST 加载的是 -1 ~ 5 的数(ICONST 与BIPUSH 的加载界限)。

BIPUSH ,即Byte Immediate PUSH ,加载 -128 ~ 127 之间的数。

SIPUSH ,即Short Immediate PUSH ,加载 -32768 ~ 32767 之间的数。

LDC ,即Load Constant ,在 -2147483648 ~ 2147483647 或者是字符串时,JVM采用LDC 指令压入枝中。

运算指令

对两个操作栈帧上的值进行运算,并把结果写入操作栈,如IADD、IMUL等

类型转换指令

显示转换两种不同的数值类型。如I2L、D2F等

对象创建与访问指令

根据对象的创建、初始化、方法调用相关指令,常见指令如下:

(1)创建指令:如NEW、NEWAPPAY等。

(2)访问属性指令:如GETF!ELD 、PUTFIELD 、GETSTATIC 等。

(3)核查实例类型指令:如INSTANCEOF、CHECKCAST 等。

操作栈管理指令

JVM提供了直接操控操作栈的指令,常见指令如下:

(1)出栈操作:如POP即一个元素,POP2即两个元素。

(2)复制栈顶元素并压入栈:DUP。

方法调用与返回指令

( I ) INVOKEYIRTUAL 指令:调用对象的实例方法。

( 2) INVOKESPECIAL 指令:调用实例初始化方法、私有方法、父类方法等。

( 3 ) INVOKESTATIC 指令: 调用类静态方法。

( 4 ) RETURN 指令: 返回VOID 类型。

同步指令

JVM使用方法结构中的ACC_SYNCHRONIZED标志同步方法,指令集中有MONITORENTER和MONITOREXIT支持synchronized语义。

方法调用计数器触发即时编译简单流程

 

方法调用计数器触发即时编译

回边计数器 它的作用就是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”。

JIT优化

HotSpot 虚拟机使用了很多种优化技术,这里只简单介绍其中的几种,完整的优化技术介绍可以参考官网内容。

(1)公共子表达式的消除

公共子表达式消除是一个普遍应用于各种编译器的经典优化技术,他的含义是:如果一个表达式E已经 计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共 子表达式。对于这种表达式,没有必要花时间再对他进行计算,只需要直接用前面计算过的表达式结果 代替E就可以了。

举个简单的例子来说明他的优化过程,假设存在如下代码:
int d = (c*b)*12+a+(a+b*c);

如果这段代码交给Javac编译器则不会进行任何优化,那生成的代码如下所示,是完全遵照Java源码的写 法直译而成的。

当这段代码进入到虚拟机即时编译器后,他将进行如下优化:编译器检测到”cb“ ”bc“是一样的表达 式,而且在计算期间b与c的值是不变的。因此,这条表达式就可能被视为:

int d = E*12+a+(a+E);

这时,编译器还可能(取决于哪种虚拟机的编译器以及具体的上下文而定)进行另外一种优化:代数化 简(Algebraic Simplification),把表达式变为:

int d = E*13+a*2;

(2)方法内联

在使用JIT进行即时编译时,将方法调用直接使用方法体中的代码进行替换,这就是方法内联,减少了方 法调用过程中压栈与入栈的开销。同时为之后的一些优化手段提供条件。如果JVM监测到一些小方法被 频繁的执行,它会把方法的调用替换成方法体本身。

比如如下代码

 private int add4(int x1, int x2, int x3, int x4) {
    return add2(x1, x2) + add2(x3, x4);
  }
private int add2(int x1, int x2) {
    return x1 + x2;
  }

运行一段时间后优化为

private int add4(int x1, int x2, int x3, int x4) {
       return x1 + x2 + x3 + x4;
  }

(3)逃逸分析

逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序 中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够 分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引
用,例如作为调用参数传递到其他地方中,称为方法逃逸。

类的加载过程

 

类的加载过程

在加载的过程中,JVM主要做3件事情

1.通过一个类的全限定名来获取定义此类的二进制字节流(class文件) 。在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化的条件时,就根据 要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程

2.将这个字节流的静态存储结构转化为方法区的运行时数据结构

3.在内存中创建一个该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口

双亲委派模型

当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶

层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

采用双亲委派的一个好处是:

比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶 层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object 对象。

 

双亲委派模型

 

内存布局

 

经典JVM内存布局

程序计数器

程序计数器(Program Counter Register),也叫PC寄存器,是一块较小的内存空间,它可以看作是 当前线程所执行的字节码指令的行号指示器。字节码解释器的工作就是通过改变这个计数器的值来选取 下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完 成。

JVM Stack(Java虚拟机栈)

栈(Stack)是一个先进后出的数据结构,就像子弹的弹夹,最后压入的子弹先发射,压在底部的子弹最后发射,撞针只能访问位于顶部的那一颗子弹。每个Java方法在执行的时候都会创建一个栈帧 (Stack Frame)相当于子弹。

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变 量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一 个栈帧在虚拟机栈里从入栈到出栈的过程。

虚拟机栈里从入栈到出栈的过程

现在我们来分析下每个栈帧的内部结构,包括局部变量表、操作栈、动态链接、方法返回地址等。

(1)局部变量表(Local Variable Table)

局部变量表是存放方法参数和局部变量的区域。相对于类属性变量的准备阶段和初始化阶段来说,局部变量表没有准备阶段,必须显示初始化。如果是非静态方法,则在index[0]的位置上存储的是方法所属对象的引用实例,随后存储的是参数和局部变量。前面提到的字节码STORE指令就是将操作栈中的计算完成的局部变量写回局部变量表的存储空间内。

存储容量:局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用 内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

(2)操作栈

操作栈是一个初始状态为空的桶式结构栈,它是一个后入先出栈(LIFO)。在方法执行过程中,会有各种指令往栈中写入和提取信息。JVM的执行引擎是基于栈的执行引擎,其中的栈就是指的操作栈。随着方法执行和字节码指令的执行,会从局部变量表 或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表 或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈 的过程。

(3)动态链接

在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。 Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。 这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接

(4)方法返回地址

方法执行时有两种退出情况,第一, 正常退出,即正常执行到任何方法的返回字节码指令, 如阻RETURN 、IRETURN 、ARETURN 等;第二, 异常退出。无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

  1. 返回值压入上层调用栈帧。
  2. 异常信息抛给能够处理的栈帧。
  3. PC 计数器指向方法调用后的下一条指令。

本地方法栈

本地方法栈和虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈 为虚拟机使用到的Native方法(比如C++方法)服务。

Java 堆(重要)

Java堆被所有线程共享,在Java虚拟机启动时创建。是虚拟机管理最大的一块内存。

Java堆是垃圾回收的主要区域,主要采用分代回收算法。堆进一步划分主要是为了更好的回收内存或更快的分配内存。

堆主要分成两大块:新生代和老年代。新生代包含(Eden空间[伊甸园]、From Survivor空间、To Survivor空间)。默认的,Eden : from : to = 8 : 1 : 1 。(可以通过参数 –XX:SurvivorRatio 来设定 。即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小)。堆大小 = 新生代+老年代。堆的大小可通过参数-Xms(堆的初识容量)、-Xmx(堆的最大容量)来指定。

内存分配

内存分配的方法有两种:指针碰撞(Bump the Pointer)和空闲列表(Free List)

 

内存分配

 

对象分配与简要GC流程

 

方法区

介绍

线程共享,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等等。

jdk1.7之前,HotSpot虚拟机对于方法区的实现称之为“永久代”,Permanent Generation.

jdk1.8之后,HotSpot虚拟机对于方法区的实现称之为“元空间”,Meta Space .

方法区是Java虚拟机规范中的定义,是一种规范,而永久代和元空间是 HotSpot虚拟机不同版本的两种实现。

运行时常量池和字符串常量池

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编 译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

 

Java的线程与内存

对象实例化

  • 确认类元信息是否存在。当JVM接受到new指令时,首先在metaspace 内检查需要创建的类元信息是否存在。若不存在,那么在双亲委派模式下,使用当前类加载器以ClassLoader +包名+类名为key 进行查找对应的.class 文件,如果没有找到文件,则抛出ClassNotFoundException 异常,如果找到,则进行类加载并生成对应的Class 类对象。
  • 分配对象内存。首先计算对象占用空间大小,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小,接着在堆中划分一块内存给新对象。在分配内存空间时,需要进行同步操作,比如采用CAS失败重试,区域加锁等方式保证分配操作的原子性。
  • 设定默认值。成员变量值都需要设定默认值,即各种不同形式的零值。
  • 设置对象头。设置新对象的哈希码,GC信息,锁信息,对象所属的类元信息等。这个过程的具体设置方式取决JVM实现。
  • 执行init 方法。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

垃圾回收

Java 会对内存进行自动分配与回收管理,使上层业务更加安全,方便地使用内存实现程序逻辑。GC 主要目的是清除不再使用的对象,自动释放内存。

GC是如何判断对象是否可以被回收的呢?为了判断对象是否存活,JVM引入了GC Roots.那么什么对象可以作为GC Roots呢?比如:类静态属性中引用的对象、常量引用的对象、虚拟栈中引用的对象、本地方法栈引用的对象等。

垃圾回收算法 主要是2种:引用计数法和根搜索算法(可达性算法)

垃圾回收器是实现垃圾回收算法并应用与JVM环境中的内存管理模块。垃圾回收器有数十种,本文只介绍Serial、CMS、G1 三种

Serial回收器是一个主要应用与YGC的垃圾回收器,采用串行单线程的方式完成GC任务。其中垃圾回收器的某个阶段会暂停整个应用程序的执行(STW-Stop The Word)。FGC 的时间相对较长,频翻FGC会严重影响应用程序性能

CMS回收器是回收停顿时间比较短、目前比较常用的垃圾回收器。它通过初识标记(Initial Mark)、并发标记(Concurrent Makr)、重新标记(Remark)、并发清楚(Concurrent Sweep)四个步骤完成垃圾回收工作。由于CMS 采用的是“标记-清除算法” 因此会产生大量的空间碎片。可以通过配置

-XX:+UseCMSCompactAtFullCollection 参数,强制JVM 在FGC 完成后对老年代进行压缩,执行一次空间碎片整理。同时空间碎片整理也会引发(STW)。

G1 垃圾回收可以通过-XX:UseG1GC 参数启用。和CMS相比,G1具备压缩功能,能避免碎片问题,G1的暂停时间更加可控。G1 采用的是“Mark-Copy”

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值