【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) 可以直观查看其中涉及的参数信息

目录
相关文章
|
13天前
|
存储 Linux Android开发
Volatility3内存取证工具安装及入门在Linux下的安装教程
Volatility 是一个完全开源的工具,用于从内存 (RAM) 样本中提取数字工件。支持Windows,Linux,MaC,Android等多类型操作系统系统的内存取证。针对竞赛这块(CTF、技能大赛等)基本上都是用在Misc方向的取证题上面,很多没有听说过或者不会用这款工具的同学在打比赛的时候就很难受。以前很多赛项都是使用vol2.6都可以完成,但是由于操作系统更新,部分系统2.6已经不支持了,如:Win10 等镜像,而Volatility3是支持这些新版本操作系统的。
|
2天前
|
存储 Java C++
Java虚拟机(JVM)在执行Java程序时,会将其管理的内存划分为几个不同的区域
【6月更文挑战第24天】Java JVM管理内存分7区:程序计数器记录线程执行位置;虚拟机栈处理方法调用,每个线程有独立栈;本地方法栈服务native方法;Java堆存储所有对象实例,垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息;运行时常量池存储常量;直接内存不属于JVM规范,通过`java.nio`手动管理,不受GC直接影响。
14 5
|
22小时前
|
存储 Java 对象存储
jvm内存模型剖析
当线程cpu时间片执行完后,线程进入休眠状态,当再次唤醒时,通过程序计数器确定指令执行到哪一行,然后继续往下执行。
12 1
|
2天前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
10 2
|
6天前
|
监控 算法 Java
Java虚拟机(JVM)使用多种垃圾回收算法来管理内存,以确保程序运行时不会因为内存不足而崩溃。
【6月更文挑战第20天】Java JVM运用多种GC算法,如标记-清除、复制、标记-压缩、分代收集、增量收集、并行收集和并发标记,以自动化内存管理,防止因内存耗尽导致的程序崩溃。这些算法各有优劣,适应不同的性能和资源需求。垃圾回收旨在避免手动内存管理,简化编程。当遇到内存泄漏,可以借助VisualVM、JConsole或MAT等工具监测内存、生成堆转储,分析引用链并定位泄漏源,从而解决问题。
17 4
|
6天前
|
存储 安全 Java
SpringSecurity6从入门到实战之初始用户如何存储到内存
Spring Security 在 SpringBoot 应用中默认使用 `UserDetailsServiceAutoConfiguration` 类将用户信息存储到内存中。当classpath有`AuthenticationManager`、存在`ObjectPostProcessor`实例且无特定安全bean时,此配置生效。`inMemoryUserDetailsManager()`方法创建内存用户,通过`UserDetails`对象填充`InMemoryUserDetailsManager`的内部map。若要持久化到数据库,需自定义`UserDetailsService`接口实
|
8天前
|
算法 Java
Java垃圾回收(Garbage Collection,GC)是Java虚拟机(JVM)的一种自动内存管理机制,用于在运行时自动回收不再使用的对象所占的内存空间
【6月更文挑战第18天】Java的GC自动回收内存,包括标记清除(产生碎片)、复制(效率低)、标记整理(兼顾连续性与效率)和分代收集(区分新生代和老年代,用不同算法优化)等策略。现代JVM通常采用分代收集,以平衡性能和内存利用率。
34 3
|
13天前
|
算法 安全 Java
JVM系列4-垃圾收集器与内存分配策略(二)
JVM系列4-垃圾收集器与内存分配策略(二)
20 0
JVM系列4-垃圾收集器与内存分配策略(二)
|
20天前
|
存储 Java
JVM内存结构(4)
JVM内存结构
19 1
|
22小时前
|
存储 缓存 算法
JVM对象创建与内存分配机制
该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
5 0