JVM内存模型分析

一、JVM概念

1、什么是JVM

JVM是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的。JVM虚拟机由类装载子系统、字节码执行引擎、运行时数据区组成。JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

2、JVM与JDK和JRE的关系

(1)JRE(Java Runtime Environment),也就是java平台。所有的java程序都要在JRE环境下才能运行。
(2)JDK(Java Development Kit),是开发者用来编译、调试程序用的开发包。JDK也是JAVA程序需要在JRE上运行。
(3)JVM(Java Virtual Machine),是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。

二、JVM虚拟机

1、JVM组成结构

JVM虚拟机主要包含三部分 类装载子系统、字节码执行引擎、运行时数据区。程序在JVM虚拟机中执行过程是先通过类装载子系统将类、变量、以及类相关信息放到运行时数据区中。通过字节码执行引擎执行.class文件(字节码)中的代码。通过运行时数据区中的相关内存来计算运行具体行数代码。

操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境:

  1. 创建JVM装载环境和配置
  2. 装载JVM.dll
  3. 初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
  4. 调用JNIEnv实例装载并处理class类。

2、JVM的生命周期

JVM实例对应了一个独立运行的java程序,它是进程级别。JVM执行引擎实例则对应了属于用户运行程序的线程它是线程级别的。
1. 启动:启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void
main(String[] args)函数的class都可以作为JVM实例运行的起点
2. 运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程
3. 消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出

三、JVM内存模型

JVM内存模型也称为JVM运行时数据区。由堆、栈、本地方法栈、方法区(元空间)以及程序计数器五部分组成。我们借助代码实例对JVM内存模型运行时数据进行分析

1、代码运行时数据流转

代码示例

package com.test.jvm;

/**
 * 分析JVM内存模型运行
 */
public class Math {
    private final static int initData = 666;
    private static User user = new User();


    public int compute(){       //一个方法对应一个块栈帧内存区域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math m = new Math();
        int result = m.compute();
        System.out.println(result);
    }
}

内存模型中数据运行

以上面代码为例,代码在内存模型中数据运行图如下

2、内存模型各区域功能

1. 栈(虚拟机栈)

栈又被称为虚拟机栈,主要存储方法、方法中局部变量以及运行时数据。也可叫做线程栈,每个线程执行时都会从栈中划分出一块给线程单独使用,所以栈是线程私有的。

栈中存放的是一个个的栈帧,程序中每个方法都有对应的一块栈帧内存区域。栈帧由局部变量表、操作数栈、动态链接、方法出口四部分组成。

局部变量表:存放方法中的局部变量,默认有一个this变量。如compute()方法中a、b、c等变量

操作数栈:是程序运行中操作数进行运算的临时的中转存放内存空间。如进行a=1、b=2、a+b、以及*10等计算操作在操作数栈中进行计算,计算过程是在CPU的寄存器中进行。可以在.class(字节码)文件中查看操作数栈运行步骤。

动态链接:在程序运行过程中将符号引用转换为直接引用。如compute()方法, 程序初始化时将符号放到了常量池(Constant Pool)中。程序初始化加载时只会解析静态方法将符号引用转换为直接引用称为静态链接。

在程序运行时需要找到compute()符号对应的代码,那么就需要对compute符号进行解析。解析compute()符号就是找compute()符号在方法区对应的直接地址(方法实际代码在加载时 加载到了方法区 有对应的地址)。这个过程就是将符号引用转换为直接引用被称为动态链接

方法出口:当前方法执行完需要回到执行这个方法后面的位置,接着往下执行,那么这个方法调用地方后面的位置信息就存储在方法出口中。如调用compute()方法时,已经将main方法运行的线程信息存放到了方法出口区域了。

线程在栈帧中运行如下

通过javap -c 对代码进行反汇编生成Math.txt文件,就可以查看文件在栈帧中执行过程。如下

打开Math.txt文件就可看到compute()执行过程。可以通过官方文档查看命令对应的具体操作

2. 堆(Heap)

堆是用来存储对象和数组(数组引用是存放在Java栈中的)。在JVM中只有一个堆。只要是堆中的对象,就可以被所有线程共享(静态变量、静态常量、字符串存储在堆中的老年代里)。

