java2
JDK
java virtual machine 是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境
Java 虚拟机有自己的硬体架构(处理器、内存等)及对应的指令系统,屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台(设备和操作系统)上不加修改地运行
jre java runtime environment =JVM+lib
JDK java development kit 包含了JRE

- Class 文件: Java 文件需要先进行编译成字节码形式的 class文件
- 类加载子系统:类的加载,主要负责从文件系统,或者网络中加载 Class 信息,并与运行时数据区进行交互;
- 运行时数据区:主要包括五个小模块,Java 堆, Java 栈,本地方法栈,方法区,寄存器
- 执行引擎:分配给运行时数据区的字节码将由执行引擎执行,执行引擎读取字节码并逐个执行。垃圾回收器就是执行引擎的一部分;
- 本地方法接口:本机方法库进行交互,并提供执行引擎所需的本机库;
- 本地方法库:它是执行引擎所需的本机库的集合。
Demo.java通过JDK的javac命令,被编译为Demo.class字节码文件
JVM的类加载器加载Demo.class并将其投放到运行时数据区,
运行时数据区与执行引擎协作,执行引擎执行字节码文件,执行完毕后,会对运行时数据区的数据进行操作,比如说垃圾回收机制是执行引擎的一部分,垃圾回收机制,针对的是运行时数据区的堆空间
Class 文件
Class 文件中的魔数、主次版本号与常量池
Class文件数据类型
根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
-
无符号数:无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节;无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值;
-
表:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表
Class 文件是一组以(8位bit的)byte 字节为基础单位的二进制流。1byte=u1
Class 文件结构
Class 文件的头 4 个字节(u4)称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。所有 Class 文件,魔数均为 0xCAFEBABE。
次版本号与主版本号
JDK 1.8.0 版本的次版本号为 u2 大小,用字节码表示为 00 00,主版本号也是 u2 大小,用字节码表示为 00 34。
- 次版本号:JDK 版本的小版本号;
- 主版本号:JDK 版本的大版本号。
如果 Class 文件的开头 8 个字节分别为 CA FE BA BE 00 00 00 34,那么我们可以确定,这是一个 JVM 可识别的 Class 文件
常量池计数器与常量池
- 常量池计数器:记录常量池中的常量的数量。由于常量池中的常数的数量是不固定的,所以在常量池的入口放置了一个 u2 类型的数据,来代表常量池容器记数值(constant_pool_count)。常量池计数器也是无符号数类型数据。
- 常量池:Class 文件中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最多的数据项目之一,同时它还是 Class 文件中第一个出现的表类型数据项目。
常量池中主要存放着两种常量,字面量(Literal)和符号引用(Synbolic References)。
- 字面量包括:文本字符、声明为 final 的常量值、基础数据类型的值等;
- 符号引用包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
cp_info类型:cp_info 又可细分为 14 种结构类型
Class 文件的访问标志与索引
在常量池结束之后,紧接着的 2 个字节即u2代表访问标志(access_flags),访问标志用于识别一些类或接口层次的访问信息。

