栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

01-JVM内存结构

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

01-JVM内存结构

1.JVM概述 1.1基本介绍

JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作

特点:

  • Java 虚拟机基于二进制字节码执行,由一套字节码指令集、一组寄存器、一个栈、一个堆、一个方法区等组成
  • 自动的内存管理,垃圾回收机制
  • 数组下标越界检查
  • JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的跨平台性,一次编译,处处执行

Java 代码执行流程:Java 程序 --(编译)–> 字节码文件 --(解释执行)–> 操作系统(Win的JVM,Linux的JVM)

JVM 结构:

- ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
- Method Area:类定义的信息是放在方法区中。
- Heap:类的实例对象。
- 当类调用方法时,会用到 JVM Stack、PC Register、Native Method stack。

方法执行时的每行代码是由执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口。

JVM、JRE、JDK 对比:

  • JDK(Java SE Development Kit):Java 标准开发包,它提供了编译、运行 Java 程序所需的各种工具和资源
  • JRE( Java Runtime Environment):Java 运行环境,用于解释执行 Java 的字节码文件

1.2架构模型

Java 编译器输入的指令流是一种基于栈的指令集架构。**因为跨平台的设计,Java 的指令都是根据栈来设计的,**不同平台 CPU 架构不同,所以不能设计为基于寄存器架构。

  • 基于栈式架构的特点:
    • 设计和实现简单,适用于资源受限的系统
    • 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现
      • 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器
      • 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数
    • 不需要硬件的支持,可移植性更好,更好实现跨平台
  • 基于寄存器架构的特点:
    • 需要硬件的支持,可移植性差
    • 性能更好,执行更高效,寄存器比内存快
    • 以一地址指令、二地址指令、三地址指令为主
  • 同样执行2+3这种逻辑操作,其指令分别如下:
  • 基于栈的计算流程(以Java虚拟机为例):
iconst_2 // 将整数2压入操作数栈
istore_1 // 把栈顶的内容放入局部变量表中索引为 1 的 slot 中
iconst_3 // 将整数3压入操作数栈
istore_2 // 把栈顶的内容放入局部变量表中索引为 2 的 slot 中
iload_1  // 把局部变量表索引为 1 的 slot 中存放的变量值 加载至操作数栈
iload_2  // 把局部变量表索引为 2 的 slot 中存放的变量值 加载至操作数栈
iadd	 // 栈顶的两个数出栈后相加,结果入栈
istore_0 // 把栈顶的内容放入局部变量表中索引为 0 的 slot 中
return   // 方法返回指令,回到调用点
  • 基于寄存器的计算流程
mov eax,2 //将eax寄存器的值设为1
add eax,3 //使eax寄存器的值加3
12

1.3生命周期

JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡

  • 启动:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点。

  • 运行:

    • main() 方法是一个程序的初始起点,任何线程均可由在此处启动

    • 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,JVM 使用的是守护线程,main() 和其他线程使用的是用户线程,守护线程会随着用户线程的结束而结束

    • 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程

    • JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多

      Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机

  • 死亡:

    • 当程序中的用户线程都中止,JVM 才会退出

    • 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止

    • 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 Java 安全管理器允许这次 exit 或 halt 操作

      • 调用System.exit() 方法 --> 调用 Runtime.exit() 方法
      • 调用Runtime.exit() 方法 --> 调用了 ShutDown.exit() 方法

1.4常见JVM

HotSpot VM (了解)
  1. HotSpot历史
    • 最初由一家名为“Longview Technologies”的小公司设计
    • 1997年,此公司被Sun收购;2009年,Sun公司被甲骨文收购。
    • JDK1.3时,HotSpot VM成为默认虚拟机
  2. 目前Hotspot占有绝对的市场地位,称霸武林。
    • 不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中,默认的虚拟机都是HotSpot
    • Sun/oracle JDK和openJDK的默认虚拟机
    • 因此本教程介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的GC机制。(比如其他两个商用虚机都没有方法区的概念)
  3. 从服务器、桌面到移动端、嵌入式都有应用。
  4. 名称中的HotSpot指的就是它的热点代码探测技术。
    • 通过计数器找到最具编译价值代码,触发即时编译或栈上替换
    • 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡
  • 具体JVM的内存结构,其实取决于其实现,不同厂商的JVM,或者同一厂商发布的不同版本,都有可能存在一定差异。主要以Oracle HotSpot VM为默认虚拟机。
