深色模式
Java虚拟机(JVM)相关知识
JVM 类加载机制
类文件的结构
类文件结构
magic
:魔数 前4个字节用来检验这个文件是否为一个Java虚拟机可以接受的Class文件,它的内容是CAFABABE
(咖啡宝贝?)。minor_version
:次版本major_version
:主版本constant_pool_count
:常量池计数constant_pool
:常量池表类加载后,这里的常量会进入方法区,位于该类的类型常量池。
- 字面量
- 文本字符串
final
常量值
- 符号引用
- 类与接口的全限定名
- 字段的名称与描述符
- 方法的名称与描述符
- 字面量
access_flags
:访问标志ACC_PUBLIC
:可以被包的类外访问。0x0001ACC_FINAL
:不允许有子类。0x0010ACC_SUPER
:当用到 invokespecial 指令时,需要特殊处理的父类方法。0x0020ACC_INTERFACE
:接口类型。0x0200ACC_ABSTRACT
:抽象类型,不能被实例化。0x0400ACC_SYNTHETIC
:标识并非 Java 源码生成的代码。0x1000ACC_ANNOTATION
:注解类型。0x2000ACC_ENUM
:枚举类型。0x4000
this_class
:类索引super_class
:父类索引interfaces_count
:接口计数interface
:接口索引表fields_count
:字段计数fields
:字段表- 访问标志
- 名称索引
- 描述符
- 属性计数
- 属性表
methods_count
:方法计数methods
:方法表- 访问标志
- 名称索引
- 描述符
- 属性计数
- 属性表
attibute_count
:属性计数attribute
:属性表- 属性名索引
- 属性长度
- 属性信息
字节码指令
- 操作码
- 操作数
类加载过程
七个阶段的开始顺序如图:
图中用红色虚线框起来的3个过程分别为验证、准备、解析,它们合称为链接(Linking)过程。另外图中紫色的5项是严格按照执行。而蓝色的解析阶段不一定要在初始化之前, 也可以在初始化之后再解析,这种情况称为动态绑定或晚期绑定。
其中,前一个阶段还没有结束,后一个阶段可能已经开始。
加载:将二进制字节码加载到内存中。
加载的步骤:
- 通过类的全限定名获取该类的二进制字节流;
- 将字节流所代表的静态存储结构 转化为 方法区的运行时数据结构;
- 生成代表该类的Class对象并存放方法区,作为方法区该类的各种数据的访问入口。
注:对于数组类,不通过类加载器创建,而是由虚拟机直接创建的。另外加载阶段尚未完成,连接阶段可能已经开始。
验证:确保Class文件的字节流符合虚拟机规范。
文件格式验证
验证是否符合Class文件格式规范。
验证点有比如是否魔数
0xCAFEBABE
开头;主、次版本号是否范围之内;常量池中常量tag标示是否正确等等,只有通过全部的验证,才能把字节流存储到内存的方法区。元数据验证
验证是否符合Java语言规范。
字节码验证
验证数据流和控制流分析。
符号引用验证
验证符号引用转化为直接引用。
准备:static变量分配内存,并设置类变量的初始值。
- 类变量:赋予零值
- 常量:赋予真实值
- 实例变量:不赋任何值
解析:将常量池内的符号引用替换为直接引用。
符号引用(Symbolic Reference):
以一组符号来描述引用目标,符号可以是任意形式的字面量。只能要准确定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标也不一定存在内存。这样兼容性强,各种虚拟机只需要能接受符号引用即可。
直接引用(Direct Reference):
直接引用就是指向目标的指针、相对偏移量或者能间接定位到目标的句柄,直接引用和虚拟机内存布局息息相关。直接引用的目标必然存在与内存中。
初始化:主要是执行构造方法。
- 主动引用触发类初始化
- 创建类的实例
- 访问类的静态变量(非常量)
- 访问类的静态方法
- 父类未加载,首先加载父类(接口不会)
- 定义main()方法的类首先被加载
- 被动引用不触发初始化
- 子类调用父类静态变量
- 数组
- 访问类的常量
- 主动引用场景
- new、getstatic、putstatic、invokestatic指令
- 反射
- 初始化子类(接口除外)
- 主类
- 动态。。。
- 被动引用场景
- 子类引用父类静态字段
- 数组
- 引用类的常量
使用
卸载
类加载器
双亲委托模型
Bootstrap ClassLoader:启动类加载器
加载
$JAVA_HOME
中jre/lib/rt.jar
里所有的class,由C++实现,不是ClassLoader
子类。Extension ClassLoader:扩展类加载器
加载java平台中扩展功能的一些jar包,包括
$JAVA_HOME
中jre/lib/*.jar
或-Djava.ext.dirs
指定目录下的jar包。App ClassLoader:应用类加载器(系统类加载器)
加载classpath中指定的jar包及目录中class。
Custom ClassLoader:自定义类加载器
自定义的ClassLoader。重写
loadClass()
和defineClass()
方法。
JVM 内存区域
Java 内存模型
Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为Java内存模型。
Java内存模型内部原理
Java内存模型把Java虚拟机内部划分为线程栈和堆。这张图演示了Java内存模型的逻辑视图。
下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。
变量与对象的存放位置。
硬件内存架构
现代硬件内存模型与Java内存模型有一些不同。下面是现代计算机硬件架构的简单图示:
Java内存模型和硬件内存架构之间的桥接
硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示:
共享对象可见性
通过volatile
或加锁,保证可见性。
运行时内存区域
方法区
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。
在Java 8以前,方法区位于永久代,即Java堆中;从Java 8开始,方法区位于Metaspace(元空间,本地堆内存),元空间仍然可以触发GC。
- 类型信息
- 类型的全名
- 父类型的全名
- 是一个类还是接口
- 类型的修饰符
- 父接口全名的列表
- 类型的常量池
- 类型字段的信息
- 类型方法的信息
- 所有的静态类变量(非常量)信息
- 一个指向ClassLoader的引用
- 一个指向Class类的引用
- 方法列表
- 运行时常量池
堆:保存对象和数组。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。
栈:由栈帧组成。
栈帧:
- 局部变量数组(包含方法参数)
- 动态链接
- 操作数栈
- 返回值
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
直接内存
不属于运行时内存区域
对象内存布局
内存布局
普通java对象,和数组对象的内存布局如下,数组对象的对象头,要多出一个4字节的字段:
对象头
对象头的具体结构,在32位和64位虚拟机上有所不同,这里以64位为例。
Mark Word
根据对象处于不同状态,保存hashcode,GC分代年龄,锁状态,持有锁信息,偏向锁的thread ID等。
Klass Word
Klass Word是一个类型指针,指向class的元数据,JVM通过Klass Word来判断该对象是哪个class的实例。
length(可选)
数组长度
实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
对齐填充
使整个对象占用的内存大小满足8字节对齐。
JVM 垃圾回收
四种引用
强引用
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。
软引用
SoftReference
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
弱引用
WeakReference
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
虚引用
PhantomReference
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。它的get()方法返回null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
引用队列
ReferenceQueue
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
与软引用、弱引用不同,虚引用必须和引用队列一起使用。
堆区分代
- 年青代
- Eden区
- 两个Survivor
- From
- To
- 年老代
- 永久代
垃圾回收策略
年青代和年老代采用不同的垃圾回收策略,年青代使用Copy算法,年老代使用Mark算法。
引用计数(Reference Counting)
每有一个引用则增加一个计数,只收集计数为0的对象。缺点是无法处理循环引用的问题。
标记-清除(Mark-Sweep)
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除(清除后产生了内存碎片)。缺点是此算法需要暂停整个应用,同时,会产生内存碎片。
复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。缺点是需要两倍内存空间。
标记-整理(Mark-Compat)
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。