JVM 之 运行时数据区(更新)

Stella981
• 阅读 534

第一篇 JVM 之 Class文件结构

JVM定义了一系列程序运行期间使用的运行时数据区(run-time data area)。这些数据区域中的一些随着JVM的启动而创建直到JVM的停止而销毁,而另一些则随着某个线程的创建而创建,随着线程的销毁而销毁。

为了能更直观的了解JVM的运行时数据区,我们先上张图来瞅瞅整个JVM内存的逻辑布局:

JVM 之 运行时数据区(更新)

以上仅是一个JVM运行时内存布局的概念模型,我们可以看出JVM主要定义5大类运行时数据区:

1)虚拟机栈,2)方法区,3)本地方法栈,4)堆,5)程序计数器。

当然除了这几个数据区还有1)运行时常量池,2)帧,3)本地变量表,4)操作数栈等数据区,下面我们都会一一分析。

对于上图个人觉得除了看到这几块区域,也没什么深入的细节上的信息了,而且很多情况下还会误导初学者,比如很多人认为虚拟机栈就那么一块区域,其实不然,而且虚拟机栈可以是不连续的。

因此作为一名程序员,个人一直认为代码是最好的注释和文档,一行代码胜过千言万语。因此为了更好的理解JVM的内存模型,我们下面用JAVA代码的形式来深入分析下。

零,JVM

像上一篇文章一样,我们还是从整体着手,然后到具体的细节逐个分析。下面就是一个可能的JVM内存的Java实现的类结构图:

JVM 之 运行时数据区(更新)

下面我们逐个列出每个数据区域的类实现来(注:该实现只是一个用来帮助理解的模型,会忽略很多细节,并且可能有不正确的地方,欢迎讨论)

//JVM.java
public class JVM {
    private Heap heap;
    private MethodArea methodArea;
    private Map<String, NativeMethodStack> nativeMethodStacks;
    private Map<String, VMStack> vmStacks;//假设线程名为键
    private Map<String, PCRegister> pcRegisters;//假设线程名为键
    //....getter, setter
}

上述代码很简单,清晰明了,不多说了。接下来我们就逐个深入分析。

===

一,Heap(堆)

堆是虚拟机中线程共享的一块数据区域,也就说所有的线程都可以访问这块区域的数据。同时堆是虚拟机中用来对象和数组分配内存的地方。堆的生命周期跟虚拟机一样,在虚拟机启动时创建,在虚拟机关闭时销毁。另外虚拟机中的对象无需显示的进行内存回收,JVM垃圾回收器会自动回收那些‘不用的’对象和数组。为更好的实现来及回收机制,通常JVM的实现会将堆内存划分为新生代(New Generation)和老年代(Tenured Generation),而新生代中又分为Eden Area和Survivor Area。下面我们看下堆内存的结构:

//堆
public class Heap {
    
    private long xms;//min heap size
    private long xmx;//max heap size
    private NewGeneration newGenration;//新生代
    private TenuredGeneration tenuredGeneration;//老年代
}
//新生代
public class NewGeneration {

    private int survivorRatio;// = (eden size / survivor size)
    private long xmn;//new generation
    private EdenArea eden;
    private SurvivorArea fromSurvivor;
    private SurvivorArea toSurvivor;
}

//老年代
public class TenuredGeneration {

    private byte[] memory;
}

//Eden
public class EdenArea {

    private byte[] memory;
}

//Survivor
public class SurvivorArea {

    private byte[] memory;
}

相信看到代码后你会感觉更加直观了。

二,VM Stack(虚拟机栈)

JVM虚拟机栈是线程私有数据区域,也就是每个线程都有一个自己的虚拟机栈内存,该内存随着线程的创建而创建,随着线程的销毁而销毁。虚拟机栈用来存储栈帧(frame),而栈帧会在下文详解。虚拟机栈类似于传统语言(如C)中的栈。它主要用来完成方法的调用和返回。由于虚拟机栈除了push和pop栈帧没有其他操作,所以虚拟机栈的内存可以是不连续的。下面是虚拟机栈的Java代码结构

import java.util.Stack;
public class VMStack {

    private Thread owner;
    private long stackDeep;//最大栈容量
    private Stack<Frame> frames;
}

废话不多说,继续往下说,既然提到栈帧,我们就看看什么是栈帧。

三,Frame(栈帧)

栈帧用来存储方法执行期间的数据和部分结果,同时还会执行动态链接,返回方法返回值,以及分派异常等动作。

每当有方法被调用时,就会创建一个新的栈帧,并压入执行该方法的线程的虚拟机栈中。当方法执行结束后,该栈帧就会被弹出并销毁,无论该方法是正常结束还是异常退出。每个栈帧内部都有一个本地变量表和操作数栈,以及一个指向当前方法所属类的运行时常量池的引用。(本地变量表,操作数栈,运行时常量池将在下文分析)