2.内存结构 2.1内存概述

​ 内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区。

JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行

  • Java1.7 以前的内存结构图:

  • Java1.8 之后的内存结果图:

线程运行诊断:

  • 定位:jps 定位进程 ID
  • jstack 进程 ID:用于打印出给定的 Java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息

常见 OOM 错误:

  • java.lang.StackOverflowError
  • java.lang.OutOfMemoryError:java heap space
  • java.lang.OutOfMemoryError:GC overhead limit exceeded
  • java.lang.OutOfMemoryError:Direct buffer memory
  • java.lang.OutOfMemoryError:unable to create new native thread
  • java.lang.OutOfMemoryError:Metaspace

2.2JVM内存结构 2.1程序计数器

Program Counter Register 程序计数器(寄存器PC Register,CPU中读取速度最快)

作用:是记录下一条 二进制字节码中JVM指令的执行地址行号

原理:

  • JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程
  • 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号

特点:

  • 是线程私有的
  • 不会存在内存溢出,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC

Java 反编译指令:javap -v Test.class

#20:代表去 Constant pool 查看该地址的指令

0: getstatic #20 		// PrintStream out = System.out;
3: astore_1 			// --
4: aload_1 				// out.println(1);
5: iconst_1 			// --
6: invokevirtual #26 	// --
9: aload_1 				// out.println(2);
10: iconst_2 			// --
11: invokevirtual #26 	// --

JVM指令不能直接交由CPU执行,需要被解释器解释为机器码。

解释器 -> 解释 -> 二进制字节码中JVM指令 -> 为 -> 机器码 ->交给 -> CPU执行 程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

  • 使用PC寄存器存储字节码指令地址有什么用呢?
  • 为什么使用 PC 寄存器来记录当前线程的执行地址呢?
  1. 因为线程是一个个的顺序执行流,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
  2. JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

PC寄存器为什么被设定为私有的?

  • 总结下面: 为了保证当并发执行的多个线程的时候, CPU时间片疯狂切换, 当切换到别的线程的时候, 我们要记录上一个线程执行到哪个指令了,所以使用pc寄存器来存储; 如果设置为线程共享的pc寄存器, 当切换到上一个线程的时候, 它恢复到原来执行的位置,就不准确了
  1. 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法(并发操作),CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
  2. 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
  3. 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
  4. 这 样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

2.2虚拟机栈 2.1定义

Java 虚拟机栈:Java Virtual Machine Stacks,每个线程运行时所需要的内存;

  • 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(一个方法一个栈帧)

  • Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的

  • 虚拟机栈是每个线程私有的,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存(方法的一次调用对应一个栈帧),每个栈帧中存储着:

    • 局部变量表(Local Variables):存储方法里的 Java 基本数据类型以及对象的引用
    • 动态链接:也叫指向运行时常量池的方法引用
    • 方法返回地址:方法正常退出或者异常退出的定义
    • 操作数栈或表达式栈和其他一些附加信息

虚拟机栈特点:

  • 不需要进行GC,方法开始执行的时候栈帧会进栈,方法调用后自动弹栈清空了数据。GC是堆中的无用对象。
  • 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大)
    • 物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少,因为一个线程对应一个栈,即栈是线程私有的,所以栈大,那么栈数目少,线程数就少。
  • 方法内的局部变量是否线程安全:
    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析)
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

m1线程安全,私有的引用局部变量

m2线程不安全,sb是方法参数传递的,说明与其他线程共享

m3线程不安全,作为返回值,也共享了

线程运行诊断

案例一:cpu 占用过多
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

 top  # top 命令,查看是哪个进程占用 CPU 过高
#通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高  (pid 进程号, tid是线程号)
ps H -eo pid, tid, %cpu | grep 进程id

jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

jstack 32655(进程id)

通过ps得出CPU占用过高进程1中的具体线程A,再通过jstack查看进程1中所有线程信息,找到线程A信息。


2.2虚拟机栈异常

栈中可能出现的异常 : 栈溢出

