ToB企服应用市场:ToB评测及商务社交产业平台

标题: 深入学习JVM-内存架构图(二) [打印本页]

作者: 何小豆儿在此    时间: 2024-12-5 17:40
标题: 深入学习JVM-内存架构图(二)
JVM深入学习-内存架构图篇

本篇聚焦于对JVM内存架构图的深度总结与分析。文中将逐一详尽先容内存架构图中的各部分,并深入理解JVM运行机制与内存管理策略。
内存架构图

JVM架构图中包罗了 类加载子系统(上篇JVM详细先容了类加载系统)、运行时数据区、执行引擎、本地接口、本地方法库。
方法区

方法区(Method Area)是Java假造机内存结构中的一个紧张组成部分,它是线程共享的区域。这意味着多个线程可以同时访问方法区中的信息。
方法区的紧张作用是存储已被假造机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。它就像一个知识库,为后续Java程序的运行提供各种信息以确保程序可以运行。
元空间替代永久代?

内存巨细:在jdk1.8之前,永久代是放在堆内存中的,也就是说在JVM内存中,但是随着项目的复杂度、框架使用、三方库的使用,永久代中需要存储的类越来越多,导致固定内存巨细的永久代无法实用,所以这里把永久代转换成元空间,并把它放到本地内存中,不占用jvm内存空间。
垃圾回收:永久代的垃圾回收相对复杂。因为它里面存储的类信息等数据的生命周期和普通对象差别,垃圾回收器很难准确判断哪些类信息是可以回收的。例如,一个类加载后,纵然没有任何实例对象存在了,只要这个类还在被其他类引用(比如通过反射),它在永久代中的信息就不能被回收。这种复杂的回收机制导致永久代的内存清理效率较低。
性能题目:永久代中的类数据和字符串常量池等内容混在一起,当举行垃圾回收大概内存整理时,会对整个永久代举行操作。永久代的垃圾回收会触发 Full GC,这好坏常耗时的过程,在高负载系统中影响较大。而元空间独立于堆内存,大大淘汰了永久代干系的 Full GC 次数,因此在运行时淘汰了长时间的中断。
方法区和其他内存结构的关系?
栈区

栈区是JVM内存结构中的一个紧张组成部分,负责管理方法调用和执行时的数据存储。
在Java假造机中,栈与线程密切干系。每个线程在创建时都会分配一个JVM栈。这个栈用于存储方法调用时的干系信息,包罗局部变量表、操作数栈、动态链接、方法返回地址。
JVM栈中的每个方法调用都会创建一个栈帧(Stack Frame),栈帧是栈中的基本存储单元,用于存放方法调用时的信息。

下面我将以一段代码简述栈帧中存储的各个部分的寄义:
  1. package focus.total;
  2. public class JVMStudy {
  3.     private static final int initData = 6;
  4.     public static User user = new User();
  5.     public int compute(){
  6.         int a=1;
  7.         int b=2;
  8.         int c = (a+b)*10;
  9.         return c;
  10.     }
  11.     public static void main(String[] args) {
  12.         JVMStudy jvmStudy = new JVMStudy();
  13.         jvmStudy.compute();
  14.         System.out.println("计算完成");
  15.     }
  16. }
