黑马JVM解析笔记(一):内存结构

1.一个JAVA对象在JVM执行的流程

可以用这张图来描述:

黑马JVM解析笔记(一):内存结构_第1张图片

  1. 首先java对象通过ClassLoader被加载进内存
  2. 然后就是到了JVM内部的结构,主要是将java类信息存储到JVM中的各个区域:
    • 方法区:存储类级别的信息,包括静态变量、方法数据和常量池等
    • 堆:存储对象实例和数组,动态的创建并管理对象的内存区域
    • JVM栈:存储线程的栈帧,栈帧中包含了方法的调用的局部变量和部分结果
    • 程序计数器:存储当前正在执行的字节码指令的地址
    • 本地方法栈:存储本地方法(例如C或C++编写的代码)调用的栈帧
  3. 执行过程:
    1. 解释器:负责逐条字节码的解析并执行
    2. 即时编译器:将字节码编译成本地机器码,以提高效率(编译过程将源代码转换为字节码,而字节码并不能直接在CPU上执行,只有机器码才能够。解释器是逐行解析代码,因此执行效率较低。而即时编译器(JIT)则通过将一些频繁执行的“热点代码”一次性编译成机器码,从而避免了重复编译相同代码的开销。这样,在遇到相同的热点代码时,JIT编译器可以直接使用已编译的机器码,显著提高执行效率。)
    3. 垃圾回收机制:负责回收不使用的对象
  4. 本地方法接口:JVM与外部本地方法进行交互的接口

2.内存结构

2.1 程序计数器(每个线程都有,线程私有的)

就是保存当前执行指令的位置,执行完当前指令自动更新。其作用:

  • 线程切换时保持当前状态,以备后续执行

  • 控制分支执行,在程序出现分支时,跳转到不同的代码,确保逻辑的正常执行

  • 异常处理和方法调用

    其特点:

  1. 是线程私有的
  2. 不会存在内存溢出的问题

2.2 虚拟机栈

2.2.1 java虚拟机栈的概念

其数据结构是先进后出,在JVM中的作用就是保存一个线程运行需要的内存空,栈中保存数据单元又被称做栈帧,每个栈帧就是一个方法运行时需要的内存。

黑马JVM解析笔记(一):内存结构_第2张图片

问题辨析:

  1. 垃圾回收是否涉及到栈内存?

    不会涉及到,它是由栈内存的生命周期和方法调用决定的

  2. 栈内存分配越大越好吗?

    不一定,电脑的物理内存是一定,栈内存越大,相对应的线程就变少了,执行效率可能比之前都差

  3. 方法内的局部变量是否线程安全?

    如果存储的东西都是栈私有的就不涉及共享变量,是线程安全的

2.2.2 栈内存溢出

什么时候会出现栈内存溢出“

  1. 栈帧过多导致,例如递归调用、循环依赖问题
  2. 栈帧过大
  3. 可以设置栈内存的大小
2.2.3 线程运行诊断(Linux环境下)

案例一:cpu占用过多

使用 top 命令确定哪个进程对CPU的占用较高

使用以下命令进一步查看哪个线程引起了CPU占用过高:

ps H -eo pid,tid,%cpu | grep 线程id

使用 jstack 进一步查看线程的详细堆栈信息:

jstack 线程id

案例二:线程执行很久没结果,一般是死锁

查看步骤和案例一一样,查询对应的进程,在看对应线程,最后看线程栈堆的信息

2.3 本地方法栈

存储本地方法(例如C或C++编写的代码)调用的栈帧

2.3 堆,通过new关键字,创建的对象都会使用堆内存

2.3.1 特点
  1. 它是线程共享的,需要考虑线程安全问题
  2. 有垃圾回收机制
2.3.2 堆内存溢出
  • 本来垃圾回收机制可以回收堆,但是如果对象被一直使用,这样就不会被回收掉,导致内存溢出,案例:死循环中使用对象
  • 可以自定义设置堆内存大小
2.3.3 堆内存使用情况
  1. jps 工具
    查看当前系统中有哪些 java 进程
  2. jmap 工具
    查看堆内存占用情况 jmap - heap 进程id
  3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测
  4. jvisualvm 工具

2.4 方法区

2.4.1 定义

方法区是JVM中用来存放类级别信息的内存区域,它在JVM内存模型中扮演着重要角色,特别是在类加载、常量存储、静态变量管理等方面。随着JVM的版本更新,方法区的实现从永久代(PermGen)转变为元空间(Metaspace),优化了内存管理和性能。

黑马JVM解析笔记(一):内存结构_第3张图片