面试题:栈中可能出现的异常

  • Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。(HostSpot不存在动态扩容)
    • 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈可以在线程创建的时候独立设置该线程的栈容量。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError异常。(递归问题)
    • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutofMemoryError异常。

**重点: **HostSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机倒是可以。所有HotSpot虚拟机是不会由于虚拟机栈无法扩展而导致OutOfMemoryError – 只要线程申请栈空间成功就不会OOM,但是如果申请时就失败,仍然是会出现OOM异常的。

总结:OOM是申请栈不到空间(HostSpot不存在动态扩容),StackOverFlowError是当前栈空间不够大,放不入栈帧

注意: 申请(每个线程进行一次) != 扩容

public class Demo01_01 {
    static int i = 1;
    public static void main(String[] args) {
        System.out.println(i++);
        main(args);
    }
}

2.3 栈内存大小

设置栈内存的大小

  • 设置栈内存大小:-Xss(size) -Xss1024k
  • 我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
-Xss1024m		// 栈内存为 1024MBS
-Xss1024k		// 栈内存为 1024KB
  • 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M(非Windows)
2.2局部变量 2.1变量表介绍
  • 局部变量表:Local Variables,也被称之为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及 returnAddress(返回值) 类型。
    • 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题
    • 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中
    • 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁
    • 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收

举例说明:局部变量表所需的容量大小是在编译期确定下来的

public class LocalVariablesTest {
    private int count = 0;

    public static void main(String[] args) {
        LocalVariablesTest test = new LocalVariablesTest();
        int num = 10;
        test.test1();
    }

    public void test1() {
        Date date = new Date();
        String name1 = "baidu.com";
        String info = test2(date, name1);
        System.out.println(date + name1);
    }
}

反编译后,可得结论:

  • 在编译期间,局部变量的个数、每个局部变量的大小都已经被记录下来
  • 所以局部变量表所需的容量大小是在编译期确定下来的

  • 利用 JClassLib 也可以查看局部变量的个数

字节码中方法内部结构的剖析

  • [Ljava/lang/String] :
    • [] 表示数组
    • L 表示引用类型
    • java/lang/String 表示 java.lang.String
  • 合起来就是:main() 方法的形参类型为 String[]

字节码,字节码长度为 16(0~15)

注意:生效行数和剩余有效行数都是针对于字节码文件的行数

2.2关于Slot

局部变量表最基本的存储单元是 slot(变量槽):

  • 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据
  • 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量
  • 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot
  • 局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的
  • 如果当前帧是由构造方法或者实例方法(非静态方法) 创建的,那么该对象引用this 将会存放在index为0 的slot处,其余的参数按照参数表顺序继续排列。

在构造器以及实例方法中,对象引用this 都会存放在索引为0的位置

//构造器
public LocalVariablesTest() {
    this.count = 1;
}

//实例方法
public void test1() {
    Date date = new Date();
    String name1 = "baidu.com";
    test2(date, name1);
    System.out.println(date + name1);
}

3.3 Slot 的重复利用

​ 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量出了其作用域,那么在其作用域之后声明新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

public void test4() {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        //变量c使用 之前已经销毁的变量b占据的slot的位置
        int c = a + 1;
    }

可以看到,局部变量c 重用了 局部变量b 的slot位置

2.4变量的分类
  • 按照数据类型分:

    • 基本数据类型
    • 引用数据类型
  • 按照在类中声明的方式分:

    • 类变量(静态变量):
      • linking的prepare阶段:给类变量默认赋值即零值
      • initial阶段:给类变量显式赋值即静态代码块赋值
    • 实例变量: 随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
    • final static 变量 : 在编译时就赋上初始值, 在linking的prepare阶段 显示赋值
    • 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过,应该是栈中数据弹出后,不会清除上次的值,再次使用时,如果不显示初始化,就会出现脏数据
  • 类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。

  • 和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

2.3操作数栈 3.1 操作数栈的特点

栈:可以使用数组或者链表来实现

  • 每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)
  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈,比如:执行复制、交换、求和等操作

3.2 操作数栈的作用
  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
  • 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个时候方法的操作数栈是空的(这个时候数组是创建好并且是长度固定的,但数组的内容为空)
  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。

  • 栈中的任何一个元素都是可以任意的Java数据类型
    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈单位深度
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
  • Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
