Java笔记:JVM

Java笔记:JVM

文章目录

  • Java笔记:JVM
    • 1. 运行时数据区
      • 1.1 组成
      • 1.2 定义
      • 1.3 方法区
      • 1.4 运行时常量池
      • 直接内存:非JVM内存区域
    • 2. 类加载机制
      • 2.1 类文件结构
        • 跨平台
        • Class文件组成
        • 字节码指令
      • 2.2 类加载流程
        • 生命周期
        • 生命周期各阶段具体行为
          • 1. 加载
          • 2. 验证
          • 3. 准备
          • 4. 解析
          • 5. 初始化
      • 2.3 类加载器
        • 三层类加载器 & 双亲委派模型
        • 破坏双亲委派模型
    • 3. 字节码执行机制:略
    • 4. 对象深入:创建、布局、访问
      • 4.1 创建
      • 4.2 布局
      • 4.3 访问
    • 5. 垃圾收集
      • 5.1 垃圾对象标记
      • 5.2 垃圾对象回收算法
      • 5.3 垃圾收集器:略
      • 5.4 内存分配与回收策略
    • 6. JDK工具

1. 运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

1.1 组成

线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:堆、方法区

1.2 定义

区域 定义 异常
程序计数器 可看作当前线程所执行的字节码的行号指示器
虚拟机栈 是方法执行的线程内存模型:每个方法执行时,JVM都会同步创建一个栈帧 StackOverflowError
OutOfMemoryError
本地方法栈 基本同虚拟机栈,针对Native方法 StackOverflowError
OutOfMemoryError
存储对象实例 OutOfMemoryError
方法区 存储被JVM加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等 OutOfMemoryError

栈帧:存储局部变量、操作数栈、动态链接、方法出口等信息

  • 局部变量表:存储编译期可知的各种JVM基本数据类型、对象引用、returnAddress类型。

    存储空间以局部变量槽Slot表示,其中64位长度的long和double类型占用2个Slot,其余占用1个Slot。局部变量表所需的内存空间在编译期完成分配

Java堆是垃圾收集器管理的内存区域。

  • 从内存回收角度看,基于分代收集设计,将堆分为:新生代、老年代、永久代、Eden空间、From/To Survivor空间等
  • 从分配内存角度看,线程共享的堆可划分出多个线程私有的分配缓冲区TLAB,以提升对象分配时的效率

1.3 方法区

关注的点:JDK 7和JDK 8的更新。

  • 在JDK 8以前,HotSpot是主流虚拟机,称呼方法区为“永久代”,但对于其他虚拟机实现,是不存在方法区的概念
  • 永久代设计的问题:容易遇到OOM
  • JDK 7:把原本放在永久代的字符串常量池、静态变量等移出(存到堆)
  • JDK 8:在本地内存实现的元空间取代了永久代,将JDK 7中永久代剩余的内容(主要是类型信息)全都移到了元空间中

1.4 运行时常量池

属于方法区的一部分(常量池表内容,类加载后进入方法区,存放到运行时常量池)。

  • Class文件除了有类的版本、字符、方法、接口等描述信息外,还有一项信息是常量池表

  • 常量池表:存放编译期生成的各种字面量和符号引用。此部分内容在类加载后存放到方法区的运行时常量池中。

  • 运行时常量池还会存储:由直接引用翻译出来的直接引用

  • 运行时进入方法区运行时常量池的常量:如String的intern()方法

  • 异常:OOM异常

直接内存:非JVM内存区域

  • JDK 1.4引入的NIO,引入了基于通道Channel和缓冲区Buffer的IO方式,它可以使用Native函数库直接分配堆外内存,并通过一个存储在堆里面的DirectByteBuffer对象作为这块内存的引用进行操作
  • 效果:提升性能。避免堆和native堆中来回复制数据
  • 异常:OOM

2. 类加载机制

2.1 类文件结构

跨平台

字节码(.class文件)和虚拟机。