假设需要访问一个接口,那么此时访问标志 ACC_INTERFACE 的值为 true,标志对应的值为 0x0200。这样 JVM 虚拟机在处理访问的时候,就能够做到有据可依。
类索引与父类索引
类索引(this_class)和父类索引(super_class)都是一个 u2 大小的数据。
- 类索引:确定当前类的全限定名。
- 父类索引:确定当前类的父类的全限定名。
接口索引计数器与接口索引集合
父类索引后边紧跟的是接口索引计数器,接口索引计数器后边紧跟的是接口索引集合。类似于常量池计数器和常量池的关系,接口索引计数器记录的是接口索引集合中接口索引的数量。
接口索引计数器和接口索引集合皆为无符号数类型
- 接口索引计数器:代表了接口索引集合中接口的数量;
- 接口索引集合:按照当前类 implements(或当前接口extends)的接口的顺序,从左到右依次排列在接口索引集合中,此部分集合称为接口索引集合。
Class 文件中的字段表、方法表与属性表
- 字段表计数器(fields_count):记录字段表中字段的数量,为无符号数类型。
- 字段表(fields):字段表(fields)用于描述接口或者类中声明的变量。字段(field)包括类级变量(即静态变量)以及实例变量(即:非静态变量),但不包括在方法内部声明的局部变量。字段表为表类型结构。与其他计数器一样,字段表计数器(fields_count)是一个无符号数结构类型的数据,u2 大小。
字段(field)包括类级变量(即静态变量)以及实例变量(即:非静态变量),上图所示的一个 field_info 就代表了一个变量。为了表示一个变量,需要知道这个变量的修饰符,如 public,还需要知道这个变量的变量名称,因此一个 field_info 中存储了很多特征值,所有的特征值综合起来就完整的描述了一个变量。
- 方法表计数器(methods_count):记录方法表中字段的数量,为无符号数类型。
- 方法表(methods):存储了当前类或者当前接口中的 public 方法,protected 方法,default 方法,private 方法等。方法表为表结构类型。
- Class文件是通过Java文件编译而来的,如果文件中有方法,就会将方法的信息存储到方法表,并通过方法表计数器进行方法的计数。
public String get(String name) {
return "";
}
对于get 方法,method_info 中会存储如下信息:
- 方法的修饰符 public:还记的我们上节所讲述的 access_flag 吗?access_flag 对应表中有一个 ACC_PUBLIC 就代表了 public 修饰符,他对应的值为
0x0001,此处的标记也需要使用到这个表。都是通用的哦; - 方法名称:get 方法;
- 方法的参数:String name;
- 方法的返回值类型: String 类型的返回值。
- 属性表计数器(attributes_count):记录属性表中属性的数量,为无符号数类型。
- 属性表(attributes):属性表(attributes)与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不能识别的属性。
类加载子系统
Java 的动态类加载功能由类加载器子系统处理,处理过程的顺序为加载、链接和初始化
JVM 类加载器分类
BootStrap Class Loader,Extention Class Loader 和 Application Class Loade
启动(Bootstrap)类加载器也称为引导类加载器,该加载器是用本地代码实现的类加载器,它所加载的类库绝大多数都是出自 %JAVA_HOME%/lib 下面的核心类库,当然还有其他少部分所需类库
扩展类加载器是由 Sun 公司提供的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 %JAVA_HOME%/lib/ext 或者少数由系统变量 -Djava.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
系统类加载器是由 Sun 公司提供的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库加载到内存中。开发者可以直接使用系统类加载器。
JVM 双亲委派模型