本地变量表和操作数栈的大小在编译期就会被确定,并且其大小由与该栈帧关联的方法的代码决定,另外他们的内存可以在方法被调用时再分配。

对每个线程,任意时刻都只会有一个栈帧(当前执行方法的栈帧)处于活动状态。这个栈帧被称为当前栈帧(current frame),相关联的方法叫做当前方法(current method),当前方法所定义的类叫做当前类(current class)。

如果当前方法调用另一个方法,那么就会创建一个新的栈帧,并成为当前栈帧。当当前方法返回时,当前栈帧就会将返回值传递回前一帧,该栈帧销毁,前一帧成为当前栈帧。

注意:某个线程创建的栈帧是该线程私有的,其他线程无法访问到。至于详细的方法调用和执行的过程我们在后续文章会进行更为详细的分析。

import java.lang.reflect.Method;
public class Frame {

    private LocalVariable[] localVariableTable;//本地变量表
    private OperandStack operandStack;//操作数栈
    private RuntimeConstantPool constantpool;//当前方法所属类的运行时常量池的引用
    private VMStack ownerStack;//所属虚拟机数栈
}

四,LocalVariable (局部变量表)

==========================

上面已经提到,每个栈帧都会包含一个局部变量表(局部变量数组),用来存储方法参数,局部变量等数据。而且局部变量表的大小由所属栈帧的关联方法的代码决定,并在编译器就确定了。

一个局部变量可以保存一个boolean,byte,char,short,int,float,reference或returnAddress的值,一对局部变量可以保存一个long或double的值。局部变量表由下标索引,索引从0开始,最大值不超过变量表大小。

long和double的值占用两个相邻的局部变量,而且不许用两个局部变量中较小的那个下表来索引该long或double值。

虚拟机使用局部变量表来进行方法调用过程中的参数传递。在静态方法调用时,所有的参数会按照顺序保存到局部变量表中从第0个位置开始的连续的局部变量。而调用实例方法时,局部变量表的第0个位置始终保存调用该方法的对象的引用(this),然后从第1个位置开始保存方法的参数。

public class LocalVariable {

    private Type type;
    private Slot slot;
    
    public enum Type{
        _boolean,
        _byte,
        _char,
        _short,
        _int,
        _float,
        _reference,
        _returnAddress,
        _long,
        _double
    }
    public static class Slot{
        private byte[] values;
    }
}

五,OperandStack(操作数栈)

第三部分已经提到,每个栈帧都包含一个后进先出的操作数栈,栈的深度在编译器便已确定,其大小由与该栈帧关联的方法体代码决定。

JVM提供了一些将常量或局部变量表中的变量加载到操作数栈中的指令,同样也提供了一些用来从操作数栈中获取数据,并操作他们,然后重新放回栈中的指令。操作数栈也会被用来准备传递给方法的参数以及接受方法的返回值。举个例子,iadd指令要求操作数栈顶预先有两个int值(其他指令压入),并将两个值弹出栈相加,让后将结果重新压回栈中。

操作数栈中的每个值都可以用来存储所有类型的数据,包括long,double。

操作数栈中的数据必须按照其类型进行适当的操作,比如我们不能将一个int值压入栈顶后按float类型弹出并操作。

public class OperandStack {

    private Slot[] values;
    
    public Slot pop(){return new Slot();}
    public void push(Slot slot){}
    
    public static class Slot{
        private byte[] v;
    }
}

六,Method Area(方法区)

同堆内存一样,方法区也是一个线程共享的数据区域。方法区有点类似传统编程语言(如C)中的用来存放编译代码的内存区域,或者类似于操作系统进程中的文本段。它主要保存着每个类的常量池,字段,方法,以及方法或构造器中的代码等数据(简单理解就是,每个类的Class文件加载,解析后就被存放在方法区中了)。

方法区的生命周期与虚拟机相同。尽管虚拟机中指出逻辑上方法区是堆内存的一部分,只是垃圾回收没有那么频繁,但是我们习惯上都会分开来讲。

更新:在HotSpot的实现中,方法区包含在了永久代中,同样在永久代中的还有一块区域我们可以称之为String literal pool(字符串常量池),该区域用于存放代码中的字符串字面量,以减少相同字符串对象创建带来的开销。最终内存布局参看下图。

public class PermGen{
       private MethodArea methodArea;
       private StringLiteralPool literalPool;
}

public class StringLiteralPool{
       private byte[] values;
}

public class MethodArea {

    private ClassInfo[] classes;
    
    public static class ClassInfo{
        private RuntimeConstantPool constantPool;
        private Field[] fields;
        private Method[] methods;
    }
}

这里我们没有用java.lang.Class是因为我们下面要讲到RuntimeConstantPool。其实方法区中存放的主要就是java.lang.Class实例集合。

七,Runtime Constant Pool(运行时常量池)

