• Java:JVM基础


    参考

    JavaGuide

    JVM内存区域

    在这里插入图片描述
    在这里插入图片描述

    程序计数器

    程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器,各线程之间计数器互不影响。
    程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期与线程同步。

    虚拟机栈

    除了一些Native方法调用通过本地方法栈实现,其他所有的Java方法调用都是通过栈来实现的,每一次方法调用都会入栈,每一个方法返回都会出栈,每个方法对应一个栈帧,栈帧内部结构如下:
    在这里插入图片描述

    • 局部变量表:存放了编译时可知的各种数据类型和对象引用。
    • 操作数栈:主要作为方法调用的中转站,用于存放方法执行过程中产生的中间计算结果和临时变量。
    • 动态链接:当一个方法要调用其他方法时,需要将符号引用转换为调用方法的直接引用,即动态链接。
    • 方法返回:一种是随return语句正常返回,一种是抛出异常,两种都会导致栈帧被弹出,方法结束。

    本地方法栈

    类似于虚拟机栈,虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用的Native方法服务,在HotSpot虚拟机中,两栈合二为一。
    Native方法被执行时在本地方法栈也会创建栈帧,结构同上。

    堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,唯一作用是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

    但随着JIT编译器的发展产生了逃逸分析技术,如果某些方法中的对象引用没有被返回或者未被外面使用,那么对象可以直接在栈上分配内存。

    Java堆是垃圾收集器管理的主要区域,在JDK7及之前,堆从垃圾回收的角度被划分为新生代、老年代和永久代;在JDK8之后永久代被元空间取代,元空间使用本地内存。

    方法区

    方法区是一种设计规范,属于JVM运行时数据区域的一块逻辑区域,是各个线程共享的内存区域,当虚拟机要使用一个类时,它需要读取并解析Class文件获取相关信息,再将信息存入方法区,主要是类信息、字段信息(成员变量)、方法信息、常量、静态变量等。

    永久代和元空间是实现方法区的两种方式,弃用永久代的主要原因是: 整个永久代有一个JVM本身设定的固定上限,不能调整,而元空间放在本地内存,不容易溢出。

    符号引用与直接引用

    符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量,比如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等,在编译期或者运行期间生成,不依赖于具体的内存地址,而是在运行时根据上下文信息去定位目标。

    直接引用时一种直接指向目标的内存地址或者偏移量,与内存地址直接相关,如指向对象实例的指针、指向类的变量的指针等。

    在程序运行时需要通过符号引用来找到对应的直接引用,这个过程称为解析,他是Java虚拟机执行引擎的一部分。

    使用两种引用的原因:

    • 动态链接:符号引用提供了一种在编译期间和运行期间都能定位目标的方法,使得Java能实现动态链接,即在运行时才确定最终目标。
    • 运行时多态:符号引用提供了一种描述方法的方式,同上。
    • 内存管理:使虚拟机更灵活地进行内存管理,如动态加载和卸载类。
    • 平台无关性:不需要针对不同平台进行特定的编译或链接。

    运行时常量池

    常量池表,用于存放编译期生成的各种字面量和符号引用,类似符号表。

    字面量是源代码中的固定值,包括整数、浮点数和字符串字面量。
    符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号等。

    字符串常量池

    字符串常量池是JVM为了提升性能和减少内存消耗针对字符串专门开辟的一块区域,主要是为了避免重复创建字符串。

    JDK1.7将字符串常量池移动到堆中,因为永久代垃圾回收效率太低,只有在整堆收集的时候才会被执行,而大量字符串通常是需要被及时回收的,因此移动到堆中。

    直接内存

    直接内存是一种特殊的内存缓冲区,通过JNI的方式在本地内存中分配。
    CSDN

    Hotspot虚拟机对象

    创建过程

    1. 类加载检查:虚拟机执行到一条new指令时,首先检查这个指令的参数能否再常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如没有则先加载。
    2. 分配内存:对象所需内存的大小在类加载时确定,内存分配方式有指针碰撞和空闲列表两种。
    • 指针列表:已分配的内存放一边,未分配的内存放一边,中间有一个分界指针,只需移动指针即可完成分配。Serial和ParNew两种GC收集器使用该种方式。
    • 空闲列表:维护一个列表记录哪些内存块是可用的,分配的时候找一块分配并更新列表。CMS使用该种方式。

    内存分配时需要考虑线程安全问题,通常采用两种方式保证线程安全:

    • CAS+失败重试:CAS是乐观锁的一种实现方式,每次先不加锁,而是假设没有冲突去尝试操作,如果因为冲突失败就重试,直到成功为止。
    • TLAB:为每一个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象不能以TLAB方式分配时再采用CAS方式。
    1. 初始化零值:分配内存完成后将除对象头之外的内存空间都初始化为零值,这保证了对象的实例字段不赋初值即可直接使用。
    2. 设置对象头:对对象进行必要的设置,如这个对象是哪个类的实例,如何找到类的元数据信息、对象的哈希码、GC分代年龄等信息。
    3. 执行init方法:把对象按照程序员的意愿进行初始化。

    虚拟机对象的内存布局

    • 对象头:包括两部分信息,第一部分用于存储对象自身的运行时数据(如哈希码、GC分代年龄等),另一部分是类型指针,即对象指向它的类元数据的指针,通过这个指针确定这个对象是哪个类的实例。
    • 实例数据:真正存储的有效数据,如程序中所定义的各种类型的字段内容。
    • 对齐填充部分:填充整个对象的大小为8字节的整数倍。

    对象访问

    对象访问的方式由虚拟机具体实现而定,目前主流的是使用句柄和直接指针两种。

    句柄方式如下:堆中划分一块内存作为句柄池,线程栈帧的局部变量表中存储的reference是对象的句柄地址,句柄中又包含了对象实例数据和对象类型数据各自具体的地址信息。
    这种方式的优点在于对象被移动时只改变句柄中的信息,而reference本身不需修改。
    在这里插入图片描述
    直接指针访问方式如下:reference中存储的就是对象地址,这种方法节省了一次指针定位的开销。HotSpot虚拟机采用这种方式。
    在这里插入图片描述

    class文件

    class文件即字节码,是面向JVM的文件,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。

    结构

    ClassFile {
        u4             magic; //Class 文件的标志
        u2             minor_version;//Class 的小版本号
        u2             major_version;//Class 的大版本号
        u2             constant_pool_count;//常量池的数量
        cp_info        constant_pool[constant_pool_count-1];//常量池
        u2             access_flags;//Class 的访问标记
        u2             this_class;//当前类
        u2             super_class;//父类
        u2             interfaces_count;//接口数量
        u2             interfaces[interfaces_count];//一个类可以实现多个接口
        u2             fields_count;//字段数量
        field_info     fields[fields_count];//一个类可以有多个字段
        u2             methods_count;//方法数量
        method_info    methods[methods_count];//一个类可以有个多个方法
        u2             attributes_count;//此类的属性表中的属性数
        attribute_info attributes[attributes_count];//属性表集合
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述
    在这里插入图片描述
    各组件说明如下:

    • magic:0xCAFEBABE,标志此文件为一个class文件。
    • Minor & Major Version:java主、次版本号。
    • Constant Pool:主要存放字面量和符号引用两种常量,常量池打大小是constant_pool_count-1,计数器从1开始,索引值为0代表不引用任何常量池项。常量池中每一项都代表一个常量。
    • Access Flags:访问标志,识别一些类或接口的访问信息,如:是类还是接口、是否为public或abstract类型等。
    • This Class、Super Class、Interfaces:确定类名、继承关系(Object类没有父类)以及实现了哪些接口(如果是接口则为被哪些类实现)。
    • Field:包含字段作用域(public、private、protected)是否static、是否final等,还有字段名称、描述符、额外属性等。
    • Methods:基本同字段作用域类似。
    • Attributes:用于描述某些场景专用的信息。

    类加载过程

    共有:加载、验证、准备、解析、初始化、使用、卸载7个阶段,其中,验证、准备、解析统称为链接阶段。
    在这里插入图片描述

    加载

    加载时需要完成以下三件事

    • 通过全类名获取定义此类的二进制字节流。
    • 将字节流代表的静态存储结构转换为运行时方法区的数据结构。
    • 在内存中生成一个代表该类的class对象,作为这些数据的访问入口。

    验证

    确保Class文件的字节流中包含的信息符合约束要求,不会危害虚拟机安全,主要包括:文件格式验证、元数据验证、字节码验证、符号引用验证。
    在这里插入图片描述

    准备

    准备阶段是正式为静态变量分配内存并设置初始值的阶段,都在方法区中分配。
    在这里插入图片描述

    解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

    每个类都有一张方法表存放所有的方法地址,当需要调用时只需根据方法表即可直接调用,也就是将符号引用替换为直接引用。

    初始化

    初始化阶段是执行()方法的过程,这一步开始才真正开始执行程序的字节码。

    使用

    卸载

    卸载类即该类的Class对象被GC。需要满足三个要求才可卸载:

    • 该类在堆中的所有实例对象已被GC。
    • 该类没有被引用。
    • 该类的类加载器实例已被GC。

    在JVM生命周期内,JVM自带的类加载器加载的类不会被卸载,可以自定义类加载器并进行类卸载。

  • 相关阅读:
    算法——滑动窗口
    20221130今天的世界发生了什么
    tcpip.sys是什么文件,tcpip.sys蓝屏的解决办法
    一篇文章学会调优 ClickHouse
    38-Java方法的参数传递机制:值传递
    qt软件正常运行的崩溃了定位行号方法
    最长不下降子序列
    Python中json.loads()无法解析单引号字符串问题的两种解决方法
    Istio实战(六)- Istio 部署
    Django之Session
  • 原文地址:https://blog.csdn.net/weixin_43249758/article/details/136436367