Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:堆、方法区
区域 | 定义 | 异常 |
---|---|---|
程序计数器 | 可看作当前线程所执行的字节码的行号指示器 | 无 |
虚拟机栈 | 是方法执行的线程内存模型:每个方法执行时,JVM都会同步创建一个栈帧 | StackOverflowError OutOfMemoryError |
本地方法栈 | 基本同虚拟机栈,针对Native方法 | StackOverflowError OutOfMemoryError |
堆 | 存储对象实例 | OutOfMemoryError |
方法区 | 存储被JVM加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等 | OutOfMemoryError |
栈帧:存储局部变量、操作数栈、动态链接、方法出口等信息
局部变量表:存储编译期可知的各种JVM基本数据类型、对象引用、returnAddress类型。
存储空间以局部变量槽Slot表示,其中64位长度的long和double类型占用2个Slot,其余占用1个Slot。局部变量表所需的内存空间在编译期完成分配
Java堆是垃圾收集器管理的内存区域。
关注的点:JDK 7和JDK 8的更新。
属于方法区的一部分(常量池表内容,类加载后进入方法区,存放到运行时常量池)。
Class文件除了有类的版本、字符、方法、接口等描述信息外,还有一项信息是常量池表。
常量池表:存放编译期生成的各种字面量和符号引用。此部分内容在类加载后存放到方法区的运行时常量池中。
运行时常量池还会存储:由直接引用翻译出来的直接引用
运行时进入方法区运行时常量池的常量:如String的intern()方法
异常:OOM异常
字节码(.class文件)和虚拟机。
魔数:[1,4]
Class文件版本号:[5,8]
次版本号 + 主版本号
常量池:[9,?]
入口:u2类型,代表常量池容量计数值(从1开始)
比如:.class的第9个字节为0x16,即十进制的22,表示常量池中有21项常量[1,21],第0项常量空出来以做他用,具体参考“《深入理解Java虚拟机 第3版》6.3.2 常量池小节”
存储常量类型:字面量、符号引用。
字面量,比如文本字符串、final常量等
符号引用:package、类和接口的全限定名、字段的名称和描述符、方法的名称和描述符、方法句柄和方法类型、动态调用点和动态常量
常量如何存储:每一项常量都是一个表。具体请参考其他资料。
javap -verbose [class类名]:javap用于反解析当前类对应的code区(汇编指令)、本地变量表、异常表、代码行偏移量映射表、常量池等等信息。
访问标志:2字节。具体值为下述标志组合的位或结果
以下为部分访问标志,共16个标志位。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public |
ACC_FINAL | 0x0010 | 类是否被声明为final |
ACC_SUPER | 0x0020 | |
ACC_INTERFACE | 0x0200 | 接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型 |
ACC_SYNTHETIC | 0x1000 | 非用户代码产生 |
ACC_ANNOTATION | 0x2000 | 注解 |
ACC_ENUM | 0x4000 | 枚举 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
类索引、父类索引、接口索引集合
类和父类索引:u2类型的数据
接口索引集合:一组u2类型的数据的集合(同常量池,有一个计数值)
字段表集合
字段修饰可选:访问修饰符、实例变量还是类变量、可变性final、并发可见性volatile、是否可被序列化transient、字段数据类型、字段名称。每个都对应一个值。
字段表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
access_flags:“字段修饰可选”里面列举的值的位或。
name_index:对常量池项的引用。表示字段的简单名称
descriptor_index:字段或方法的描述符
有如下类:
package com.hzk;
public class User {
private String name;
private int age;
}
全限定名:com.hzk.User
简单名称:name和age
描述符:用于描述字段的数据类型、方法的参数列表和返回值。
方法表集合:基本同字段表,方法表结构依次为访问标志、名称索引、描述符索引、属性表集合。
方法表的属性集合,存储一个名称Code的属性,为Java程序方法体的代码经编译器处理之后变为字节码存储在Code属性内。
Java字节码的指令组成:操作码、操作数(零至多个参数)。由于Java虚拟机采用操作数栈,而非寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码。
定义:JVM把描述符的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,称为类加载机制。
说明:Class文件是一个二进制字节流,可能源自磁盘、网络或者内存等等。
加载 -> 连接(验证 -> 准备 -> 解析) -> 初始化 -> 使用 -> 卸载。
上述七个阶段的顺序并不完全是顺序开始的,其中:“加载、验证和准备、初始化、卸载”这五个阶段的顺序是确定的。而“解析”既可以在“初始化”前(正常流程)、也可以在“初始化”后(运行时绑定,动态绑定,晚期绑定)发生。
何时发生加载,JVM并没有强制约束;但是何时发生初始化,《Java虚拟机规范》规定有且只有六种情况必须立即对类进行初始化(而加载、验证和准备则需要在初始化前发生):
java.lang.reflect
进行反射调用除了上述六种情况以外,其他任何方式都不会触发初始化,称为被动引用。如下:请注意被动三,常量会存到调用类的常量池中。
// 被动一:子类引用父类的静态字段,不会触发子类的初始化
// 被动二:数组类T[],不会触发类T的初始化
// 被动三:常量在编译期会存入调用类的常量池中,不会触发定义常量的类的初始化
class Parent {
public static int value = 123;
public static final int ID = 111111;
}
class Child extends Parent {}
// 场景1:子类引用父类static字段,字段不会被初始化
main: System.out.println(Child.value);
// 场景2:类数组定义,不会触发Parent初始化
main: Parent[] parents = new Parent[10];
// 场景3:引用Parent.ID常量,不会触发定义常量类Parent的初始化
class Cal {
print: System.out.println(Parent.ID);
}
1)通过类的全限定名获取定义此类的二进制字节流
2)将二进制字节流的静态存储结构转化为方法区的运行时数据结构
3)内存中生成Class对象,作为方法区该类的各种数据的访问入口
要求:Class文件的字节流,数据符合JVM规范
类静态变量分配内存并设置类变量初始值(类型零值)
说明:概念上看,类变量所属内存位于方法区。JDK 7方法区的实现为永久代,实现符合概念;JDK 8方法区,类变量则会随着Class对象存放到堆中。
将常量池内的符号引用,替换为直接引用的过程。
可能发生在类加载时,可能发生在被使用时(运行时)
执行类构造器:(javac编译器的自动生成物)。主要是static(类变量、静态语句块)语句合并产生
说明:static语句按定义顺序逐条执行,静态语句块中只能访问定义在其之前的变量,定义在其之后变量,语句块可对其执行赋值操作,但不能访问。
说明2:
父类的先于子类执行;
接口实现类在初始化时不会执行接口的
接口执行不需要先执行父接口的
总之,对于接口而言,只有当使用到其定义的类变量,才会对其进行初始化
类加载器:实现“通过一个类的全限定名,获取该类的二进制字节流”动作的代码,为类加载器。其位于JVM之外实现。
类加载器 | 加载信息 | 说明 |
---|---|---|
启动类加载器 | -Xbootclasspath参数指定的路径中存放且能够被JVM识别的类; |
|
扩展类加载器 | java.ext.dirs系统变量指定路径中所有类库 |
Java 9后被模块化取代 |
应用程序/系统类加载器 | 程序中默认的类加载器 |
从JVM角度看,存在两种类加载器:启动类加载器(BootstrapClassLoader)、其他所有类加载器
BootstrapClassLoader,由C++实现,是虚拟机自身的一部分
其他类加载器,由Java实现,独立存在于虚拟机外部
双亲委派模型:除了顶层启动类加载器外,所有其他类加载器都有自己的父类加载器(组合而非继承)。
工作流程:如果一个类加载器收到了类加载的请求,首先将这个请求委托给父类加载器去完成。父类加载器无法完成时,子类加载器才会尝试去完成加载。
作用:保证任何一个类,由加载它的类加载器和类本身确立其唯一性
双亲委派模型并非一个具有强制性约束的模型。在Java 9模块化出现为止,出现过三次较大规模被破坏的情况。
破坏原因:父类加载器需要委托子类加载器去加载class文件。比如Driver接口,定义在JDK中,由启动类加载器加载,而各个数据库服务商的驱动,由系统类加载器加载。
):没有则进行类加载
方法引用计数算法
可达性分析算法
// GC Roots:虚拟机栈、本地方法栈、方法区(static、final)
虚拟机栈引用的对象(方法参数、局部变量、临时变量)
方法区中类静态属性引用的对象(static对象)
方法区中常量引用的对象(final对象)
本地方法栈引用的对象
JVM内部的引用
被同步锁(synchronized)持有的对象
JMXBean、JVMTI中注册的回调等
算法 | 实现 | 优点 | 缺点 |
---|---|---|---|
引用计数算法 | 对应中添加一个引用计数器 | 简单,效率搞 | 循环引用问题 |
可达性分析算法 | GC Roots的根对象 |
引用:强引用、软引用、弱引用、虚引用
可达性分析算法的标记:两次标记
没有与GC Roots相连的引用链,第一次标记,随后进行一次筛选(此对象是否有必要执行finalize()方法)
没有必要执行,进入“即将回收的集合”
有必要执行,对象被放到F-Queue的队列,稍后由Finalizer线程执行
有必要执行的条件:重写finalize,且没有被执行过
第二次标记:有必要执行的对象,在其finalize()方法中成功与GC Roots相连,就被移出“即将回收的集合”
方法区回收:不再使用的类、废弃常量
垃圾收集算法 | 实现 | 场景 | 缺点 |
---|---|---|---|
标记-清除 | 标记,清除 | 效率;内存碎片 | |
标记-复制 | 标记,复制,清除 | 新生代 | 内存利用率 |
标记-整理 | 标记,移动,清除 | stop the world |
分配担保: 标记复制。Survivor空间 < Monir回收后的存活对象,依赖其他区域(一般是老年代)进行分配担保
做法:新生代划分为8:1:1的Eden和Survivor*2。
命令 | 关键字 |
---|---|
jps | Java进程 |
jstat | 虚拟机进程,类加载 内存 垃圾收集 即时编译 |
jinfo | 虚拟机参数 |
jmap | 堆,转储快照 |
jhat | 堆转储快照,分析 |
jstack | 栈 |
可视化工具:JConsole、JHSDB、VisualVM、JMS