JVM调优一:JVM内存模型

JVM调优一:JVM内存模型

一、前言

我们都知道Java一直宣传的口号是:一次编译,到处运行。而实现这一口号的基础,便是在各个平台上基于统一的规范实现的虚拟机,这一规范便是JVM(Java Virtual Machine)。

通过统一的规范,Java语言屏蔽了与具体平台的相关信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

随着业务的推进,我们或多或少的需要对JVM进行一些参数调优,以最大化的利用机器的性能,以此达到降低硬件成本以及提高用户体验的目的。这时,了解JVM的运行原理可以很好的帮助我们根据业务需要进行JVM参数调优。

二、JVM基本结构

下图为JDK1.8的JVM结构图(本文内容以JDK1.8为准):
在这里插入图片描述

  1. 方法区(元空间):旧版本JDK中叫永久带,主要存放常量、静态变量、类元信息,为各线程共享区域。

  2. 堆:也是线程共享的区域,大多数的类的示例都放在堆中。因为程序运行过程中会产生许许多多的实例,因此堆也是空间最大的。

  3. 线程栈:旧版本JDK中叫虚拟机栈,它的生命周期与线程相同,且与线程一一对应,即一个线程对应一个线程栈,线程栈是JVM执行程序的主要区域,本文后续内容会详细讲解线程栈。

  4. 程序计数器:记录程序要执行的下一行指令码的指针,可以简单理解为程序要执行的下一行的代码行号。

  5. 本地方法栈:调用本地方法的接口,是JVM连接操作系统、硬件交互的桥梁。

三、JVM运行原理

Java程序是以Java线程为单位进行执行的,所以JVM的运行原理基本可以认为是线程栈的工作原理。JVM线程栈的结构如下图所示:
20190929223028295

栈帧:线程内没调用一次方法,便会生成一个栈帧。

栈帧由局部变量表、操作数栈、动态链表、方法出口四个部分组成。

本文将以如下代码为例,对线程栈的运行原理进行解读:

public class Calculate {
    public Integer calculate() {
        int a = 10;
        int b = 20;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Calculate calculate = new Calculate();
        calculate.calculate();
    }
}

先将代码进行打包,生成Calculate.class文件
在这里插入图片描述
用javap命令,将class文件编译成易读的JVM指令码:
在这里插入图片描述
生成文件内容如下图所示:
在这里插入图片描述
生成的文件内容是可读性比较高的JVM指令码程序,我们可以在网上搜索一份JVM指令码手册,参考手册进行解读,这里摘录了一部分示例代码中用到的指令码:

new 创建一个新对象

dup 复制栈顶部一个字长内容

invokespecial 根据编译时类型来调用实例方法

astore 将将引用类型或returnAddress类型值存入局部变量

aload 从局部变量中装载引用类型值(refernce)

invokevirtual 调度对象的实便方法

pop 弹出栈顶端一个字长的内容

return 从方法中返回,返回值为void

bipush 将一个8位带符号整数压入栈

istore 将int类型值存入局部变量(istore后面接数字代表局部标量表的index)

iload 从局部变量中装载int类型值(指令后的数字通istore)

iadd 执行int类型的加法

imul 执行int类型的乘法

invokestatic 调用命名类中的静态方法

areturn 从方法中返回引用类型的数据

所有准备均已就绪,接下来我们开始按照指令码逐步讲解JVM的执行过程。

1、执行main方法

main方法是示例代码的入口,所以首先从main方法开始执行。JVM执行的第一步,便是在虚拟机内部创建程序主线程的线程栈。(本文主要讲解jvm的运行原理,所以跳过了类加载的过程)
在这里插入图片描述
2、主线程调用main方法,将main方法栈压入线程栈,程序计数器记录下一个要执行的指令码的指针(new指令码)
在这里插入图片描述

3、操作数栈调用new指令,现在元数据去找到Calculate.class的class实例,程序计数器记录下一个指令dup
在这里插入图片描述

4、执行dup指令,获取到操作数栈中Calculate.class的指针。程序计数器内保存下一个指令的指针(invokespecial)
在这里插入图片描述

5、执行invokespecial指令指向的init方法,生成对象,生成的对象压如操作数栈,程序计数器保存下一个指令的指针(astore_1)
20190930113245119
6、执行astore_1,将操作数栈栈顶的数据弹出,保存进局部变量表1(1为局部变量表的index),并记录下一个指令aload_1的到程序计数器。
在这里插入图片描述

7、执行aload_1,即从局部变量表1中读取值到操作数栈
在这里插入图片描述
8、执行invokevirtual(Method calculate:() Ljava/lang/Integer;),即执行操作数栈顶Calculate实例的calculate方法,这时需要将栈帧calculate压入线程栈,且栈帧calculate的方法出口指向栈帧main的操作数栈invokevirtual的指令码对应的结果地址。
在这里插入图片描述
9、执行bipush 10(将一个8位带符号整数压入栈),即将数字10压如操作数栈。
在这里插入图片描述
10、执行istore_1(将int类型值存入局部变量1),弹出操作数栈栈顶的数据,存入本地变量表1。
在这里插入图片描述
11、同理处理变量2
在这里插入图片描述
12、按顺序执行iload_1、iload_2,得到结果如下图
在这里插入图片描述
13、执行iadd指令(执行int类型的加法),即将操作数栈栈顶的两个int数据相加,结果压入操作数栈。
在这里插入图片描述
14、参考9~14步,处理后续的乘法,得到结果如下图。
在这里插入图片描述

15、执行 areturn 从方法中返回引用类型的数据,将300根据方法出口返回给main方法,并弹出栈帧-calculate
在这里插入图片描述20190930122822584
16、依次执行pop、return,结束掉main方法。

四、其他

了解了JVM的运行流程,我们可以根据业务情况适当调整线程栈的大小(-Xss参数)。

以上示例都是在未考虑JVM逃逸分析(俗称栈上分配,通过-XX:+DoEscapeAnalysis参数来进行配置是否启用)的情况下运行的流程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值