Class文件组成

  1. 魔数:[1,4]

  2. Class文件版本号:[5,8]

    次版本号 + 主版本号

  3. 常量池:[9,?]

    入口:u2类型,代表常量池容量计数值(从1开始)

    比如:.class的第9个字节为0x16,即十进制的22,表示常量池中有21项常量[1,21],第0项常量空出来以做他用,具体参考“《深入理解Java虚拟机 第3版》6.3.2 常量池小节”
    

    存储常量类型:字面量、符号引用。

    字面量,比如文本字符串、final常量等
    符号引用:package、类和接口的全限定名、字段的名称和描述符、方法的名称和描述符、方法句柄和方法类型、动态调用点和动态常量
    

    常量如何存储:每一项常量都是一个表。具体请参考其他资料。

    javap -verbose [class类名]:javap用于反解析当前类对应的code区(汇编指令)、本地变量表、异常表、代码行偏移量映射表、常量池等等信息。
    
  4. 访问标志: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 标识这是一个模块
  5. 类索引、父类索引、接口索引集合

    类和父类索引:u2类型的数据

    接口索引集合:一组u2类型的数据的集合(同常量池,有一个计数值)

  6. 字段表集合

    字段修饰可选:访问修饰符、实例变量还是类变量、可变性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
    描述符:用于描述字段的数据类型、方法的参数列表和返回值。
    
  7. 方法表集合:基本同字段表,方法表结构依次为访问标志、名称索引、描述符索引、属性表集合。

    方法表的属性集合,存储一个名称Code的属性,为Java程序方法体的代码经编译器处理之后变为字节码存储在Code属性内。

字节码指令

Java字节码的指令组成:操作码、操作数(零至多个参数)。由于Java虚拟机采用操作数栈,而非寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码。

  1. 加载和存储指令
  2. 运算指令
  3. 类型转换指令
  4. 对象创建和访问指令
  5. 操作数栈管理指令
  6. 控制转移指令
  7. 方法调用和返回指令
  8. 异常处理指令
  9. 同步指令:基于管程monitor。方法同步是方法表的acc_synchronized标志,语句块是Java语言的synchronized关键字的语义实现(monitorenter、monitorexit指令)。

2.2 类加载流程

定义:JVM把描述符的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,称为类加载机制。

说明:Class文件是一个二进制字节流,可能源自磁盘、网络或者内存等等。

生命周期

加载 -> 连接(验证 -> 准备 -> 解析) -> 初始化 -> 使用 -> 卸载。

Java笔记:JVM_第1张图片

上述七个阶段的顺序并不完全是顺序开始的,其中:“加载、验证和准备、初始化、卸载”这五个阶段的顺序是确定的。而“解析”既可以在“初始化”前(正常流程)、也可以在“初始化”后(运行时绑定,动态绑定,晚期绑定)发生。

何时发生加载,JVM并没有强制约束;但是何时发生初始化,《Java虚拟机规范》规定有且只有六种情况必须立即对类进行初始化(而加载、验证和准备则需要在初始化前发生):

  1. 遇到new、getstatic、putstatic、invokestatic这四条指令,若类型没有进行过初始化,则需要进行初始化。场景:
    • new关键字实例化对象:T t = new T();
    • 读写静态字段(final static修饰的字段,会在编译期把结果放入常量池,不会触发初始化)
    • 调用静态方法
  2. 使用反射包java.lang.reflect进行反射调用
  3. 初始化类时,其父类没有初始化,则先触发父类初始化
  4. JVM启动,指定的主类(包含main方法的类)
  5. JDK 7的动态语言支持(未了解)
  6. 当接口定义default方法(Java 8特性),其实现类发生初始化,则先触发该接口

除了上述六种情况以外,其他任何方式都不会触发初始化,称为被动引用。如下:请注意被动三,常量会存到调用类的常量池中。

// 被动一:子类引用父类的静态字段,不会触发子类的初始化
// 被动二:数组类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. 加载
1)通过类的全限定名获取定义此类的二进制字节流
2)将二进制字节流的静态存储结构转化为方法区的运行时数据结构
3)内存中生成Class对象,作为方法区该类的各种数据的访问入口
2. 验证
要求:Class文件的字节流,数据符合JVM规范
3. 准备
类静态变量分配内存并设置类变量初始值(类型零值)

说明:概念上看,类变量所属内存位于方法区。JDK 7方法区的实现为永久代,实现符合概念;JDK 8方法区,类变量则会随着Class对象存放到堆中。
4. 解析
将常量池内的符号引用,替换为直接引用的过程。
可能发生在类加载时,可能发生在被使用时(运行时)
5. 初始化
执行类构造器:(javac编译器的自动生成物)。主要是static(类变量、静态语句块)语句合并产生

说明:static语句按定义顺序逐条执行,静态语句块中只能访问定义在其之前的变量,定义在其之后变量,语句块可对其执行赋值操作,但不能访问。
说明2:
	父类的先于子类执行;
	接口实现类在初始化时不会执行接口的
	接口执行不需要先执行父接口的
总之,对于接口而言,只有当使用到其定义的类变量,才会对其进行初始化

2.3 类加载器

类加载器:实现“通过一个类的全限定名,获取该类的二进制字节流”动作的代码,为类加载器。其位于JVM之外实现。

三层类加载器 & 双亲委派模型