双亲委派模型原理:
- 向上委托:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。如果父类加载器可以完成类加载任务,就成功返回;
- 向下委派:倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
JVM 中类加载的链接与初始化
链接(Linking)这一步,里边包含了三个更加细致的步骤,分别为验证(verify),准备(prepare)和解析(resolve)
链接-验证(verify)
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。
验证过程的主要验证信息:验证过程中,主要对三种类型的数据进行验证,分别是“元数据验证,字节码验证和符号引用验证”。具体内容请看下边的讲解。
元数据验证:
- 验证这个类是否有父类(除了 java.lang.Object 之外,所有类都应当有父类);
- 验证这个类是否继承了不允许被继承的类(被 final 修饰的类);
- 如果这个类不是抽象类,验证该类是否实现了其父类或接口之中所要求实现的所有方法;
- 验证类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。
字节码验证:字节码验证主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中;
- 保证跳转指令不会跳转到方法体以外的字节码指令上;
- 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。
符号引用验证:符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:
- 符号引用中通过字符串描述的全限定名是否能够找到对应的类;
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
- 符号引用中的类、字段、方法的访问性(private、default、protected、public)是否可被当前类访问。
链接-准备(prepare)
定义:准备阶段是正式为类变量分配内存并设置类变量默认值(通常情况下是数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。
链接-解析(resolve)
定义:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。
解析过程具体的解析内容:解析过程中,主要对如下4种类型的数据进行验证:
- 类或接口的解析;
- 字段解析;
- 类方法解析;
- 接口方法解析。
初始化
定义:进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
初始化顺序 父类静态变量然后父类静态代码块,子类静态(变量然后代码块),父类构造(代码块然后方法),子类构造(代码块然后方法)
运行时数据区

-
方法区(Method Area):所有的类级数据将存储在这里,包括静态变量。每个 JVM 只有一个方法区,它是一个共享资源;
-
堆区域(Heap Area):所有对象及其对应的实例变量和数组将存储在这里。每个 JVM 也只有一个堆区域。由于方法和堆区域共享多个线程的内存,所存储的数据不是线程安全的;
-
栈区(Stack Area):对于每个线程,将创建单独的运行时栈。对于每个方法调用,将在栈存储器中产生一个条目,称为栈帧。所有局部变量将在栈内存中创建。栈区域是线程安全的,因为它不共享资源;
-
PC寄存器(PC Registers):也称作程序计数器。每个线程都有单独的 PC 寄存器,用于保存当前执行指令的地址。一旦执行指令,PC 寄存器将被下一条指令更新;
-
本地方法栈(Native Method stacks):本地方法栈保存本地方法信息。对于每个线程,将创建一个单独的本地方法栈。
栈的基本介绍
基本概念:Java 栈有两个,分别是虚拟机栈和本地方法栈。这里以虚拟机栈为例,本地方法栈和虚拟机栈基本相同。
栈的特点:对于每个线程,将创建单独的运行时栈。对于每个方法调用,将在栈存储器中产生一个条目,称为栈帧。所有局部变量将在栈内存中创建。栈区域是线程安全的,因为它不共享资源。
- Java 虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭);
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常;
- Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法执行的同时会创建一个栈帧。对于我们来说,主要关注的栈内存,就是虚拟机栈中局部变量表部分。
- 从栈的特点的最后一点可以看到,开发者主要关注的是栈内存,而栈内存的消耗是因为每个方法执行的同时会创建一个栈帧,而占用空间最大的部分就是栈帧的局部变量表部分
栈帧
定义:栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的 java 虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
栈帧初始化大小:在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。 因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
栈帧结构:如下图所示,在一个线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
栈帧 - 局部变量表
在栈帧中,局部变量表占用了大部分的空间,那么接下来我们看下局部变量表的基本概念与特点。
基本概念:每个栈帧中都包含一组称为局部变量表的变量列表,用于存放方法参数和方法内部定义的局部变量。
特点:
- 局部变量表的容量以变量槽(Variable Slot)为最小单位;
- 在方法执行过程中,Java 虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程;
- 局部变量表中的 Slot 是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码程序计数器的值已经超过了某个变量的作用域,那么这个变量相应的 Slot 就可以交给其他变量去使用,节省栈空间。
栈帧 - 操作数栈
操作数栈也是栈帧中非常重要的结构,操作数栈不需要占用很大的空间,那么我们一起来看下操作数栈的作用及特点。
- 操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程;
- 操作数栈的每一个元素可以是任意的 Java 数据类型,32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2;
- 当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中。
栈帧 - 动态链接与返回地址
动态链接的基本概念及作用如下:
- 每个栈帧都包含一个指向运行时常量池(JVM 运行时数据区域)中该栈帧所属方法属性的引用,持有这个引用是为了支持方法调用过程中的动态链接。
- 在 Class 文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用(1. 类的全限定名,2. 字段名和属性,3. 方法名和属性),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态链接。
返回地址:返回地址代表的是方法执行结束,方法执行结束有两种方式,我们来具体看下栈帧中返回地址的作用:
- 当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(例如:return),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。
- 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是 Java 虚拟机内部产生的异常,还是代码中使用 throw 字节码指令产生的异常,只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
- 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等。
寄存器简介
寄存器( PC register )基本概念:每个线程启动的时候,都会创建一个 PC(Program Counter,程序计数器)寄存器。PC 寄存器里保存有当前正在执行的 JVM 指令的地址。
寄存器简介:
- 每一个线程都有它自己的 PC 寄存器,也是该线程启动时创建的。保存下一条将要执行的指令地址的寄存器是:PC 寄存器。PC 寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量;
- 每个线程都有一个寄存器,是线程私有的,其实就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,以及即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记;
- 这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 如果执行的是一个 Native 方法,那这个计数器是空的。
寄存器的特点
通过对寄存器的介绍,我们知道,寄存器器是用来存储指向下一条指令的地址,以及即将要执行的指令代码。我们来看下寄存器的特点:
- 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域; -
- 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致;
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 java 方法的 JVM 指令地址:或者,如果是在执行 native 方法,则是未指定值(undefined);
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成;
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一个条需要执行的字节码指令;
- 它是唯一一个在 Java 虚拟机规范中没有规定任何 OOM 情况的区域。
JVM 方法区
方法区,也称非堆(Non-Heap),是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field 等元数据对象、static-final 常量、static 变量、JIT 编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域 “运行时常量池”
方法区存放的数据
在讲解方法区内存放的数据之前,我们先通过示意图来直观的看下,方法区存放的数据与堆内存之间的关系

