前言简介
class文件是源代码经过编译后的一种平台中立的格式
里面包含了虚拟机运行所需要的所有信息,相当于 JVM的机器语言
JVM全称是Java Virtual Machine ,既然是虚拟机,他终归要运行在物理机上
在操作系统中体现出来的也就是一个进程
操作系统会给他分配资源,割一块内存作为他的地盘
class文件是静态的,想要运行程序,JVM需要将class文件中的信息加载到加载到他的地盘
然后处理他可以处理的数据类型的数据
JVM将这块内存按照功能进行了更细的划分,不过终究是一个规范,虚拟机的厂商在实现的时候仍旧有很大的自由度
接下来将会从两个方面 虚拟机可以处理的数据类型 以及 运行时的数据区的内存模型
对虚拟机进行简单的介绍
数据类型
数据类型分类
虚拟机可以处理的数据类型分为:基本类型和引用类型两类
所以对于值,也就存在基本类型值 和 引用值 两种类型的值
|
基本类型又分为 数值类型/boolean/returnAddress 三种
数值类型又分为整数类型和浮点数类型
整数类型(byte short int long char) 与浮点数(float double) 与java语言中的值域在任何地方都是一致的,比如 取值范围表示含义
boolean编译后使用Java虚拟机中的int 数据类型代替,不过Java虚拟机支持boolean类型的数组,0表示false 1表示true
returnAddress 在Java语言中并不存在相应的类型 也就是程序员不能使用这个类型 ,而且也无法在程序运行期间更改
|
引用类型分为三种 类类型 接口类型 数组类型 值都是动态创建对象的引用 类类型的值是对类实例的引用 数组类型的值是对数组对象的引用 接口类型的值 是对实现了该接口的某个类实例的引用 另外还有一个特殊的引用null |
取值范围
byte | 8位 有符号 二进制补码整数 默认值零(-2^7到2^7-1 包括两端的值在内) |
short | 16位 有符号 二进制补码整数 默认值零(-2^15到2^15-1 包括两端的值在内) |
int |
32位 有符号 二进制补码整数 默认值零(-2^31到2^31-1 包括两端的值在内)
|
long |
64位 有符号 二进制补码整数 默认值零(-2^64到2^64-1 包括两端的值在内)
|
char | 16位 无符号 Unicode字符 默认值为null的码点 ‘/u0000’ (0 到2^16-1 包括两端的值在内) |
float | 32位 IEEE754标准单精度浮点数 默认值正数0 |
double | 64位 IEEE754标准双精度浮点数 默认值正数0 |
returnAddress | 同一方法中某操作码的地址 |
reference | 堆中堆某对象的引用,或者是null |
内存结构
内存结构组成部分
上面说过,程序运行,必然需要装载数据到内存
class文件会经由classLoader加载到JVM的运行时数据区域
JVM的内存结构为下图右侧部分
从图中可以看得出来
大致分为 方法区/堆/程序计数器/虚拟机栈/本地方法栈 五部分
接下来逐个进行介绍
ps:在抽象一点,逻辑上来说其实可以理解为堆/栈/程序计数器 三类 程序的运行 , 需要数据还需要方法,还需要说明从哪个指令位置开始执行 程序计数器就是指向要执行的指令地址,标志从哪个位置开始执行 栈是方法调用概念的具体化数据结构,描述了怎么执行 堆用于保存程序运行需要用到的数据对象等,描述了 执行什么,操作什么 |
内存结构各部分详情
一个运行时的java虚拟机实例就是负责一个java程序的运行
启动一个Java程序一个虚拟机实例也就诞生,当这个Java程序关闭,这个虚拟机实例就销毁
每个Java程序都运行于他自己的Java虚拟机实例中
一个虚拟机实例中,堆和方法区是这个Java程序所有线程共享的
Java虚拟机栈和本地方法栈和程序计数器 是线程隔离独有的
下面所有说到的都是基于一个Java程序内的场景
方法区
方法区是可供各个线程共享的运行时内存区域,存储了每一个类的结构信息,如: 运行时常量池/字段和方法数据/构造函数和普通方法的字节码内容/类实例接口初始化时用到的特殊方法 方法区在虚拟机启动的时候创建 方法区也可以被垃圾收集 方法区大小不必是固定的 可以根据需要动态扩展 方法区空间也不必是连续的 具体存储的信息包括: |
类型信息 类的全限定名 类型的直接超类全限定名
类型 类还是接口
访问修饰符
直接超接口的全限定名
|
字段信息 字段名 字段类型 字段的修饰符 |
方法信息 方法名 方法的返回类型 方法的参数数量和类型 方法的修饰符 方法的字节码(有方法体的) 操作数栈和该方法栈帧中的局部变量表 的大小(其实也还是class文件属性表的内容 静态的) |
常量池–下面的运行时常量池区域 |
除了常量以外的所有类变量 类变量是所有类实例共享的,即使没有任何类实例,他也可以被访问,这些变量仅仅和类有关 所以 类变量总是作为类型信息的一部分存储在方法区 除了在类中声明的编译时常量外,虚拟机使用某个类之前 必须在方法区中为这些类分配空间 编译时常量指的是final声明以及用编译时已知的值初始化的类变量 这种和一般的类变量还不一样,每个使用编译时常量的类型,都会复制他的所有常量到自己的常量池中 或者嵌入到他的字节码流中 说白了对于这种值不变的,直接复制过去 |
类ClassLoader的引用/Class类的引用 每个类被装载后都必须跟踪他是由哪个类加载器加载的 对于每个被装载的类型,不管是类还是接口,虚拟机都会相应的为他创建一个java.lang.Class类的实例 而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来 |
运行时常量池
运行时常量池属于方法区的一部分
class文件中每一个类或者接口的常量池表 constant_pool table 运行时的表示形式
只需要记住与class文件中的constant_pool相对应即可理解所包含的内容
包括了若干种不同的常量
从编译器可知的数值字面量到必须在运行期解析后才能获得的方法或字段引用
运行时常量在Java虚拟机的方法区分配 加载类或者接口到虚拟机后,就创建对应的运行时常量池
|
总结:
所有的类型信息,静态数据信息,都加载到方法区中
另外类加载器以及当前Class对象这种运行时必须的信息,也被保存在方法区
Java堆
一个java程序独占一个虚拟机实例,也就是每个java程序一个独立的堆空间 但是对于同一个java程序 堆是各个线程共享的运行时内存区域 是所有类实例和数组对象分配内存的区域
Java堆在虚拟机启动时就被创建了
存储了被自动内存管理系统 也就是GC( garbage Collector) 所管理的各种对象
这些受管理的对象不需要也也不能显式的销毁
之所以这么说是因为有分配新对象的指令,却没有释放内存的指令,所以就不能显式的销毁
堆是垃圾收集器工作的主要区域
堆空间不必连续也可以动态扩展或者收缩
对象的内部表示形式,规范并没有规定 实现者可以按需发挥
|
Java虚拟机栈
java虚拟机栈,是对方法调用这一抽象概念的具体化描述,方法执行的内存模型
启动一个新线程 Java虚拟机就会为他分配一个Java栈,用于保存栈帧
虚拟机只会直接对Java栈执行两种操作 以栈帧为单位的出栈或者入栈
每个方法在执行的同时都会创建一个栈帧 栈帧用于存储局部变量表 操作数栈 动态链接 方法出口等信息
每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程
栈上所有的数据都是线程私有的任何线程都不能访问另一个线程的栈数据
也就是说,完全无需考虑多线程情况下数据的访问同步问题
当一个线程调用另一个方法时,方法的局部变量保存在调用线程的Java虚拟机栈的栈帧中
只有一个线程总是能访问那些局部变量即调用方法的线程
|
栈帧
三部分组成: 局部变量表 操作数栈 以及栈帧数据区
当虚拟机调用一个方法时,从对应类的类型信息中得到此方法的局部变量表和操作数栈的大小(code 属性)
并以此分配栈帧内存,然后压入Java栈中
栈帧随着方法的调用而创建随着方法结束而销毁,无论方法正常完成还是异常完成都算作方法结束
局部变量表 长度由编译期决定,通过方法code属性提供 除了long和double 使用两个局部变量外,其余类型均为一个
保存了对应方法的参数和局部变量
操作数栈 后进先出,操作数栈最大深度编译期决定通过code属性保存提供 每个位置可以保存一个java虚拟机中定义的任意数据类型的值包括long double
操作数栈作为虚拟机的工作区,大多数指令都要从这里弹出数据执行计算然后把结果压回操作数栈
栈帧数据区 除了局部变量和操作数栈外,还需要一些其他的数据,比如 常量池的入口信息,每当虚拟机要执行某个需要用到常量池数据的指令时
都会通过栈帧数据区中指向常量池的指针来访问他
|
本地方法栈
本地方法栈并不是虚拟机明确定义的,是可选的
Java虚拟机实现可能会使用到传统的栈(通常称之为C stack) 来支持native方法(指 用Java以外的其他语言编写的方法)
这个栈就是本地方法栈 当Java虚拟机使用其他语言比如C语言,来实现指令集解释器的时候,也可以使用本地方法栈
如果Java虚拟机本身不支持native方法,或是本身不依赖传统栈,那么可以不提供本地方法栈
如果支持本地方法栈 那这个栈一般会在创建线程的时候按线程分配
|
程序计数器
程序计数器 又叫做 PC寄存器 PC为program counter
不管称呼如何,其本意是保存当前正在执行的指令的地址,
此处,我们可以看做是当前线程所执行的字节码的行号指示器
也就是程序的运行完全依赖PC寄存器,需要依靠他获取下一条需要执行的字节码指令
JVM的多线程时通过线程轮流切换并分配处理器执行时间片的方式实现的,在任何一个确定的时刻
一个处理器(一个内核) 都只能执行一条线程中的指令,为了线程切换后能恢复到正确的位置
所以每个线程都需要一个独立的程序计数器,所以程序计数器是线程私有的 线程启动时创建
如果执行的是Java方法,值为正在执行的虚拟机的字节码指令地址
如果是Native方法,计数器值为空 Undefined ,此区域 没有OOM
|
直接内存
直接内存并不是虚拟机运行时的数据区,也不是Java虚拟机规范中定义的内存区
但是这部分内存也被频繁的调用,也可能导致OOM
是引入NIO后,引入的一种基于通道与缓冲区的IO方式
可以使用native 函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作
能在一些场景中显著提高性能
既然不属于java堆,自然不受制于Java堆大小的限制,但是,必须运行于物理机
自然受制于本机总内存大小
|
总结
JVM运行时的内存结构,就是为了执行字节码文件,而将class文件中的信息加载到内存中的一个逻辑映射
class文件是源代码的静态抽象的数据结构描述
运行时内存结构是对于class文件的执行行为的结构描述
以上所有的要求说明都是属于规范上的并不要求所有的实现与规范中定义的抽象元素完全的对应起来
抽象的内部组件和行为的描述,仅仅是定义Java虚拟机所应该呈现出来的外部行为
也就是说,一个具体的虚拟机实现,可能与我们说过的规范相同,也可能与规范有出入
但是只要他的外部行为是一致的,正确识别class文件,遵守class文件中包含的Java代码的语义,能够按照规定所需要呈现出来的行为结果
执行字节码文件即可
至于方法区到底应该如何分配空间,对象的内部表现形式如何,垃圾收集器如何运作,如何加载类都是由设计者来决定实现的.
举一个浅显的例子
你去超市购物,为了方便携带,你可能会按照他们的形状或者类别组织放到购物袋里面
比如 生鲜放到一个袋子,零食放到一个袋子,或者放置稍大商品的购物袋里面的缝隙处,放置一些小的商品
这是属于所有商品的静态描述组织
回到家需要做饭,可能你会把鱼拿出来放到盘子里,可能你会把青菜放到水槽中浸泡清洗,然后你可能会准备作料,洗锅准备做菜等等
一切都按照你下厨的习惯来放置食材以及步骤进行做菜
这就是属于动态执行行为的结构描述
我们的内存结构 程序计数器 堆 栈 就是对于代码执行行为过程的一种描述
可以理解你想要先做那道菜? 程序计数器((如果把想要做的菜都列一个清单,程序计数器就是从什么位置开始做,就是先做哪道菜)
都有哪些食材? 台面上有青菜 鱼 豆腐… 这都是存放在堆中
具体的怎么做? 红烧还是清蒸?这些具体的行为封装在虚拟机栈的栈帧中 每次做一道菜就是入栈,做好了刷锅就是出栈
而每道菜所需要的调味料和配菜可能是独有的,不能乱放,这些就相当于栈帧中的局部变量和操作数栈
|
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/15579.html