复制代码
当上述程序被执行时,JVM假造机会分配一个Main主线程,并为该线程分配一个栈内存空间,并创建main栈帧,程序计数器初始化到main方法字节码指令的第一条,并按照顺序执行,当执行到compute()方法时,创建compute方法的栈帧。在cmopute方法中我将对栈帧中的存储结构举行逐个先容。
反汇编JVMStudy.class代码观察JVM执行的指令
下面关于字节码指令的解释其实就已经把局部变量表和操作数栈的寄义解释清晰了。
  1. Compiled from "JVMStudy.java"
  2. public class focus.total.JVMStudy {
  3.   public static focus.total.User user;
  4.   public focus.total.JVMStudy();
  5.     Code:
  6.        0: aload_0
  7.        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
  8.        4: return
  9.   public int compute();
  10.     Code:
  11.     //iconst_1 : 将int类型的1压入操作数栈中
  12.     //istore_1 : 将int类型的值存入局部变量表中
  13.     //两个组合起来其实就是我们首先在我们的操作数栈中存储int值1,然后在局部变量表中存入a,然后将操作数栈中的值取出,赋给局部变量表中的a  ----  int a=1;
  14.        0: iconst_1
  15.        1: istore_1
  16.     //iconst_2 : 同上,区别是值为2
  17.     //istore_2 : 同上
  18.     // 这里思考一个问题,如果此时发生了线程切换,那么当重新返回这个线程时,如何知道从哪里继续执行?
  19.     //程序计数器:记录下一行即将运行的代码的内存地址。
  20.     // 程序计数器是每个程序在运行时都会给他分配的一段内存空间代码,存放下一次即将运行的指令内存地址。
  21.     // 那么当我们程序在执行3的这行指令时,来了一个优先级高的线程将cpu抢占过去,那么此时该线程会执行完3指令之后,在程序计数器+1,然后让出CPU,并挂起。当抢占的线程运行完毕之后,该线程重新拿到cpu使用权,此时就会按照程序计数器中存储的位置区执行。
  22.        2: iconst_2
  23.        3: istore_2
  24.    // iload_1 : 将局部变量表中第1个位置的整数值加载到操作数栈顶
  25.    // iload_2 : 从局部变量2中装载int类型值
  26.    // iadd :执行int类型的加法
  27.    // 这三个指令其实就是 ,从局部变量表中分别取出a的值和b的值放入操作数栈,然后调用iadd命令,将两个操作数取出操作数栈并完成加法指令。把结果重新压回我们的操作数栈。
  28.    // 此时我们的操作数栈中存放了 int 值 3
  29.        4: iload_1
  30.        5: iload_2
  31.        6: iadd
  32.     // bipush :向操作数栈中放入 int 值 10
  33.     // imul : 乘法 3 * 10
  34.        7: bipush        10
  35.        9: imul
  36.     // istore_3 : 将栈顶的整数值存储到局部变量表的第3个位置。
  37.     // iload_3 :  将局部变量表中第3个位置的整数值加载到操作数栈顶。
  38.       10: istore_3
  39.       11: iload_3
  40.       12: ireturn
  41.   public static void main(java.lang.String[]);
  42.     Code:
  43.        0: new           #2                  // 创建一个新的JVMStudy对象
  44.        3: dup                                // 复制栈顶的JVMStudy对象引用
  45.        4: invokespecial #3                  // 调用JVMStudy的构造方法 "<init>":()V
  46.        7: astore_1                           // 将栈顶的JVMStudy对象引用存储到局部变量1
  47.        8: aload_1                            // 将局部变量1中的JVMStudy对象引用加载到栈顶
  48.        9: invokevirtual #
  49.            
  50.            
  51.            4                  // 调用JVMStudy对象的compute方法,返回一个int
  52.       12: pop                                // 弹出栈顶的int返回值(不使用)
  53.       13: getstatic     #5                  // 获取System类的out字段(PrintStream对象)
  54.       16: ldc           #6                  // 将字符串 "计算完成" 压入栈顶
  55.       18: invokevirtual #7                  // 调用PrintStream的println方法,打印字符串
  56.       21: return                             // 从main方法返回
  57.   static {};
  58.     Code:
  59.        0: new           #8                  // 创建一个新的User对象
  60.        3: dup                                // 复制栈顶的User对象引用
  61.        4: invokespecial #9                  // 调用User的构造方法 "<init>":()V
  62.        7: putstatic     #10                 // 将栈顶的User对象引用存储到静态字段user
  63.       10: return                             // 从静态初始化块返回
复制代码
动态链接:在 Java 假造机的运行机制中起着关键作用。在之前论述对方法区的理解时,就已经涉及到栈与方法区之间存在的动态链接关系。我们都知道,在类加载阶段的分析过程中,会完成符号引用到直接引用的转换,这一转换实际上就是将方法区中的常量池厘革为运行时常量池的过程。而这里所说的动态链接,其核心操作便是把方法的符号引用借助动态链接这种方式,准确地链接到方法在内存中的实际地址,从而为方法的成功调用奠定基础,确保在程序运行过程中,当需要调用某个方法时,能够通过这种动态链接机制迅速定位到方法的实际执行代码地点的内存位置并顺利执行。
那么此时就有一个疑问,在类加载阶段就已经完成转换了,为什么这里还需要举行转换?
那是因为这里的动态链接,紧张用于处理在编译时无法确定具体调用目标类型的情况,特殊是在多态(虚方法调用)下发挥作用。
多态场景下的动态链接示例(以Animal为例)
动态链接与其他概念的关联

方法返回地址
就如下,我们调用compute方法时,就会在方法返回地址中记录,用于存储compute方法返回后的地址。
  1. public static void main(String[] args) {
  2.     JVMStudy jvmStudy = new JVMStudy();
  3.     jvmStudy.compute();
  4.     System.out.println("计算完成");
  5. }
复制代码
程序计数器

程序计数器是JVM内存模型中的一个紧张部分,它是线程私有的,也就是说每个线程都会按照本身的程序计数器指向指令去按顺序执行。
程序计数器的紧张职责是告诉JVM接下来应该执行哪条字节码指令。
在类加载阶段,程序计数器尚未发挥作用。而当程序启动时,主线程的程序计数器会初始化为指向 Main 方法字节码指令的首条。随后,随着字节码指令的渐渐执行,它持续更新,每执行完一条指令,便精准指向下一条指令地址,以此确保方法中的字节码指令依序执行。若遭遇 if - else 分支语句,程序计数器会依据判断结果跳转至相应的字节码指令地址;当遇到方法调用时,它会先留存当前方法当前字节码指令的地址,而后跳转至被调用方法内继续执行,待被调用方法执行完毕,再重新回到之前留存的位置,从而保障程序执行流程的连贯性与准确性。
堆区

堆区(Heap Area)是JVM内存模型中的一个紧张部分,它是线程共享的,这意味着多个线程可以同时访问该区域,获取对象、数组等干系信息。
堆区紧张是用于存储Java对象实例(包罗数组对象),在程序执行过程中,通过new关键字创建的对象都会在堆区种分配内存空间。
示例代码:
  1. Person person = new Person();
  2. person.setName("张三")
  3. person.setAge(22);
复制代码
当程序运行完这段代码后,就会在堆种存储person对象及name和age属性信息,在栈种存储Person类型的对象引用,该引用指向堆内存种实际的存储地址。
关于堆的内存模型:
在Java8中可以看到堆内存被分别为年轻代和老年代

简述对象在堆中的一个简单进程:
当程序new一个新的对象,就会把它放在堆中的Eden区,但是当Eden区域放满之后,就需要举行GC -- (minor gc)

这个gc是由执行引擎后台发起一个垃圾网络线程,去堆Eden中的对象举行回收(可达性算法),在回收的过程中假如仍有对象被引用那么就将这些对象复制到 幸存区(此中一个空的,这两者肯定有一个大概两个都是空的) ,然后就这样gc回收,假如一个对象在经过 15 次垃圾回收后依然存活于幸存区中,那么就会将这个对象放到老年代中。此后GC -- (full gc)也会对老年代的垃圾举行回收。
一段代码观察堆内存溢出的情况
下面这段代码肯定会内存溢出,因为我们新new的对象都是存放在lists集合中,而lists又是在main方法栈帧中的变量,是一个GC Root,所以这些新new的对象都不会被回收!!
  1. public class HeapTest{
  2.         public static void main(String[] args){
  3.                 ArrayList<HeapTest> lists = new ArrayList<>();
  4.                 while(true){
  5.                         lists.add(new HeapTest());
  6.                         Thread.sleep(5);
  7.                 }
  8.         }
  9. }
复制代码
使用jvisualvm举行检测
本地方法栈

在Java假造机(JVM)中,本地方法栈(Native Method Stack)是专门为本地方法(native methods)服务的内存区域。当一个线程调用本地方法时,会使用本地方法栈来执行这些方法。
当Java程序调用本地方法时,JVM会保存当前栈帧,然后在本地方法栈空间中创建当前本地方法的栈帧,通过JNI调用本地方法,本地方法执行完毕之后,JVM回到之前的栈帧,继续执行Java代码。
简单一句话就是执行本地方法的。
示例:本地方法栈的使用
以下是一个简单的本地方法示例,展示了如何使用本地方法栈:
  1. public class NativeExample {
  2.     // 声明本地方法
  3.     public native void nativePrint();
  4.     static {
  5.         // 加载本地库
  6.         System.loadLibrary("NativeExample");
  7.     }
  8.     public static void main(String[] args) {
  9.         new NativeExample().nativePrint();
  10.     }
  11. }
复制代码
  1. 假设对应的C代码如下:
  2. #include <jni.h>
  3. #include <stdio.h>
  4. #include "NativeExample.h"
  5. // 实现本地方法
  6. JNIEXPORT void JNICALL Java_NativeExample_nativePrint(JNIEnv *env, jobject obj) {
  7.     printf("Hello from native code!\n");
  8. }
复制代码
执行引擎

执行引擎中包罗相识释器、JIT即时编译器、垃圾回收
解释器:解释器是执行引擎的一个紧张组成部分,它的紧张工作方式是逐行读取字节码指令并举行解释执行。例如,当遇到字节码指令中的iload(将局部变量加载到操作数栈)时,解释器会根据指令的参数,从局部变量表中找到对应的变量并将其加载到操作数栈中,这个过程是一个一个指令依次举行的。
JIT即时编译器:JIT 即时编译器是为了进步 Java 程序的执行效率而引入的。它会在程序运行过程中,对那些频仍执行的热点代码(通过一些动态监测机制确定)举行编译。这个编译过程是将字节码转换为机器码,这样在后续执行这些代码时,就可以直接执行已经编译好的机器码,而不是每次都通过解释器解释字节码。
垃圾回收:这块紧张是针对堆内存中的垃圾对象回收,以免随着程序的运行对象越来越多导致OOM,具体的GC会在下一篇JVM深入学习中提到。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4