JVM1️⃣概述、运行时数据区

1、JVM

1.1、含义

Java 虚拟机(Java Virtual Machine)

  1. 含义:Java 程序(二进制字节码)的运行环境,位于 Java 程序和操作系统之间。

    image-20211217215448810

  2. 优点

    1. 一次编写,到处运行(跨系统跨平台)
    2. 自动内存管理,垃圾回收功能
    3. 数组下标越界检查
    4. 多态
  3. 常见 JVMOracle JVM 规范

    0_引言

1.2、概念对比

  1. JVM
  2. JRE = JVM + 基础类库
  3. JDK = JRE + 编译工具
  4. JavaSE 开发 = JDK + IDE
  5. JavaEE 开发 = JavaSE + 服务器

1.3、体系结构(❗)

  • 运行时数据区

    • 方法区:类
    • :实例
    • 虚拟机栈:引用(本地方法栈存放 Native 方法的引用)
    • 程序计数器:存储下一行指令地址
  • 执行引擎

    • 解释器:逐行执行代码;

    • 即时编译器:优化热点代码;

    • 垃圾回收

      image-20220216181130084

2、运行时数据区

JVM 在执行 Java 程序的过程中,会将它所管理的内存划分为若干个不同的数据区,也就是运行时数据区。

2.1、程序计数器

image-20220216183804851

  • 作用:若当前执行的是 JVM 的方法,程序计数器保存当前执行指令的地址;若当前执行的是 native 方法,程序计数器为空。

  • 特点:线程私有,不存在内存溢出。

  • 流程:Java 类经过编译,成为二进制字节码。之后进行以下步骤:

    1. 解释器读取一行 JVM 指令,此时程序计数器存储下一行指令地址;
    2. 解释器将 JVM 转为机器码,交给 CPU 执行;
    3. 解释器从程序计数器获得下一行指令地址,此时程序计数器再存储下一行指令地址;
    4. 重复以上,直到代码结束。
    // 二进制字节码				// Java代码
    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 		// --
    14: aload_1 				// out.println(3);
    15: iconst_3 				// --
    16: invokevirtual #26 		// --
    19: aload_1 				// out.println(4);
    20: iconst_4 				// --
    21: invokevirtual #26 		// --
    24: aload_1 				// out.println(5);
    25: iconst_5 				// --
    26: invokevirtual #26 		// --
    29: return
    

2.2、虚拟机栈、本地方法栈

image-20220216211235155

  • :仅允许在线性表的一端进行插入和删除操作,后进先出(LIFO)
  • 虚拟机栈:每个线程运行时所需的内存;
    • 由多个栈帧(Frame)组成
    • 栈帧:对应每个方法调用时所占内存。
      • 存储局部变量表、操作数栈、动态链接、方法出口等信息
      • 每个线程只能有一个活动栈帧,对应当前正在执行的方法。
  • 本地方法栈:跟虚拟机栈的区别在于,本地方法栈是给 Native 方法使用的。
  • 特点:与线程同时创建,线程私有,不存在内存溢出,不存在垃圾回收。

栈内存

栈内存分配并非越大越好。物理内存大小固定,栈内存分配越大,线程数目越少。

  1. JVM参数-Xss size,有默认值
    • Linux、macOS、Oracle Solaris:1024 KB;
    • Windows:取决于虚拟内存;
  2. 内存溢出java.lang.StackoverFlowError
    • 栈帧过大(理论上不会)
    • 栈帧过多:举例如下
      1. 逻辑判断条件永远为 true;
      2. 无限递归:递归没有终止条件;
      3. 代码循环引用,如一对多关系的实体类转 Json。

线程安全问题

针对方法内的变量,判断依据为变量是否线程私有。

  • 线程安全:方法内的变量没有逃离方法的作用范围。
  • 线程不安全:方法内的变量逃离方法的作用范围。
    • :方法外部定义的引用类型参数、作为方法返回值、方法参数等;
    • 可能存在一个以上的线程,对该变量进行存取操作。(如数据库的脏读、幻读等)

线程运行诊断

案例1:程序占用过多 CPU 内存

案例2:程序发生死锁(运行长时间没结果)

  • 通过命令定位程序
    1. top 命令:实时显示进程信息(Progress)
    2. ps 命令:抓取进程快照,查看线程id
  • 工具jstack 进程id