每个运行时常量池都是某个对应类或者接口的class文件中的常量池的运行时映射。一个运行时常量池就像是传统编程语言里面的符号表,不过它所包含的数据类型比符号表丰富。

所有的运行时常量池都分配在方法区中,某个类或者接口的运行时常量池会在该类或者接口被加载时创建。

public class RuntimeConstantPool {

    private ClassInfo clazz;
    private byte[] values;
}

八,PC Register(程序计数器)

或许这个该放在最前面分析的。Java的多线程机制离不开程序计数器,每个线程都有一个自己的程序计数器,以便完成不同线程上下文环境的切换。

任意时刻,如果当前方法不是native的,那么程序计数器都会保存当前被执行的指令的地址。如果当前方法是native的,那么程序计数器的值为undefined。程序计数器应该足够大以至于可以容纳returnAddress和特定平台的指针。

public class PCRegister {

    private Thread ownerThread;
    private byte[] values;
}

九,Native Method Stack (本地方法栈)

JVM的实现可以使用本地方法区来作为传统语言的栈来支持本地方法的调用(native方法)。本地方法栈同样可以用于其他语言(如C)写的虚拟机指令集的解释器实现。通常本地方法栈也是线程私有的数据区,生命周期同线程相同。

更新:引用http://blog.jamesdbloom.com/JVMInternals.html 文章中的图(很详细)

JVM 之 运行时数据区(更新)

点赞
收藏
评论区
推荐文章
【面试必会】最新阿里+头条+腾讯大厂Java笔试真题
一、内存与线程1、内存结构内存是计算机的重要部件之一,它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。JVM的内存结构规定Java程序在执行时内存的申请、划分、使用、回收的管理策略,通说来说JVM的内存管理指运行时数据区这一大块的管理。2、线程运行JVM中一个应用是可以有多个线程并行执行,线程
九路 九路
3年前
1 Java内存区域与内存溢出异常
1java虚拟机对内存的管理java虚拟机在执行java程序的时候把内存分为若干个不同的区,这些区各自有不同的用处,以及创建和销毁时间.有的区随着虚拟机的启动而启动,有的区则依赖用户线程的启动和结束而启动和结束.根据java虚拟机规范,java虚拟机将内存分为下面几个部分:如下图image(https://imghelloworld.o
zdd小小菜鸟 zdd小小菜鸟
1年前
JVM面试
JVM面试1.说一下JVM的主要组成部分?及其作用?tex类加载器(ClassLoader)运行时数据区(RuntimeDataArea)执行引擎(ExecutionEngine)本地库接口(Nativ
Stella981 Stella981
2年前
JVM内存区域划分
JVM内存区域划分一、JVM运行时数据区划分根据《Java虚拟机规范》JVM会把它管理的内存划分为若干个不同的数据区域,如下图所示:方法区、堆、栈(虚拟机栈、本地方法栈)、程序计数器。线程私有的意思是指,JVM每遇到一个新的线程就会为他们分配栈和程序计数器。!(https
Stella981 Stella981
2年前
JVM系列之:内存与垃圾回收篇(二)
JVM系列之:内存与垃圾回收篇(二)本篇内容概述:1、堆HeapArea2、方法区MethodArea3、运行时数据区总结4、对象的实例化内存布局和访问定位一、堆HeapArea1、堆的核心概念
Stella981 Stella981
2年前
JVM运行时数据区
Java虚拟机在执行Java程序的过程中会将其管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,及创建和销毁的时间,有些区域随虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束来建立和销毁。Java虚拟机所管理的内存包括以下几个运行时数据区域,如图(图片引自网络):!(https://static.oschina.net/uplo
Stella981 Stella981
2年前
ClassLoader解惑
一、什么是Classloader    一个Java程序要想运行起来,首先需要经过编译生成.class文件,然后创建一个运行环境(jvm)来加载字节码文件到内存运行,而.class文件是怎样被加载中jvm中的就是JavaClassloader所做的事情。    那么.class文件什么时候会被类加载器加载到j
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
2年前
JVM中的Stack和Frame
JVM执行Java程序时需要装载各种数据,比如类型信息(Class)、类型实例(Instance)、常量数据(Constant)、本地变量等。不同的数据存放在不同的内存区中,这些数据内存区称作“运行时数据区(RuntimeDataArea)”。运行时数据区有这样几个重要区:JVMStack(简称Stack或者虚拟机栈、线程栈、栈等),Frame(又称S
Stella981 Stella981
2年前
JVM类加载
运行时数据区java虚拟机定义了若干种程序运行时使用到的运行时数据区1.有一些是随虚拟机的启动而创建,随虚拟机的退出而销毁2.第二种则是与线程一一对应,随线程的开始和结束而创建和销毁。java虚拟机所管理的内存将会包括以下几个运行时数据区域!(http://static.oschina.net/uplo