- 类型全限定名:全限定名为 package 路径与类名称组合起来的路径;
- 类型的直接超类的全限定名:父类或超类的全限定名;
- 类型是类类型还是接口类型:判定当前类是 Class 还是接口 Interface;
- 类型的访问修饰符:判断修饰符,如 pulic,private 等;
- 类型的常量池:这部分会在下文进行讲解;
- 字段信息:类中字段的信息;
- 方法信息:类中方法的信息;
- 静态变量:类中的静态变量信息;
- 一个到类 ClassLoader 的引用:对 ClassLoader 的引用,这个引用指向对内存;
- 一个到 Class 类的引用:对对象实例的引用,这个引用指向对内存。
运行时常量池
Class 文件中的常量池:
在 Class 文件结构中,最头的 4 个字节用于存储 Megic Number,用于确定一个文件是否能被 JVM 接受,再接着 4 个字节用于存储版本号,前 2 个字节存储次版本号,后 2 个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个 u2 类型的数据 (constant_pool_count) 存储常量池容量计数值。
常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)。更加具体的知识,同学们可以翻看之前相关的小节内容。
其实 Class 文件中的常量池与运行时常量池的关系非常容易理解,Class 文件中的常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。简单总结来说,编译器使用 Class 文件中的常量池,运行期使用运行时常量池。
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是 String 类的 intern() 方法。
常量池的优势
- 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
- 节省运行时间:比较字符串时,
==比equals ()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
.8 版本中移除了方法区并使用 metaspace(元数据空间)作为替代实现。metaspace 占用系统内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间。但这不意味着我们不对方法区进行限制,如果方法区无限膨胀,最终会导致系统崩溃。
JVM 堆内存
堆内存结构
堆内存是运行时数据区中非常重要的结构,实例对象会存放于堆内存中。在后续小节中,我们讲解 GC 垃圾回收器,绝大多数的垃圾回收都发生在堆内存中,因此对于 JVM 来说,堆内存占据着十分重要的且不可替代的位置。