2.3、堆

image-20220216222116467

  • 功能:分配所有的类实例和数组。(通过 new 关键字创建的对象)
  • 特点:JVM启动时创建,线程共享(线程安全问题),存在垃圾回收机制。

堆内存

  1. JVM参数-Xmx size

  2. 内存溢出java.lang.OutOfMemoryError: Java Heap space(OOM)

    • 原因:在对象的生命周期内,gc 不会对该对象进行回收。
    • 举例:向 ArrayList 中无限添加 String 对象。
  3. :当一个线程抛出OOM异常后,它所占据的内存资源会被全部释放掉,不会影响其他线程。

内存诊断工具

  1. jps:当前系统中的 Java 进程
  2. jmap:堆内存占用情况,某一时刻的快照( jmap - heap 进程id )
  3. jconsole:可视化,连续监测
  4. jvisualvm:可视化(堆转储:堆 Dump 功能)

2.4、方法区

image-20220216224205202

方法区是一个概念上的结构。

  • 功能:存储类的结构
    • 方法和构造函数的代码;
    • 运行时常量池、字段和方法数据;
    • 接口初始化、实例初始化、类中的特殊方法;
  • 特点:JVM 启动时创建,线程共享(线程安全问题),存在垃圾回收机制。
  • 方法区在逻辑上,是堆的一部分。
  • 实现位置
    • <= JDK 1.6:永久代(Permanent Generation),位于堆中
    • JDK 1.7:有永久代,逐渐 “去永久代”,部分位于堆中;
    • >= JDK 1.8:元空间(Meta Space),位于本地内存中。

方法区内存

:当一个线程抛出OOM异常后,它所占据的内存资源会被全部释放掉,不会影响其他线程。

< JDK 8 :永久代

  • JVM参数-XX:MaxPermSize=size
  • 内存溢出java.lang.OutOfMemoryError: PermGen space(OOM)

>= JDK 8:元空间

  • JVM参数-XX:MaxMetaspaceSize=size
  • 内存溢出java.lang.OutOfMemoryError: Metaspace(OOM)

运行时常量池

运行时常量池(run-time contant pool)是常量池(constant_pool)的运行时表示。

  • 每个类文件或接口都有一个常量池表,包含字面量、方法和字段引用等信息;
  • 在类或接口创建时(class文件被加载),构造运行时常量池;
  • 引用地址
    • 常量池:符号地址;
    • 运行时常量池:真实地址;
  • 所有对象引用在最初都是符号引用,在实际使用时才创建对象。

字节码文件

可通过 javap 反编译工具查看

  • 包含 类基本信息、常量池、类方法定义
  • JVM 在执行指令时,根据符号地址查询常量池表,找到引用的对象;
  • 重复以上动作,直到所有方法执行结束。

实例:在 main 方法中,运行System.out.println("helloworld ")。以下是反编译代码的一部分。

(简单演示一行 JVM 指令的执行过程)

image-20220217150943100

串池:StringTable

串池

  • 存储字面量方式创建的字符串对象,具有不重复的特性。
  • 底层实现:HashTable
    • 字符串对象作为 HashTable 的 Key;
    • Key 具有唯一性,可以保证字符串对象的不重复性。
  • 位置
    • JDK 1.6 前:永久代
    • JDK 1.7 开始:堆

字符串对象创建

常量池中的字符串对象,最初仅是符号引用,在实际使用时才创建对象。

  1. 字面量创建String str = "abc";
    • 类编译后,字面量 "abc" 存储在常量池;
    • 类加载后,字面量 "abc" 存储在运行时常量池;
    • 执行代码对应的 JVM 指令时
      1. ldc:查询运行时常量池表,找到字面量 "abc";
      2. astore:在本地变量表中为其创建一个 String 对象;
      3. 检查 StringTable 中是否存在该对象:不存在则加入对象,存在则返回 StringTable 中的该对象。
  2. new 方式String str = new String("abc");
    • 中为字符串对象实例分配空间。
  3. intern 方法:主动将字符串对象放入串池
    • 串池中已有该对象:返回该对象;
    • 串池中没有该对象
      • JDK 1.6 前:将对象的副本放入串池,返回副本对象;
      • JDK 1.7 开始:将对象放入串池,并返回该对象。

