【JVM入门食用指南】JVM内存管理

简介: 面试都要求我们有扎实的Java语言基础。对JVM规范的理解,可以让我们更好的了解Java,并对代码进行分析。故带大家对JVM中的内存管理规进行一定的入门学习与使用。以求大家对JVM有一定的认知与理解。

📑即将学会

JVM的内存管理的相关知识点,JVM对内存管理进行了哪些规范

Java从编译到执行

.java文件经过javac编译成.class文件 .class文件通过类加载器(ClassLoader)加载到方法区 jvm执行引擎执行 把字节码翻译成机器码

解释执行与JIT执行

解释执行

JVM 是C++ 写的 需要通过C++ 解释器进行解释

解释执行优缺点

通过JVM解释 速度相对慢一些

JIT (just-in-time compilation 即时编译)(hotspot)

方法、一段代码 循环到一定次数 后端1万多 代码会走hotspot编译 JIT执行(hotspot)(JIT) java代码 直接翻译成(不经解释器) 汇编码 机器码

JIT执行优缺点

速度快 但是编译需要一定时间

JVM是一种规范

JVM两种特性 跨平台 语言无关性

  • 跨平台
  • 相同的代码在不同的平台有相同的执行效果
  • JVM语言无关性
  • 只识别.class文件 只要把相关的语言文件编译成.class文件 就可以通过JVM执行
  • 像groove、kotlin、java语言 本质上和语言没有关系,因此,只要符合JVM规范,就可以执行 语言层面上 只是将.java .kt等文件编译成.class文件

因此 JVM是一种规范

JVM 内存管理规范

运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存 划分为若干个不同的数据区域 数据划分 而数据划分这块 依据线程私有线程共享这两种进行划分

  • 线程共享区
  • 线程共享区 分为方法区 和 堆
  • 方法区 (永久代(JDk1.7 前) 元空间(JDK1.8) hotspot实现下称呼 )
  • 在JVM规范中,统称为方法区
  • 只是hotSpot VM 这块产品用得比较多 。hotSpot利用永久代 或者 元空间 实现method区 Hotspot不同的版本实现而已

几乎所有对象都会在这里分配

线程私有区 每启动一个线程划分的一个区域

直接内存 堆外内存

  • JVM会在运行时把管理的区域进行虚拟化 new 对象()

通过JVM内存管理,将对象放入堆中,使用的时候只需要找到对象的引用 就可以直接使用,比直接通过分配内存,地址寻址 计算偏移量,偏移长度更方便。

  • 而这块数据没有经过内存虚拟化 (运行时外的内存 内存8G JVM 占用5G 堆外内存3G)

可以通过某种方法 进行申请、使用、释放。不过比较麻烦,涉及分配内存、分配地址等

java方法的运行与虚拟机栈

虚拟机栈

栈的结构 存储当前线程运行Java方法所需要的数据、指令、返回地址

public static void main(String[] args) {
    A();
}
private static void A() {
    B();
}
private static void B() {
    C();
}
private static void C() {
}

比如以上代码,当我们运行main方法时,会启动一个线程,这个时候,JVM会在运行时数据区创建一个虚拟机栈。 在栈中 运行方法 每运行一个方法 ,会压入一个栈帧

  • 虚拟机栈大小限制 Xss参数指定

-Xsssize
设置线程堆栈大小(以字节为单位)。k或k表示KB, m或m表示MB, g或g表示GB。默认值取决于虚拟内存。
下面的示例以不同的单位将线程堆栈大小设置为1024kb:
-Xss1m
-Xss1024k
-Xss1048576
这个选项相当于-XX:ThreadStackSize。

栈帧

栈帧内主要包含

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 完成出口

栈帧对内存区域的影响

以以下代码为例

public class Apple {
    public int grow() throws Exception {
        int x = 1;
        int y = 2;
        int z = (x + y) * 10;
        return z;
    }
    public static void main(String[] args) throws Exception {
        Apple apple = new Apple();
        apple.grow();
        apple.hashCode();
    }
}

因为JVM识别的.class文件,而不是.java文件。因此,我们需要拿到其字节码,可以通过ASM plugin插件 右键获取 或者通过 javap -v xxx.class 获取 (本文通过javap 方式获取) 其字节码如下

Classfile /XXX/build/classes/java/mainXXX/Apple.class
  Last modified 2021-8-11; size 668 bytes
  MD5 checksum d10da1235fad7eba906f5455db2c5d8b
  Compiled from "Apple.java"