3.3代码追踪
public void testAddOperation() {
        //byte、short、char、boolean:都以int型来保存
        byte i = 15;
        int j = 8;
        int k = i + j;
}
 0 bipush 15
 2 istore_1
 3 bipush 8
 5 istore_2
 6 iload_1
 7 iload_2
 8 iadd
 9 istore_3
10 return

程序执行流程

  • 首先执行第一条语句,PC寄存器指向的是0,也就是指令地址为0,然后使用bipush让操作数15入操作数栈。

  • 执行完后,让PC寄存器 + 1,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表索引1的位置,我们可以看到局部变量表的已经增加了一个元素

  • 解释为什么局部变量表索引从 1 开始,因为该方法为实例方法,局部变量表索引为 0 的位置存放的是 this。

  • 然后PC寄存器+1,指向的是下一行。让操作数8也入栈,同时执行 istore 操作,存入局部变量表中

  • 然后从局部变量表中,依次将数据取出放在操作数栈中,等待执行 add 操作

  • 将操作数栈的两个元素出栈,执行iadd操作
  • 这里的 iadd 操作具体是:执行引擎将字节码指令翻译成机器指令,然后被CPU进行运算,得出结果,重新放入操作数栈中

  • 然后执行 istore 操作,将操作数23 存储到局部变量表索引为3的位置

关于 int j =8; 的说明

  • 我们反编译得到的字节码指令如下
    • 因为 8 可以存放在 byte 类型中,所以压入操作数栈的类型为 byte ,而不是 int ,所以执行的字节码指令为 bipush 8
    • 然后将数值 8 转换为int类型存储在局部变量表中:istore_2

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

public int getSum() {
    int m = 10;
    int n = 20;
    int k = m + n;
    return k;
}

public void testGetSum() {
    //获取上一个栈桢返回的结果,并保存在操作数栈中
    int i = getSum();
    int j = 10;
}

3.4栈顶缓存技术

栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率。

  • 寄存器的主要优点:指令更少,执行速度快

基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术。

2.4动态链接

动态链接(或指向运行时常量池的方法引用)

动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定

  • 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里
  • 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

  • 为了支持当前方法的代码能够实现动态链接,栈帧内部的指令包含一个指向运行时常量池或该栈帧所属方法的引用

  • 在 Java 源文件被编译成的字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中

    常量池的作用:提供一些符号和常量,便于指令的识别

为什么要用常量池呢?

  • 因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,然后记录其引用即可,节省了空间
  • 常量池的作用:就是为了提供一些符号和常量,便于指令的识别


2.5返回地址

Return Address:存放调用该方法的 PC 寄存器的值

方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置

  • 正常:调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
  • 异常:返回地址是要通过异常表来确定

正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者

异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出

两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值

2.6附加信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。


2.3方法调用:解析和分派

PS:这一小结是对栈帧中动态链接的补充

3.1静态链接与动态链接

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

  • 静态链接:
    • 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期确定,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
  • 动态链接:
    • 如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
3.2方法的绑定机制

静态链接和动态链接对应的方法的绑定机制为:早期绑定(Early Binding)和f晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

  • 早期绑定

    • 早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
  • 晚期绑定

    • 如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
  • 随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。

  • Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

代码举例

class Animal{

    public void eat(){
        System.out.println("动物进食");
    }
}
interface Huntable{
    void hunt();
}
class Dog extends Animal implements Huntable{
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }

    @Override
    public void hunt() {
        System.out.println("捕食耗子,多管闲事");
    }
}

class Cat extends Animal implements Huntable{

    public Cat(){
        super();//表现为:早期绑定
    }

    public Cat(String name){
        this();//表现为:早期绑定
    }

    @Override
    public void eat() {
        super.eat();//表现为:早期绑定
        System.out.println("猫吃鱼");
    }

    @Override
    public void hunt() {
        System.out.println("捕食耗子,天经地义");
    }
}
public class Demo02_Early_LateBing {
    public void showAnimal(Animal animal){
        animal.eat();//表现为:晚期绑定
    }
    public void showHunt(Huntable h){
        h.hunt();//表现为:晚期绑定
    }
}