对于基本数据类型对象(如byte、short、int、long、float、double、char),在方法体内声明时,会直接分配在栈中,其它情况都会分配在堆中。
  
对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用它的引用。如main()方法中Math对象的引用保存在栈帧的局部变量表中。但是在开启了逃逸分析时,如果发现某个对象只会在方法内部使用,则可能会将该对象经过标量替换后也存在栈中。

堆的几个重要参数:
  -Xms:堆的最小值(初始值,默认单位是:字节,要求是1024的整数倍);
  -Xmx:堆的最大值;
  -Xmn:新生代的大小;
  -XX:NewSize;新生代最小值(初始值);
  -XX:MaxNewSize:新生代最大值;

堆的组成

堆由年轻代和老年代组成,两者默认比例为1:2。年轻代包含eden区和两个Survivor区,它们的比例默认 8:1:1。

年轻代中的对象经过多次mintor GC回收后若一直存活,当对象年龄达到15时,就会将对象移动到老年代中。因为对象年龄是在对象头中用4个bit存储,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

Mintor GC

当eden区内对象存满时就会触发mintor GC,这时会对eden区和其中一个survivor区进行GC,对垃圾对象(死亡对象)进行回收,将非垃圾对象(存活对象)采用复制算法放到另一个空的Survivor区。这时存活的对象年龄+1。

Full GC

当老年代中对象存储满时就会触发Full GC,Full GC是对整个堆内存和方法区内存空间进行垃圾回收。当老年代中的对象还是被GC root引用,无法进行回收时,再往老年代中放对象时就会触发OOM

STW机制

STW机制全称叫Stop the World机制,该机制是在堆发生GC(mintor、Full)过程中会暂停用户线程,不让线程继续执行。凡是由用户发起的线程都被称为用户线程

STW机制的核心目的是确保垃圾回收过程中内存分析的一致性。在进行可达性分析算法时,JVM需要确保整个堆内存的状态在某一时刻是静态的、一致的,以便准确地判断哪些对象是可达的,哪些对象是不可达的。

为什么设计STW机制,不使用STW机制不行吗

不行,若没有STW机制,无法确保堆内存中某一时刻是静态的。无法判断出哪些对象是非垃圾对象那些是垃圾对象,这样就会出现判断不准问题,若是这样就无法确保内存分析的一致性且会大大增加垃圾回收算法的复杂度和实现难度。

如执行main线程时 在执行c = (a + b) *10 时发生了GC,在GC过程中对堆内存中对象进行了可达性分析,判断出math变量是非垃圾对象。继续进行分析时若没有STW机制,那么线程就会继续执行 ,线程结束时 意味着线程中局部变量如math出栈进行销毁。意味着math变量就成了垃圾对象与之前分析的冲突了,需要重新判断。

3. 程序计数器

每个线程都有一个自己的程序计数器,用来存放线程正在运行或马上运行的哪一行代码的位置或行号。字节码运行引擎执行程序,每执行一行代码,就会对程序计数器进行修改。

由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一个线程中的指令。因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰。所以程序计数器是每个线程所私有的。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

4. 方法区(元空间)

方法区与堆一样是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

运行时常量池

在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。若静态变量是Object对象,那么静态变量在方法区中存放的是对象在堆中的内存指针地址,如Math.class文件中User变量。

元空间大小参数设置:
jdk1.7 及以前:-XX:PermSize;-XX:MaxPermSize;
jdk1.8 以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSize ;如果不设置参数,默认21M。

方法区使用的是物理内存(直接内存)。若JVM不进行设置,当21M都是用满了后就会触发Full GC。

大的项目程序启动慢的情况下,可看下是否设置了方法区的大小。因为若是不设置,在程序启动时可能就会频发的发生full GC, 通过动态扩容方式再对方法区进行扩容,这样就会大大影响程序启动。这也是JVM优化的一点。

5. 本地方法栈

本地方法栈与Java栈的作用和原理非常相似。区别只不过是栈(虚拟机栈)是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。

根据上面分析可知,栈、本地方法栈、程序计数器是线程私有的,堆和元空间是线程共享的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值