public class Apple
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#29         // java/lang/Object."<init>":()V
   #2 = Class              #30            // Apple
   #3 = Methodref          #2.#29         // Apple."<init>":()V
   #4 = Methodref          #2.#31         // Apple.grow:()I
   #5 = Methodref          #6.#32         // java/lang/Object.hashCode:()I
   #6 = Class              #33            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Apple;
  #14 = Utf8               grow
  #15 = Utf8               ()I
  #16 = Utf8               x
  #17 = Utf8               I
  #18 = Utf8               y
  #19 = Utf8               z
  #20 = Utf8               Exceptions
  #21 = Class              #34            // java/lang/Exception
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               apple
  #27 = Utf8               SourceFile
  #28 = Utf8               Apple.java
  #29 = NameAndType        #7:#8          // "<init>":()V
  #30 = Utf8               Apple
  #31 = NameAndType        #14:#15        // grow:()I
  #32 = NameAndType        #35:#15        // hashCode:()I
  #33 = Utf8               java/lang/Object
  #34 = Utf8               java/lang/Exception
  #35 = Utf8               hashCode
{
  public Apple();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Apple;
  public int grow() throws java.lang.Exception;
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 4
        line 8: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Apple;
            2      11     1     x   I
            4       9     2     y   I
           11       2     3     z   I
    Exceptions:
      throws java.lang.Exception
  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Apple
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method grow:()I
        12: pop
        13: aload_1
        14: invokevirtual #5                  // Method java/lang/Object.hashCode:()I
        17: pop
        18: return
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 13
        line 14: 18
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  args   [Ljava/lang/String;
            8      11     1 apple   Lcom/enjoy/ann/Apple;
    Exceptions:
      throws java.lang.Exception
}
SourceFile: "Apple.java"

从其字节码中 我们可以看到这么一段

public Apple();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Apple;

这是Apple的构造方法,虽然我们没有写,但是默认有无参构造方法实现

回到正文 下面我们对grow()方法做解析

public int grow() throws java.lang.Exception;
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 4
        line 8: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Apple;
            2      11     1     x   I
            4       9     2     y   I
           11       2     3     z   I
    Exceptions:
      throws java.lang.Exception

我们可以看到其code代码区域,有 0 1 2 3 既

这是grow栈帧中的字节码地址(相对于改方法的偏移量)表,当程序运行的时候,程序计数器中的数会被调换为运行这个方法的字节码的行号 0 1 2 3 [字节码行号] 而字节码的行号 对应JVM 字节码指令助记符 下面对字节码地址表中涉及的字节码行号 进行理解

0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn

首先 进入grow方法 记录进入方法时所在main()中的行号 作为完成出口,如main方法中grow方法字节码地址为3 方法完成后,接着执行完成出口的下一行字节码地址,所有的操作都在操作数栈中完成

进入grow()栈帧中。程序计数器将计数器置为0,如果该类是静态方法,则局部变量表不变,如果该类不是静态方法,则在局部变量量中加入该对象实例this。类似下图

  • 0: iconst_1
  • i 表示int const 表示常量 后面的1 表示值 ,这里表示创建int常量 1 ,放入操作数栈。

然后code代码运行下一行

  • 1: istore_1
  • 这里将程序计数器count值改为1,然后 i 表示 int ,store表示 存储, 1 表示存储下标 存储到局部变量表中1的位置 ,我们这里将操作数中值为1的int出栈放到局部变量中 存储。

上面两条字节码 对应 int X = 1

i_const_1 对应右边 定义1

i_store_1 对应左边 用一个变量X存储 1 int y = 2参考上面分析

下面我们来看看 int z = (x + y) * 10; x 和 y都在本地布局变量中有存储,因此,执行这条代码的时候,我们不需要上述步骤了,我们可以通过4: iload_1,将布局变量中1位置的数据加载到操作数栈中

下面执行code中 6: iadd,将操作数栈中的数据弹出两个操作数,再将结果存入栈顶,这个时候结果仅仅保留在操作数栈

这个时候我们已经完成了 (X + y)这步 ,接下来看 * 10这步,这个时候我们跳到 7: bipush 10这个值也是常量值,但是比较大 操作指令有点不一样,0-5用const,其它值JVM采用bipush指令将常量压入栈中。 10对应要压入的值 。

然后我们跳到下一行 9: imul .这是一个加法操作指令。我们可以看到操作号直接从7变成了9.这是因为bipush指令过大,占用了2个操作指令长度。

这个时候我们已经得到了计算结果,还需要将其赋值局部变量z进行变量存储.

此时,我们已经完成了 z = (x + y) * 10的操作了。 此时执行最后一行 return z;首先 ,取出z,将其load进操作数栈,然后利用ireturn返回结果。该方法结束。这个时候,完成出口存储的上一方法中的程序计数器的值,回到上一方法中正确的位置。

补充

0 1 2 3 4 7 9 字节码偏移量 针对本方法的偏移

程序计数器只存储自己这个方法的值

动态连接 确保多线程执行程序的正确性

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为JVM执行 Java 方法(也就是字节码)服务,而本地方法栈则是为JVM使用到的 Native方法服务。在hotSpot中,本地方法栈与虚拟机栈是一体

在本地方法栈中,程序计数器为null,因为,本地方法栈中运行的形式不是字节码


线程共享区

下面还是来一段代码

public class Learn {
    static int NUMBER = 18; //静态变量 基本数据类型
    static final int SEX = 1; //常量 基本数据类型
    static final Learn LERARN = new Learn(); //成员变量 指向 对象
    private boolean isYou = true; //成员变量
    public static void main(String[] args) {
        int x = 18;//局部变量
        long y = 1;
        Learn learn = new Learn(); //局部变量 对象
        learn.isYou = false;//局部变量 改变值
        learn.hashCode(); //局部变量调用native 方法
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(128 * 1024 * 1024);//分配128M直接内存
    }
}

类加载过程中

Learn 加载到方法区

类中的 静态变量、常量加载到方法区。

方法区

是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,

我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。

创建的时候,到底是在堆上分配,还是在栈上分配呢?

这和两个方面有关:对象和在 Java 类中存在的位置。 对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。 当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

JVM内存处理

先来一段代码进行后续分析

public class JVMObject {
    public final static String MAN_TYPE = "man"; // 常量
    public static String WOMAN_TYPE = "woman";  // 静态变量
    public static void  main(String[] args)throws Exception {
        Teacher T1 = new Teacher();
        T1.setName("A");
        T1.setSexType(MAN_TYPE);
        T1.setAge(36);
        for(int i =0 ;i < 15 ;i++){
            //每触发一次gc(),age+1 记录age的字段是4位 最大1111 对应15 
            System.gc();//主动触发GC 垃圾回收 15次--- T1存活  T1要进入老年代
        }
        Teacher T2 = new Teacher();
        T2.setName("B");
        T2.setSexType(MAN_TYPE);
        T2.setAge(18);
        Thread.sleep(Integer.MAX_VALUE);//线程休眠 后续进行观察   T2还是在新生代
    }
}
class Teacher{
    String name;
    String sexType;
    int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getSexType() {
        return sexType;
    }
    public void setSexType(String sexType) {
        this.sexType = sexType;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}
  1. JVM申请内存
  2. 初始化运行时数据区
  3. 类加载

  • 执行方法(加载后运行main方法)

4.创建对象

流程

JVM 启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法。方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。 同时在方法的执行过程中创建的对象一般情况下都是放在堆中,堆中的对象最后通过垃圾回收处理。

  • 堆空间分代划分

通过HSDB查看堆空间划分 及内存分配

  • 先运行相关类
  • CMD命令行 运行jps 查看相关进程
  • 找到JDK安装目录 java8u292\bin bin 目录下点击HSDB.exe运行程序
  • 通过File下 点击 下图 进行 进程绑定
  • 将之前通过jps获取的进程号输入 输入框
  • 绑定后界面为 该进程下进程信息

  • 通过Tools栏下的heap parameter 可以观察堆分配情况
  • 我们可以看到堆分区的划分和之前的是类似的,这样可以直观的看到堆的地址,也可以让我们对JVM将内存虚拟化有更直观的认知。

  • 对象的地址分配
  • 我们也可以通过object histogram查看对象的分配情况

  • 进入后界面如下所示

  • 我们可以通过全类名搜索相关类

  • 找到自己想要的查看的类后,可以看到 第一行表示这个类所有对象的size ,count 数量是多少个。比如标红的表示,Teacher类所有对象占用48,一共两个对象。双击这一栏,进入详细页面

  • 点击对应条目,点击下方insperctor查看详细信息,将其与之前堆内存地址分配对比,发现一个主动调用gc()从新生代慢慢进入老年代,这个A已经进入老年代了,而另一个B还在新生代。

通过HSDB查看栈

可以在HSDB绑定进程时,查看所有列出的线程信息,点击想要查看的线程,如main线程。点击浮窗菜单栏上的第二个 我们可以查看主线程的栈内存情况 ,如下图所示。

有兴趣的朋友可以去玩玩 这个工具

内存溢出

栈溢出 StackOverflowError

方法调用方法 递归

堆溢出

OOM 申请分配内存空间 超出堆最大内存空间

可以通过设置运行设置 进行模拟

可以通过设置 VM options进行设置JVM 相关参数配置参考相关链接第一个 可以通过 调大 -Xms,-Xmx参数避免栈溢出

方法区溢出

(1) 运行时常量池溢出

(2)方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们配置。

class回收条件
  • 该类所有的实例都已经被回收,堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

直接内存溢出

直接内存的容量可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样)

相关链接

java (oracle.com) 可以直观查看其中涉及的参数信息

目录
相关文章
|
23天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
41 4
SpringBoot入门(4) - 添加内存数据库H2
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
25天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
29 2
SpringBoot入门(4) - 添加内存数据库H2
|
10天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
8天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
8 1
|
18天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
59 13
|
12天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
32 4
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
27天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
46 10