3.3虚方法与非虚方法
  • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
    • 静态方法、私有方法、final 方法、实例构造器、直接super.父类方法。
  • 其他方法称为虚方法。

子类对象的多态性的使用前提

  • 类的继承关系
  • 方法的重写

虚拟机中调用方法的指令

  • 普通调用指令:
    • invokestatic:调用静态方法,解析阶段确定唯一方法版本
    • invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
    • invokevirtual:调用所有虚方法
    • invokeinterface:调用接口方法
  • 动态调用指令:在jdk1.7后新增
    • invokedynamic:动态解析出需要调用的方法,然后执行
  • 区别
    • 前四条指令固化在虚拟机内部,方法的调用执行不可人为干预
    • 而invokedynamic指令则支持由用户确定方法版本
    • 其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。

代码举例

class Father {
    public Father() {
        System.out.println("father的构造器");
    }

    public static void showStatic(String str) {
        System.out.println("father " + str);
    }

    public final void showFinal() {
        System.out.println("father show final");
    }

    public void showCommon() {
        System.out.println("father 普通方法");
    }
}

class Son extends Father {
    public Son() {
        //invokespecial
        super();
    }
    public Son(int age) {
        //invokespecial
        this();
    }
    //不是重写的父类的静态方法,因为静态方法不能被重写!
    public static void showStatic(String str) {
        System.out.println("son " + str);
    }
    private void showPrivate(String str) {
        System.out.println("son private" + str);
    }

    public void show() {
        //invokestatic
        showStatic("atguigu.com");
        //invokestatic
        super.showStatic("good!");
        //invokespecial
        showPrivate("hello!");
        //invokespecial
        super.showCommon();

        //虚方法如下:
        //invokevirtual
        showFinal();//因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。

        //invokevirtual
        showCommon();
        info();

        MethodInterface in = null;
        //invokeinterface
        in.methodA();
    }

    public void info(){

    }
    public void io(){
        System.out.println();
    }
    public void display(Father f){
        f.showCommon();
    }


}

interface MethodInterface{
    void methodA();
}
class Demo03_VirtualMethod {
    public static void main(String[] args) {
        Son so = new Son();
        so.show();
        so.io();
    }
}
3.4invokedynamic指令
  • JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进。
  • 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的 Lambda表达式 的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
  • Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。

动态类型语言和静态类型语言

  • 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
  • 说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
Java语言:String info = "mogu blog";     		(Java是静态类型语言的,会先编译再进行类型检查)
JS语言:var name = "shkstart";  var name = 10;	(运行时才进行检查)
Python语言:info = 130.5;  (动态类型语言)

3.5方法重写的本质

Java 语言中方法重写的本质:

  • 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
  • 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.lang.IllegalAccessError 异常
  • 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
  • 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

IllegalAccessError介绍

  • 程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。
  • 一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
  • 比如,你把应该有的jar包放从工程中拿走了,或者Maven中存在jar包冲突
3.6虚方法表
  • 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。
  • 因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,非虚方法不会出现在表中。使用索引表来代替查找。
  • 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
  • 虚方法表是什么时候被创建的呢?虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的虚方法表也初始化完毕。
  • 如图所示:如果类中重写了方法,那么调用的时候,就会直接在该类的虚方法表中查找

回顾解析阶段

  • 解析阶段就是将常量池内的符号引用转换为直接引用的过程
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

虚方法表举例

interface Friendly {
    void sayHello();
    void sayGoodbye();
}
class Dog {
    public void sayHello() {
    }
    public String toString() {
        return "Dog";
    }
}
class Cat implements Friendly {
    public void eat() {
    }
    public void sayHello() {
    }
    public void sayGoodbye() {
    }
    protected void finalize() {
    }
    public String toString(){
        return "Cat";
    }
}

class CockerSpaniel extends Dog implements Friendly {
    public void sayHello() {
        super.sayHello();
    }
    public void sayGoodbye() {
    }
}

public class VirtualMethodTable {
}

2.4本地方法栈

​ 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。

​ 本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

JNI : Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植

  • 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常

  • 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一

  • 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限

    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    • 直接从本地内存的堆中分配任意数量的内存
    • 可以直接使用本地处理器中的寄存器

