Java虚拟机是相对于物理机的概念,也就是对现有冯诺依曼体系结构的硬件系统的抽象,现有计算机硬件系统是由输入,运算器、控制器、存储器和输出构成的。Java虚拟机则是建立在硬件系统之上,提供自己的规范,使在虚拟机上运行的程序具有跨平台、自动回收内存等诸多特性。
虚拟机内存划分概述
Java虚拟机对内存划分为5种不同的区域,分别是栈内存、本地方法栈、堆内存、方法区、程序计数器。
栈内存
栈内存是线程私有的,它随着线程的创建而创建,随着线程的销毁而销毁,在执行方法时会创建栈帧,栈帧中存放的是局部变量表(方法的参数以及局部变量)、操作数栈(指令所操作的数据存放处)等信息。方法的调用就是栈帧入栈的过程。
本地方法栈
执行Native方法时所用到的存储单元
堆内存
堆内存是线程共享的,当虚拟机启动时便会创建。该内存区域是用来存放对象实例的,也是垃圾收集器管理的主要区域。
方法区
方法区是线程共享的内存区域,它存储的主要是在类加载时产生的数据,可以理解为是存储Class文件信息或者是类相关信息的。所以编译后的字节码、常量、静态变量等与对象无关只有类有关的数据都放在这里。
程序计数器
程序计数器作为线程执行时的行号指示器,分支、跳转、循环都是通过它实现的,在任一时刻,一个处理器核都只会执行一条线程中的指令,每一个核对应一个程序计数器。
Class文件详解
Java原码中的各种变量,关键字和运算符号的语义最终都是由多条字节码命令组合而成的,而ava原码通过编译器编译为虚拟机可加载的Class文件,Class文件中包含了Java字节码和符号表和其他辅助信息。
Class文件是一组以8位字节为原子单位的二进制流,各个数据严格按照规定的顺序紧凑的排列(顺序中包含着信息),而Class文件只包含两种数据类型:
无符号数 -- 表示数字、索引引用、数量值、或按照UTF-8表示字符串,通过u1、u2、u4表示1字节2字节4字节
表 -- 是由多个无符号数组成的复合结构,所有表都以"_info"结尾
类型 | 名称 | 描述 | 数量 |
---|---|---|---|
u4 | magic | 魔数 | 1 |
u2 | minor_version | 副版本号 | 1 |
u2 | major | 主版本号 | 1 |
u2 | constant_pool_count | 常量池数量 | 1 |
cp_info | constant_pool | 常量池 | 常量池数量-1 |
u2 | access_flags | 访问标识 | 1 |
u2 | this_class | 类索引 | 2 |
u2 | super_class | 父类索引 | 2 |
u2 | interfaces_count | 接口数量 | 1 |
u2 | interfaces | 接口索引集合 | 接口数量 |
u2 | fields_count | 字段数量 | 1 |
field_info | fields | 字段表 | 字段数量 |
u2 | methods_counts | 方法数量 | 1 |
method_info | methods | 方法表 | 方法数量 |
u2 | attributes_count | 属性数量 | 1 |
attributes_info | attributes | 属性表 | 属性数量 |
魔数以及版本号
Class文件的前4个字节是魔数它的唯一作用就是确定这个文件是Class文件,接下来2个字节是副版本号,再接下来两个字节是主版本号,用来确定版本号。
常量池数量
由于每个Class文件的常量数量不是固定的,所以紧接着主版本号后面的是常量池数量,用来确定常量的数量
常量池
根据常量池的数量,后面是常量的具体内容,常量池中包含两大类常量:
字面量:字面量就是没有特殊意义的常量,就是用来表示常量,比如文本字符串(字段名等),或是被final修饰的常量值等
符号引用包括以下三类
类和接口的全限定名(用来确定类和接口)
字段名称和描述符(用来确定字段及字段的其他信息)
方法名称和描述符(用来确定字段及字段的其他信息)
类型 | 标识 | 描述 |
---|---|---|
CONSTONT_utf8_info | 1 | utf8编码字符串 |
CONSTONT_Integer_info | 3 | 整型字面量 |
CONSTONT_Float_info | 4 | 浮点型字面量 |
CONSTONT_Long_info | 5 | 长整型字面量 |
CONSTONT_Double_info | 6 | 双精度浮点型字面量 |
CONSTONT_Class_info | 7 | 类或者接口的符号引用 |
CONSTONT_String_info | 8 | 字符串类型字面量 |
CONSTONT_Fieldref_info | 9 | 字段的符号引用 |
CONSTONT_Methodref_info | 10 | 方法的符号应用 |
CONSTONT_InterfaceMethodref_info | 11 | 接口方法的符号应用 |
CONSTONT_NameAndType_info | 12 | 字段或方法的部分引用 |
CONSTONT_MethodHandle_info | 15 | 方法的句柄 |
CONSTONT_MethodType_info | 16 | 方法的类型 |
CONSTONT_InvokeDynamic_info | 18 | 动态方法调用点 |
常量池中的数据是以_info结尾,前面讲到Class文件只有两种类型,无符号数和表,而常量池的数据全部都是表,也就是由多个无符号数组成的复合结构。
常量池是提供其他表进行引用的,一个class文件中的所有出现的字符都该在常量池中显示
访问标识
在常量池结束之后,接着的是2个字节的访问标识,用于识别该文件是类还是接口、是否为public、是否为abstract等信息
类索引、父类索引、接口索引集合
在访问标识后的为类索引,是这个类的的全限定名,指向常量池中的CONSTONT_Class_info
字段表和方法表
字段表用于表述接口或者类中声明的变量。
类型 | 名称 | 描述 |
---|---|---|
u2 | access_flags | 描述字段、方法作用域等信息 |
u2 | name_index | 字段、方法简单名称常 |
u2 | descriptor_index | 字段、方法描述符 |
u2 | attributes_count | 属性数量 |
attributes_info | attributes | 属性表 |
属性表
属性表的作用是对其他表额外进行描述,下面是一些常用的属性。
名称 | 使用位置 | 描述 |
---|---|---|
code | 方法表 | 编译后的字节码指令 |
Exceptions | 方法表 | 方法抛出的异常 |
其中code属性非常重要,如果把Java程序中的信息分为代码和元数据两部分,那么Code属性就是用来描述代码的,其他所有数据项都是用来描述元数据。
虚拟机类加载过程
虚拟机类加载过程就是将原来磁盘中的代码加载到内存中。
类加载过程生命周期
类加载的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。
其中加载、验证、准备、初始化、卸载这五个过程顺序是确定的,而解析阶段有可能在初始化前也有可能在初始化后,在初始化前的解析是静态绑定(前期绑定),也就是可以在编译时确定,而初始化后的解析是动态绑定(后期绑定)。
加载
- 通过类的全限定名来获取类的二进制流
- 将类加载到方法区中
- 生成这个类的Class对象,获取方法区数据的入口(反射时会使用的)
验证
验证合法性
准备
分配类变量内存,并且赋初值
解析
将运行时常量池(类文件中的常量池加载到方法区)的符号引用替换为直接引用(内存地址)。
初始化
执行
类加载器
上面的生命周期是由类加载器完成的,对于任何一个类,都需要通过类加载器和这个类来确定在JVM中的唯一性。(同一个类不同类加载器加载后的Class对象的equals()方法返回false)
双亲委派机制
如果有一个类加载器收到加载请求,它首先会请求父加载器去加载,所以会导致所有加载请求都会到顶层的启动类加载器去;当父加载器无法完成加载请求(搜索范围没有这个类),就传递给子加载器加载。
字节码执行过程
字节码源代码进过编译又经过类加载器加载到方法区中的,字节码的执行代表着程序运行时一个方法的执行。
运行时栈帧
栈帧是方法执行时的数据结构,主要包括:局部变量表、操作数栈、动态连接、方法返回地址等。
局部变量表
用来存放方法参数和方法的变量的存储空间。它的大小在编译期间就已经确定,存放到Code属性中。
操作数栈
当方法开始执行时,操作数栈是空的,方法执行过程根据字节码会往操作数栈中写入和提取内容。
动态连接
每个栈帧都包含一个指向运行时常量池中所属方法的引用,这个引用是为了完成方法调用过程中的动态连接(确定方法),其中静态方法和私有方法是编译器可知,运行期不可变的。所以在编译期就可以确定。如重载就是需要动态连接来确认的
方法返回地址
就是方法的返回地址。。。。。
垃圾回收机制
垃圾回收是回收堆内存中不使用的对象。
判断对象是否存活
- 引用计数算法
每当有一个地方引用对象就将计数加1,当引用失效就减1,当计数器为零就说明对象不再被使用了。(无法解决对象循环引用) - 可达性算法
通过一系列GC Root 对象作为起点,当一个对象到GC Root 没有任何引用链相连就证明对象不可使用
垃圾收集算法
- 标记清除算法
标记需要回收的对象,之后统一回收。(会产生内存碎片) - 复制算法
将内存二等分,每次使用一半,当一半内存使用完之后触发复制算法,将存活的对象复制到另外一半中。(内存缩小到原来的一半)。
适用于新生代,大量对象死去,少量存活,移动很少的对象。 - 标记整理
标记需要回收的对象,将所有存活对象移动到一端,清除边界意外的内存。