如图所示:JVM1.6是利用永久代的方法创建方法区,其包括:常量池(StringTable放在常量池)、类文件、类加载文件等,到了1.8就采用元空间的方式,其把常量池、类文件、类加载文件放在本地内存中,由JVM提供一个索引指向这个内存地址,StringTable放在JVM的堆中

2.4.2 方法区内存溢出
  • 1.8 之前会导致永久代内存溢出
    • 使用 -XX:MaxPermSize=8m 指定永久代内存大小
  • 1.8 之后会导致元空间内存溢出
    • 使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

场景:

动态代理生成代理类其中的数据就会保存到方法区中,代理类产生太多,且没有及时的清理,这样就会导致内存溢出。

2.2.3 运行时常量池

**方法区存储信息:**二进制字节码包括(类的基本信息、常量池、类方法的定义,包括了虚拟机的指令)

  1. 类的基本信息:包括类名、父类、字段、方法、访问控制修饰符等元数据,在方法区存储。
  2. 常量池:存储字符串常量、符号引用等信息,用于提高效率和节省内存。
  3. 类方法的定义:包括实例方法、静态方法和构造方法的信息,存储在方法区。
  4. 虚拟机指令:字节码指令是方法执行的核心,JVM根据指令集逐条执行方法,完成类的操作。

**常量池:**就是一张表,虚拟机指令根据这张表,找到要执行的类名、方法名、参数类型、字符信息。常量池找不到才会创建新的常量

运行时常量池
常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

首先看这段代码在方法区保存是怎么样的结构

1.java代码
public class StringTableDemo1 {

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
    }
}
2.在方法区的结构
2.1 类的常量池:保存常见的类的基本信息(类名、字段名、方法名等——JDK8之后)、字符串常量、基本引用信息等
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."":()V   【方法引用 #2.#3 表示父类构造方法引用】
   #2 = Class              #4             // java/lang/Object				【父类的名称】
   #3 = NameAndType        #5:#6          // "":()V  【该类的构造方法引用, ()V方法签名】
   #4 = Utf8               java/lang/Object		【父类的全限定名】
   #5 = Utf8               
   #6 = Utf8               ()V
   #7 = String             #8             // a 【字符串常量引用】
   #8 = Utf8               a				【字符串常量】
   #9 = String             #10            // b
  #10 = Utf8               b
  #11 = String             #12            // ab
  #12 = Utf8               ab
  #13 = Class              #14            // com/wangxin/StringTableDemo1  【类名】
  #14 = Utf8               com/wangxin/StringTableDemo1		【该类的全限定名】
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this 【局部变量当前引用】
  #19 = Utf8               Lcom/wangxin/StringTableDemo1;【描述this是谁应用,即StringTableDemo1 类的引用】
  #20 = Utf8               main   【main方法】
  #21 = Utf8               ([Ljava/lang/String;)V 【描述main方法为 参数定义为String类型,返回为void】
  #22 = Utf8               args
  #23 = Utf8               [Ljava/lang/String; 【指定arg参数类型为String】
  #24 = Utf8               s1
  #25 = Utf8               Ljava/lang/String;
  #26 = Utf8               s2
  #27 = Utf8               s3
  #28 = Utf8               SourceFile
  #29 = Utf8               StringTableDemo1.java
2.2 类的基本信息:包括类名、父类、字段、方法、访问控制修饰符等元数据,在方法区存储。

主要是存储在元数据中(JDK8),有一些保存在常量池中

2.3 类方法的定义和它包含指令:
{
  【构造方法分描述】
  public com.wangxin.StringTableDemo1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wangxin/StringTableDemo1;

  【main方法的描述】
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #7                  // String a
         2: astore_1
         3: ldc           #9                  // String b
         5: astore_2
         6: ldc           #11                 // String ab
         8: astore_3
         9: return
      LineNumberTable:
      	【line 12,其中12表示对源代码第几行】
        line 12: 0
        line 13: 3
        line 14: 6
        line 15: 9
      【局部变量】
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1    s1   Ljava/lang/String;
            6       4     2    s2   Ljava/lang/String;
            9       1     3    s3   Ljava/lang/String;
}

3.直接内存:提高程序的I/O效率

就是在操作系统缓冲区,开辟一个区域可以有JVM直接方法。目的就是加快文件的传输JVM访问本地数据有两个缓冲区,操作系统和JVM中,经过两个缓冲区就会到传输相对比较慢

缺点

  1. 管理起来比较复杂:需要开发者进行管理,分配内存和释放内存
  2. 可能存在一些内存安全的问题:导致操作系统内存泄漏

你可能感兴趣的:(JVM,jvm,笔记,java)