2.5堆 5.1定义

​ Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题。

存放哪些资源:

  • 对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存
  • 字符串常量池:
    • 字符串常量池原本存放于方法区,JDK7 开始放置于堆中
    • 字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table
  • 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
  • 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率
5.2堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出

# 设置堆内存指令 可以使用 -Xmx Size来指定堆内存大小。
 -Xmx8m 

内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常。

5.3堆内存诊断

堆内存诊断工具:(控制台命令)

  1. jps:查看当前系统中有哪些 Java 进程 jps

    jsp
    
  2. jmap:查看堆内存占用情况

    jmap --heap --pid 进程id
    
  3. jconsole:图形界面的,多功能的监测工具,可以连续监测

在 Java7 中堆内会存在年轻代、老年代和方法区(永久代):

  • Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间。
  • Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区。
  • Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理

分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能

public static void main(String[] args) {
    //返回Java虚拟机中的堆内存总量
    long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
    //返回Java虚拟机使用的最大堆内存量
    long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
    
    System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M
    System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M
}

2.6方法区 6.1定义

​ 方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)。

  • 方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。
  • 方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)
  • 方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现。
  • 为了避免方法区出现 OOM,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中
    • 类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表


ClassLoader用来加载类的字节码。

6.2方法区内存溢出

1.8 之前会导致永久代内存溢出

#使用 -XX:MaxPermSize=size 指定永久代内存大小
-XX:MaxPermSize=8m

1.8 之后会导致元空间内存溢出

使用 -XX:MaxMetaspaceSize=size 指定元空间大小
-XX:MaxMetaspaceSize=8m
6.3运行时常量池

在类加载阶段将常量(Constant Pool)池放入运行时常量池。

反编译(二进制字节码得到 类基本信息,常量池,类方法定义,包含了虚拟机指令)

#查看class文件反编译结果(idea打开class文件会被默认反编译)
javap -v Test.class 

常量池表(Constant Pool Table)是 .class 文件的一部分,存储了类在编译期间生成的字面量、符号引用,JVM 为每个已加载的类维护一个常量池

  • 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等
  • 符号引用:类、字段、方法、接口等的符号引用

运行时常量池是方法区的一部分

  • 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池
  • 类在解析阶段将这些符号引用替换成直接引用
  • 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()
6.4 StringTable

jdk1.6 StringTable 位置是在永久代中,1.6以后 StringTable 位置是在堆中。

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中


intern方法 1.8
调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功
  • 如果有该字符串对象,则放入失败
    无论放入是否成功,都会返回串池中的字符串对象
String a = "123456"
a1 = a.intern() 
如果 “123456”存在常量池中,a不改变引用
如果 “123456”不存在常量池中,a改变引用,在常量池新建中“123456”
a1无论如何都会指向“123456”

例1:

public class Main {
	public static void main(String[] args) {
		String str = new String("a") + new String("b");	
		String st2 = str.intern();
		String str3 = "ab";
		// true
		System.out.println(str == st2);
        // true
		System.out.println(str == str3);
	}
}

例2:

public class Main {
	public static void main(String[] args) {
        // 此处创建字符串对象 "ab" ,因为串池中还没有 "ab" ,所以将其放入串池中
		String str3 = "ab";
        // "a" "b" 被放入串池中,str 则存在于堆内存之中
		String str = new String("a") + new String("b");
        // 此时因为在创建 str3 时,"ab" 已存在与串池中,所以放入失败,但是会返回串池中的 "ab" 
		String str2 = str.intern();
        // false
		System.out.println(str == str2);
        // false
		System.out.println(str == str3);
        // true
		System.out.println(str2 == str3);
	}
}

intern方法 1.6 (区别是a的引用不可能改变)

String a = "123456"
a1 = a.intern() 
如果 “123456”存在常量池中,a不改变引用
如果 “123456”不存在常量池中,a不改变引用,在常量池新建中“123456”
a1无论如何都会指向“123456”

StringTable 垃圾回收 : JVM参数设置