- 堆内存从结构上来说分为年轻代(YoungGen)和老年代(OldGen)两部分;
- 年轻代(YoungGen)又可以分为生成区(Eden)和幸存者区(Survivor)两部分;
- 幸存者区(Survivor)又可细分为 S0区(from space)和 S1区 (to space)两部分。
什么是堆内存
物理层面:从物理层面(硬件层面)来说,当 Java 程序开始运行时,JVM 会从操作系统获取一些内存。JVM 使用这些内存,这些内存的一部分就是堆内存。
Java层面:从开发层面来说,堆内存通常在存储地址的底层,向上排列。当一个对象通过 new 关键字或通过其他方式创建后,对象从堆中获得内存。当对象不再使用了,被当做垃圾回收掉后,这些内存又重新回到堆内存中。
总结来说,堆内存是JVM启动时,从操作系统获取的一片内存空间,他主要用于存放实例对象本身,创建完成的对象会放置到堆内存中。
堆内存的分代概念
从上文堆内存的结构图中,我们看到了比较多的JVM堆内存中的专有名词,比如:年轻代,老年代。那么对于堆内存来说,分代是什么意思呢?为什么要进行分代呢?
分代:将堆内存从概念层面进行模块划分,总体分为两大部分,年轻代和老年代。从物理层面将堆内存进行内存容量划分,一部分分给年轻代,一部分分给老年代。这就是我们所说的分代。
分代的意义:易于堆内存分类管理,易于垃圾回收。类似于我们经常使用的 Windows 操作系统,我们会将物理磁盘划出一部分存储空间作为用户系统安装盘(如 C 盘),我们还极大可能将剩余的磁盘空间划分为 C, D, E 等磁盘,用于存储同一类型的数据。
-
易于管理:对于堆空间的分代也是如此,比如新创建的对象会进入年轻代(YoungGen)的生成区(Eden),生命周期未结束的且可达的对象,在经历多次垃圾回收之后,会存放入老年代(OldGen),这就是分类管理;
-
易于垃圾回收:将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
堆内存结构详解
堆内存每个模块之间的关系及各自的特点概述如下:
- JVM 内存划分为堆内存和非堆内存,堆内存分为年轻代(YoungGen)、老年代(OldGen);
- 年轻代又分为 Eden 和 Survivor 区。Survivor 区由 FromSpace 和 ToSpace 组成。Eden 区占大容量,Survivor 两个区占小容量,默认比例是 8:1:1;
- 堆内存存放的是对象,垃圾收集器就是收集这些对象,然后根据 GC 算法回收;
- 新生成的对象首先放到年轻代 Eden 区,当 Eden 空间满了,触发 Minor GC,存活下来的对象移动到Survivor0 区,Survivor0 区满后触发执行 Minor GC,Survivor0 区存活对象移动到 Suvivor1 区,这样保证了一段时间内总有一个 survivor 区为空。经过多次 Minor GC 仍然存活的对象移动到老年代;
- 老年代存储长期存活的对象,GC 期间会停止所有线程等待 GC 完成,所以对响应要求高的应用尽量减少发生 Major GC,避免响应超时。
JVM 中堆的对象转移与年龄判断
垃圾回收器
可达性分析法基本原理
方法原理:通过一系列称为"GC Roots"的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链(即GC Roots到对象不可达时),则证明此对象是不可用的。
可以作为 GCRoots 的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象;
- 方法区中的类静态变量属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(Native方法)引用的对象。
GC Roots 四种类型解释:从上图中,我们可以看到四种 GC Roots。这里我们对这四种 GC Roots 做一下更为细致的解释。
-
虚拟机栈中的引用的对象:我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的;
-
全局的静态的对象:也就是使用了 static 关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为 GC Roots 是必须的;
-
常量引用:就是使用了 static final 关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为 GC Roots;
-
Native 方法引用对象:这一种是在使用 JNI 技术时,有时候单纯的 Java 代码并不能满足我们的需求,我们可能需要在 Java 中调用 C 或 C++ 的代码,因此会使用 native 方法,JVM 内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为 GC Roots。
可达性分析的四种引用类型
强引用;
软引用;
弱引用;
虚引用。
JVM 常见的垃圾回收算法
- 标记-清除(Mark-Sweep)算法;
- 复制(coping)算法;
- 标记-整理(Mark-Compact)算法;
- 分代收集算法。
准确来说,垃圾回收只有 3 种算法:
- 标记-清除(Mark-Sweep)算法;
- 复制(coping)算法;
- 标记-整理(Mark-Compact)算法。
7 种垃圾收集器:Serial 收集器,Parnew 收集器,Parallel Scavenge 收集器,Serial Old 收集器,Parallel Old 收集器,CMS 收集器和G1 收集器。
其中专门针对年轻代的收集器有 Serial 收集器,Parnew 收集器和 Parallel Scavenge 收集器;专门作用于老年代的收集器有Serial Old 收集器,Parallel Old 收集器和 CMS 收集器;而 G1 收集器即能够作用于年轻代,也能够作用于老年代。
执行引擎


-
解释器:解释器是作用于字节码的解释。解释器的缺点是当一个方法被调用多次时,每次都需要一个新的解释;
-
JIT 编译器:JIT 编译器消除了解释器的缺点。执行引擎将在转换字节码时使用解释器的帮助,但是当它发现重复的代码时,将使用 JIT 编译器,这提高了系统的性能;
-
垃圾回收器(Garbage Collector):收集和删除未引用的对象。可以通过调用
System.gc()触发垃圾收集。

JVM常用参数配置
浙公网安备 33010602011771号