注册

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

📑即将学会

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

Java从编译到执行

image.png

.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程序的过程中会把它所管理的内存 划分为若干个不同的数据区域 数据划分 image.png 而数据划分这块 依据线程私有 和 线程共享这两种进行划分

  • 线程共享区
    • 线程共享区 分为方法区 和 堆
  • 方法区 (永久代(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会在运行时数据区创建一个虚拟机栈。 在栈中 运行方法 每运行一个方法 ,会压入一个栈帧

image.png

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

image.png

-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 既

image.png

这是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。类似下图

image.png

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

image.png 然后code代码运行下一行

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

image.png

上面两条字节码 对应 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位置的数据加载到操作数栈中

image.png

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

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

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

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

image.png 此时,我们已经完成了 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),有两种情况。 当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。 image.png

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. 类加载

image.png

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

image.png 4.创建对象

image.png

流程

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

  • 堆空间分代划分

image.png

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

  • 先运行相关类

  • CMD命令行 运行jps 查看相关进程

    • image.png
  • 找到JDK安装目录 java8u292\bin bin 目录下点击HSDB.exe运行程序

  • 通过File下 点击 下图 进行 进程绑定

    • image.png
    • 将之前通过jps获取的进程号输入 输入框
    • image.png
    • 绑定后界面为 该进程下进程信息
    • image.png

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

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

    • image.png

    • 进入后界面如下所示

    • image.png

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

    • image.png

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

    • image.png

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

    • image.png

通过HSDB查看栈

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

image.png

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

内存溢出

栈溢出 StackOverflowError

方法调用方法 递归

堆溢出

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

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

image.png

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

方法区溢出

(1) 运行时常量池溢出

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

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

直接内存溢出

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


0 个评论

要回复文章请先登录注册