-Xmx10m  # 指定堆内存大小
-XX:+PrintStringTableStatistics  # 打印字符串常量池信息
-XX:+PrintGCDetails -verbose:gc   #打印 gc 的次数,耗费时间等信息
public class Code_05_StringTableTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            for(int j = 0; j < 10000; j++) { // j = 100, j = 10000
                String.valueOf(j).intern();
                i++;
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

内存不足,触发一次垃圾回收,垃圾回收速度很快,新生代的垃圾回收快

StringTable 性能调优

​ 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶(对象数组长度)的个数,减少hash碰撞的可能性,链的长度较短,来减少字符串放入串池所需要的时间,哈希桶的长度太小的话,如果String常量对象很多,哈希碰撞更严重,链表插入、扩容、红黑树费时

  • 考虑是否需要将字符串对象入池

  • 可以通过 intern 方法减少重复入池,不同对象(相同字符串)指向池中同一String

  • 设置桶的长度:

-XX:StringTableSize=桶个数(最少设置为 1009 以上)
2.7本地内存 7.1基本介绍

​ 虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM

本地内存:又叫做堆外内存,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 Java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM

本地内存概述图:


7.2元空间

PermGen 被元空间代替,永久代的类信息、方法、常量池等都移动到元空间区

元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制

方法区内存溢出:

  • JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space

     -XX:MaxPermSize=8m		#参数设置
    
  • JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace

    -XX:MaxMetaspaceSize=8m	#参数设置	
    

元空间内存溢出演示:

public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

7.3直接内存

直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。

Direct Memory -----是操作系统的内存 —Java和系统都可以访问,避免了内存重复

常见于 NIO 操作时,用于数据缓冲区
分配回收成本较高,但读写性能高
不受 JVM 内存回收管理

**使用直接内存的好处 **

普通的文件读写流程:

  • Java本身不具备磁盘读写的能力 ,不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 Java 堆内存中。

  • 缺点 : 数据存储了两份,在系统内存中有一份,Java 堆中有一份,造成了不必要的复制。

DirectBuffer 文件读取流程

直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

直接内存回收原理

public class Demo07_DirectMemory {

    public static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
//        method();
        method1();
    }

    // 演示 直接内存 是被 unsafe 创建与回收
    private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {

        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(Unsafe.class);

        long base = unsafe.allocateMemory(_1GB);
        unsafe.setMemory(base, _1GB, (byte) 0);
        System.in.read();

        unsafe.freeMemory(base);
        System.in.read();
    }

    // 演示 直接内存被 释放
    private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer = null;
        System.gc(); // 手动 gc
        System.in.read();
    }
}

直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。

第一步:allocateDirect 的实现

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

底层是创建了一个 DirectByteBuffer 对象。
第二步:DirectByteBuffer 类

DirectByteBuffer(int cap) {   // package-private
   
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size); // 申请内存
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 
    att = null;
}

这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。

 public void clean() {
        if (remove(this)) {
            try {
            // 都用函数的 run 方法, 释放内存
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }

可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法,

	public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        // 释放内存
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }

直接内存的回收机制总结

  • 使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
  • ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存。

注意:

    private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer = null;
        System.gc(); // 手动 gc 失效
        System.in.read();
    }

一般用 JVM调优时,会加上下面的参数:

-XX:+DisableExplicitGC  // 静止显示的 GC

意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 Full GC,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。


2.8变量位置

变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置

静态内部类和其他内部类:

  • 一个 class 文件只能对应一个 public 类型的类,这个类可以有内部类,但不会生成新的 class 文件

  • 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证)

类变量:

  • 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 Java 进程产生和销毁
  • 在 Java8 之前把静态变量存放于方法区,在 Java8 时存放在堆中的静态变量区

实例变量:

  • 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分
  • 在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中

局部变量:

  • 局部变量是定义在类的方法中的变量
  • 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出

类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?

  • 类常量池与运行时常量池都存储在方法区,而字符串常量池在 Jdk7 时就已经从方法区迁移到了 Java 堆中
  • 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符
  • 在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池
  • 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池

什么是字面量?什么是符号引用?

  • 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示

    int a = 1;				//这个1便是字面量
    String b = "iloveu";	//iloveu便是字面量
    
  • 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址


参考1-尚硅谷

参考2-黑马

参考3-cyc2018

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/940524.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号