类加载器 加载信息 说明
启动类加载器 \lib目录;
-Xbootclasspath参数指定的路径中存放且能够被JVM识别的类;
扩展类加载器 \lib\ext目录;
java.ext.dirs系统变量指定路径中所有类库
Java 9后被模块化取代
应用程序/系统类加载器 程序中默认的类加载器

从JVM角度看,存在两种类加载器:启动类加载器(BootstrapClassLoader)、其他所有类加载器

BootstrapClassLoader,由C++实现,是虚拟机自身的一部分
其他类加载器,由Java实现,独立存在于虚拟机外部

双亲委派模型:除了顶层启动类加载器外,所有其他类加载器都有自己的父类加载器(组合而非继承)。

工作流程:如果一个类加载器收到了类加载的请求,首先将这个请求委托给父类加载器去完成。父类加载器无法完成时,子类加载器才会尝试去完成加载。

作用:保证任何一个类,由加载它的类加载器和类本身确立其唯一性

破坏双亲委派模型

双亲委派模型并非一个具有强制性约束的模型。在Java 9模块化出现为止,出现过三次较大规模被破坏的情况。

破坏原因:父类加载器需要委托子类加载器去加载class文件。比如Driver接口,定义在JDK中,由启动类加载器加载,而各个数据库服务商的驱动,由系统类加载器加载。

3. 字节码执行机制:略

4. 对象深入:创建、布局、访问

4.1 创建

  1. Java语言层次的创建对象:new、复制、反序列化等
  2. 检查上述指令参数是否能在常量池中定位到一个类的符号引用
  3. 检查符号引用代表的类是否被加载、解析和初始化():没有则进行类加载
  4. 分配内存:1.指针碰撞;2.空闲列表
    • 是否规整,而堆规整与否取决于GC是否带有空间压缩整理能力)
    • 分配内存有两个操作,申请内存,赋给指针。存在线程安全问题,常见解决方式有1.CAS;2.本地线程分配缓冲区
  5. 实例部分:初始化为零值
  6. 对象头
  7. 构造模块:方法

4.2 布局

  • 对象头:两类信息
    1. 存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程池有的锁、偏向线程ID、偏向时间戳等。称为Mark Word
    2. 类型指针:指向它的类型元数据的指针
  • 实例数据
  • 对齐填充

4.3 访问

  • 句柄访问:左图
  • 直接指针:右图

Java笔记:JVM_第2张图片

5. 垃圾收集

5.1 垃圾对象标记

  • 引用计数算法

  • 可达性分析算法

    // GC Roots:虚拟机栈、本地方法栈、方法区(static、final)
    虚拟机栈引用的对象(方法参数、局部变量、临时变量)
    方法区中类静态属性引用的对象(static对象)
    方法区中常量引用的对象(final对象)
    本地方法栈引用的对象
    JVM内部的引用
    被同步锁(synchronized)持有的对象
    JMXBean、JVMTI中注册的回调等
    
算法 实现 优点 缺点
引用计数算法 对应中添加一个引用计数器 简单,效率搞 循环引用问题
可达性分析算法 GC Roots的根对象

引用:强引用、软引用、弱引用、虚引用

可达性分析算法的标记:两次标记

  1. 没有与GC Roots相连的引用链,第一次标记,随后进行一次筛选(此对象是否有必要执行finalize()方法

    • 没有必要执行,进入“即将回收的集合”

    • 有必要执行,对象被放到F-Queue的队列,稍后由Finalizer线程执行

      有必要执行的条件:重写finalize,且没有被执行过

  2. 第二次标记:有必要执行的对象,在其finalize()方法中成功与GC Roots相连,就被移出“即将回收的集合”

方法区回收:不再使用的类、废弃常量

5.2 垃圾对象回收算法

垃圾收集算法 实现 场景 缺点
标记-清除 标记,清除 效率;内存碎片
标记-复制 标记,复制,清除 新生代 内存利用率
标记-整理 标记,移动,清除 stop the world

分配担保: 标记复制。Survivor空间 < Monir回收后的存活对象,依赖其他区域(一般是老年代)进行分配担保

做法:新生代划分为8:1:1的Eden和Survivor*2。

5.3 垃圾收集器:略

5.4 内存分配与回收策略

  1. 对象优先在Eden空间
  2. 大对象直接进老年代
  3. 长期存活对象进老年代
  4. 动态对象年龄判断
  5. 空间分配担保

6. JDK工具

命令 关键字
jps Java进程
jstat 虚拟机进程,类加载 内存 垃圾收集 即时编译
jinfo 虚拟机参数
jmap 堆,转储快照
jhat 堆转储快照,分析
jstack

可视化工具:JConsole、JHSDB、VisualVM、JMS

你可能感兴趣的:(知识总结)