字符串拼接

  • 变量拼接

    • 变量:编译时无法确定值,运行时才能确定值;
    • 本质上使用 StringBuilder.append().toString()
  • 常量拼接

    • 常量:编译时已确定值;
    • 编译期优化,常量在编译时期就连在一起。

练习

练习1

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
String s7 = new String("ab");
// 问
System.out.println(s3 == s4);	
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s4 == s7);

// 答:fasle、true、true、false(堆中的不同地址)

分析

说明 位置
s1 "a" 常量,字面量方式 串池
s2 "b" 常量,字面量方式 串池
s3 "ab" 常量拼接,字面量方式(编译期优化) 串池
s4 new String("ab") 变量拼接,使用 new StringBuilder()
s5 "ab" 常量,字面量方式 串池
s6 "ab" intern(),返回串池中的对象 串池
s7 new String("ab") new 方式创建

练习2

String x1 = new String("c") + new String("d");
x1.intern();
String x2 = "cd";
// 问
System.out.println(x1 == x2);

分析

x1 x2 x1.intern() 结果
JDK 1.8 new String("cd") "cd" 串池中已有 cd,不将 x1 放入 false
JDK 1.6 new String("cd") "cd" 串池中已有 cd,不将 x1 放入 false

练习3:(调换练习2 最后两行代码位置)

String x1 = new String("c") + new String("d");
String x2 = "cd";
x1.intern();

// 问
System.out.println(x1 == x2);

分析

x1 x1.intern() x2 结果
JDK 1.8 new String("cd") 串池中没有 cd,将 x1 放入 "cd" true
JDK 1.6 new String("cd") 串池中没有 cd,将 x1 的副本放入 "cd" false

性能调优

  • 设置桶(bucket)数目

    • JVM参数-XX:StringTableSize=桶个数
    • 桶数目越少,平均每个桶存放的元素越多,查找效率越低;
  • 使用 intern():利用串池保证字符串对象的不重复性,避免重复占用内存。

2.5、直接内存

Direct Memory

  • 属于操作系统内存,不受 JVM 内存回收管理;
  • 常见于 NIO 操作时,用于数据缓冲区;
  • 分配回收成本较高,但读写性能高。
  • 内存溢出java.lang.OutOfMemoryError: Direct buffer Memory(OOM)
  • :当一个线程抛出OOM异常后,它所占据的内存资源会被全部释放掉,不会影响其他线程。

使用

Java 只能操作堆内存,无法直接操作系统内存,效率低。

image-20220217173222574

Direct Memory 在系统内存中分配一块直接内存(allocateDirect)。

  • Java 可以直接操作直接内存;

  • 省去从系统缓存区到 Java 缓冲区的复制操作;

    image-20220217173428415

分配和回收原理

  • 主动调用 Unsafe 对象的 freeMemory() 方法,回收 Direct Memory 的分配。
  • ByteBuffer 子类:DirectByteBuffer 类
    • 使用 Cleaner(虚引用类型)监测 ByteBuffer 对象;(观察者模式)
    • 当 ByteBuffer 对象被回收,创建 ReferenceHandler 线程
      • 调用 Cleaner 的 clean() 方法;
      • clean() 方法会调用 freeMemory() 来释放直接内存。

3、相关概念

3.1、一次编译,到处运行

Java 跨平台性:一次编译,到处运行

  1. Java 程序执行的过程
    1. 编译:程序运行前,编译器将 Java 源文件(.java)编译成字节码文件(.class)
    2. 类加载:加载、链接(验证、准备、解析)、初始化
  2. 运行时
    1. 解释器:直接对代码解释执行。
    2. JIT 编译器:编译成机器码执行。

Hint

  1. 字节码无法直接运行,需通过 JVM 解释执行,或编译成机器码运行。
  2. 字节码是统一,每台机器的机器码有所不同。
  3. JVM 基于 C/C++ 开发,不具有跨平台性,需要安装相应版本 JVM。
  4. 只需在不同平台安装 JVM,无需修改源码,就能进行以上操作。

3.2、类的唯一性确定

如何唯一确定 Java 类类加载器 + 全限类名

posted @ 2022-02-24 11:38  Jaywee  阅读(50)  评论(0编辑  收藏  举报

👇