【jvm系列-03】精通运行时数据区私有区域---虚拟机栈、程序计数器、本地方法栈

简介: 【jvm系列-03】精通运行时数据区私有区域---虚拟机栈、程序计数器、本地方法栈

深入理解运行时数据区的内容

1,运行时数据区的组成部分

在jvm的整个内存结构中,通过类加载器的子系统,将字节码文件加载到运行时数据区中。


f4b9f0e4fa36497f963205241acbabe5.png


在运行时数据区中,主要包含方法区,堆,虚拟机栈,本地方法栈和程序计数器,同时运行时数据区中还存在与其他区域的交互。在jdk1.8之后,方法区又被称为元空间


e188f33a08df4b7fb01488e93dfb2646.png


在java虚拟机中,定义了若干程序在运行时期间会使用到这个运行时数据区,期中有一些会随着虚拟机的启动而创建,随着虚拟机的退出而销毁,即和当前进程的生命周期是一样的。另外也存在一些是与线程一一对应的,这些线程对应的数据区域会随着线程开始和结束而创建和销毁。


通过上图运行时数据区的内容分可知,红色部分是线程共享的,会随着虚拟机的创建而创建,销毁而销毁,灰色部分是线程私有的。


🖐 线程私有:程序计数器,本地方法栈和虚拟机栈


🖐 线程共享:堆,堆外内存(永久代或者元空间、代码缓存等)


2,程序计数器

2.1,程序计数器概述

程序计数器,又被称为PC寄存器,英文名为Program Counter Register,类似于CPU寄存器的一个模拟,用于存储指令相关的现场信息,CPU只有吧数据装在到寄存器才能够运行。


程序计数器主要是用来存储指向下一条指令的地址,也是即将要执行的指令代码,有执行引擎读取下一条指令。每个线程有属于自己的程序计数器,生命周期与当前线程的生命周期一致。


它是持续的控制流的指示器,分支,循环,跳转,异常等基础功能都是通过这个计数器来完成的,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。


603fd406a4434f638421fb29a9181bef.png


2.2,程序计数器的作用

主要是在多线程的场景下,如果出现资源抢占,CPU就会出现轮换以及线程的切换,当前线程就会出现挂起的情况,因此可以通过记录这个行号,再次运行该线程时,就不需要从头开始运行,只需要从记录的行数再次运行即可。


每个线程都会记录当前线程运行到哪一行,因此需要给每个线程一个程序计数器,因此程序计数器属于线程私有。


3,虚拟机栈

3.1,虚拟机栈的基本概述

在内存中,栈是运行时的单位,而堆是存储单位。栈解决的是程序的运行问题,即程序如何执行,数据如何处理;而堆解决的是数据的存储问题,即数据应该怎么放,放哪儿。


虚拟机栈是线程私有的,因此每个线程都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法的调用,其生命周期个线程是一致的。


虚拟机栈主管Java程序的运行,用于保存方法的局部变量,部分结果,并参与方法的调用和返回。


3.2,虚拟机栈的特点

🖐 快速有效的分配存储方式,访问速度仅次于程序计数器


🖐 主要操作只有两个,分别是入栈和出栈


🖐 对于栈来说不存在垃圾回收问题,如GC,OOM等


3.3,栈中可能出现的异常

🖐 StackOverflowError:栈溢出


🖐 OutOfMemoryError:没有足够的内存异常


设置栈的大小:-Xss1024k


3.4,栈运行的原理

🐵 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另一个线程的栈帧


🐵 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给当前栈帧,接着虚拟机就会废弃当前栈帧,使得前一个栈帧重新成为当前栈帧


🐵 Java方法有两种返回函数的方式,一种是正常的函数返回,使用的是return指令;另一种是在没有处理异常的时候抛出异常。不管使用那种方式,都会导致栈帧被弹出。


3.5,栈帧的内部结构

在栈帧中主要由五部分组成,分别是局部变量表,操作数栈,方法返回地址,动态链接和一些附加信息等。


c422e9c16d114618a814a1d575cc23da.png


这五部分的大小影响着栈帧的大小,而栈帧的大小同时也影响着栈帧个数的多少。


3.6,局部变量表(重点)

Local variables:局部变量表,又被称为局部变量数组或者本地变量表。


定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型主要包括各种基本的数据类型,对象引用以及returnAddress类型。

public void test(int i,int j){
    String m;
    String  n;
}

由于局部变量表是建立在线程的栈上,栈中的线程是私有的数据,因此不存在数据的安全问题。


局部变量表所需要的容量大小是在编译期间就被确认下来,并且在运行期间是不会修改局部变量表的大小的。


局部变量表中的变量只在当前方法中调用有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束之后,随着方法栈帧的销毁,局部变量表也会随之销毁。


3.6.1,槽

在讲解这个槽之前,再先了解一下这个jclasslib对实例方法的使用,在对这个类进行编译之后,然后打开查看这个bytecode,在这个SlotTest类中,定义了一个main方法和一个test1的实例方法

a5af62f27465432f820e5538a1626a8d.png



然后可以直接分析这个右边Methods下面的test1方法中的Code属性,可以发现右边存在三个字段,分别是ByteCode,Exception和misc。


byteCode指的是反编译的字节码指令,左边白色编号1-19部分代表的是在代码中出现的位置,右边红色的编号0-32代表的是字节码指令的位置;

d01b553001634cb486f15da9300b1105.png


Exception table指的是出现的异常情况;


misc中第一个字段表示的是版本,第二个字段表示的是出现的变量的个数,第三个字段表示的是字节码之类的长度。

f8f8a5863e9a4801b6e67555cc76478e.png



在这个Code下面,存在两个字段,分别是LineNumberTable和LocalVariableTable这两个属性,LineNumberTable中的详细如下,主要指的是字节码指令个代码出现的位置的一一映射


ca2da4d24eaa4f8d9a660dbf86e7bed1.png


LocalVariableTable的详细信息如下,主要是指的是一些变量的个数以及对应的值。


10513624929d4c2eb34689da64753603.png


好了,在了解这个字节码的反编译是如何操作的之后,接下来再详细的了解一下这个重点内容槽。在局部变量表中,其最基本的存储单元是Slot(变量槽),而32位内的类型占一个槽,64位类型占两个槽,其中引用类型也是占32位,但是Long和Double占两个slot。


jvm会为局部变量表中的每一个Slot分配一个访问索引,通过这个索引就可以成功的访问到局部变量表中指定的局部变量值。


当存在一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表的每一个slot上。


如果是当前帧是由构造方法或者是实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的按照参数顺序表继续,这就是为啥我上面要先说明这个jclasslib的实例方法的各个参数了,如下面的这个test1中,这个this是存在这个index下标为0处的

d37c2bd11a14459eaf74d0f859f6275c.png



接下来再在这个方法里面加一个构造方法和一个静态的类方法

public SlotTest(){
    int j = 10;
    System.out.println(j);
}
public static void test2(){
    System.out.println("hello jvm!");
}

可以发现这个构造方法是在init中的,其也存在这这个this,并且存放在这个index下标为0的地方

530b9b79326145cca073ff10d2e19cfe.png



但是这个static的这个test2方法中,是没有这个LocalVariableTable属性的,因此也就没有this这个字段


37ef13cfd8154321a6d4f7702e75de7a.png


因此可以说明,在实例方法和构造方法中,其局部变量表示存在这个this字段的,而静态方法中的局部变量表是不存在这个this字段的,因此这就说明了为什么可以在实例方法和构造方法中使用this这个字段,而不能在类方法中使用这个this字段了。


在栈帧中,如果变量出了这个作用域,那么该槽位也能被重复利用。


3.6.2,静态变量与局部变量

在变量的分类中,主要是按两种方式进行分类,一种是按照数据类型分,一种是按照类中声明的位置进行分类。


按照类型:主要分为基本数据类型的变量和引用数据类型的变量


按照声明的位置:又可以分为成员变量和局部变量


🐶 成员变量在使用前,都会经历过默认的初始化赋值,如类变量在准备阶段有一个默认的赋值,在初始化阶段有一个真正的赋值,还有实例变量会随着对象的创建,会在堆空间中分配实例变量空间,并进行默认的赋值。

🐶 而在局部变量中,在使用局部中的变量时,必须给这些局部变量进行显示的赋值,否则会直接出现编译不通过


在栈帧中,与性能调优关系最为密切的部分就是局部变量表,在方法执行的时候,虚拟机使用局部变量表完成方法的传递。


局部变量表中的变量也是重要的垃圾回收的根节点,只要被局部变量表中的直接或者间接引用的对象都不会被回收


3.7,操作数栈

3.7.1,操作数栈基本概念

每一个栈帧中除了包含局部变量表之外,还包含一个先进先出的操作数栈,在方法执行过程中,会根据字节码指令,往栈中写入数据或者提取数据,即入栈(push)和出栈(pop)的操作。


这些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用后再把他们的结果压入栈。


操作数栈主要用于保存计算中间的结果,同时作为计算过程中变量临时的存储空间。


操作数栈是随着方法的执行而创建的,其生命周期和方法的生命周期一致,并在编译期间就被确定其大小


操作数栈并不是采用访问索引的方式来访问数据的,而是只能通过标准的入栈和出栈操作一次完成


3.7.2,操作数栈具体分析

如再在这个类中定义一个test的方法,其代码如下,主要有下面三个参数,接下来通过这个字节码指令分析一下

public void test(){
    int i = 15; //byte,short,char,boolean都以int类型保存在数组中
    int j = 8;  
    int k = i + j;
}

在这个Bytecode中,可以发现其字节码指令如下,依次加载15、8然后再相加再存储,并且整个流程需要程序计数器来实现代码的下移运行。

 0 bipush 15  //将15入栈
 2 istore_1   //出栈,将值在存储局部变量表的index为1的slot位置,为0的位置为this
 3 bipush 8   //将8入栈
 5 istore_2   //出栈,将值在存储在局部变量表的index为2的slot位置
 6 iload_1    //取出局部变量表的index为1位置的值,加入到栈中
 7 iload_2    //取出局部变量表的index为2位置的值,加入到栈中
 8 iadd       //8和15出栈,执行相加操作
 9 istore_3   //存储到局部变量表中
10 return     //返回

而通过这个流程也可以发现这个操作数栈只是一个中间过程,入栈之后还是得出栈将值加入到这个局部变量表中,主要还是因为这个栈可以保证先后顺序性,同时在计算复杂的四则运算的时候,这个栈的优势就被体现出来了。


通过这个字节码中的LocalVariableTable表中的值也可以看到各个参数所分步在slot槽点的位置


d29bc554d74341d493ce1188166561d8.png


如果被调用的方法中带有返回值的话,其返回值将会被压入栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令。


并且操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间再次进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段需要再次验证。


3.7.3,栈顶缓存技术

在基于栈式架构的虚拟机所使用的零地址指令更加的紧凑,但是完成一项操作的时候必然需要更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派次数和内存的读写次数。


由于操作数是存储在内存中的,因此频繁的执行内存读写操作必然会影响执行速度,因此在JVM中引入了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,降低对内存的读写次数,从而提升执行引擎的执行效率。


3.8,动态链接

每一个栈帧内部包含一个指向运行时常量池中该栈帧方法所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)


在Java源文件被编译到字节码文件的时候,所有的变量和方法引用都作为符号引用保存在class文件的常量池中,比如描述一个方法调用了另外一个方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转化为调用方法的直接引用。


大部分的字节码指令,在执行的时候,都需要进行常量池的访问,而这个动态链接,又被称为是指向运行时常量池的方法引用。


如在这个test3方法中,调用了这个test方法,同时也引用了这个全局变量进行一个自增的操作

public void test3(){
    test();
    k++;
}

接下来再次查看一下这个反编译文件,其字节码指令如下,在加载这个this变量之后,会有一个invokevirtual操作,后面也有一个#7,再后面就是表明改行对应的就是调用的test方法,接下来主要分析这个#7


 0 aload_0
 1 invokevirtual #7 <com/tky/jvm/neicun/SlotTest.test>
 4 pop
 5 getstatic #8 <com/tky/jvm/neicun/SlotTest.k>
 8 iconst_1
 9 iadd
10 putstatic #8 <com/tky/jvm/neicun/SlotTest.k>
13 return

在这个反编译插件的第二个属性中,就有着这个Constant Pool的这个运行时常量池,而上面的#7,就是对应的这个07,其就是一个Methodref,就是一个方法的引用,然后可以依次的通过右边的cp info #9,#46等依次往下找,就可以找到对应的引用。下面的#8也是一样的道理

f9b88d0a66c94cd199f09ba0c0836575.png



这说明了啥,之前定义的变量和方法没有显示的加载到常量池中,但是字节码指令是直接去常量池中获取数据的,说明了jvm内部会对每个方法或者变量,都会将他的数据引用作为符号引用加载到运行时常量池中,相当于做一个缓存,后面别的方法要用时,可以直接去常量池里面找。因此叫做指向运行时常量池的方法引用更加贴切。


而字节码文件中的常量池,在运行起来之后,就会保存在方法区中。


739a0b65e8cb417c84dc006aacc384c7.png


其本质也是利用了封装的思维,假设有100个方法都要和test3一样,如果不利用符号引用,而是在每个文件的字节码中都加入有关test方法的字节码指令,那么每个字节码文件都会非常的大,然后就把这个test的字节码指令抽离出去,加到这个运行时常量池中,那么这100个文件要使用这个test方法的字节码指令,直接去运行时常量池中找即可,从而减少文件中字节码指令的数量以及文件的大小。


3.9,方法的调用

3.9.1,静态绑定和动态绑定

在jvm中,将符号引用转化为调用方法的直接引用与方法的绑定机制有关。符号引用就是字节码指令中的#8,直接引用就是这个#8或者通过#8一直找,所找到的对应的内容,符号引用转直接引用的方式主要分为两种,一种是静态链接,一种是动态链接


静态链接


静态链接指的就是在一个字节码文件被加载到jvm内部时,如果被调用的目标方法在编译期间可以确定下来,且运行期间保持不变,那么这种情况下将调用方法的符号引用转化为直接引用的过程就被称为静态链接,同时也可以被称为早期绑定


动态链接


这里的动态链接和3.8的是同一个,如果被调用的方法在编译期间无法被确定下来,也就是说,只能够在程序运行其将调用方法的符号引用转化为直接引用,由于这种引用的转换工程具备动态性,因此也就被称为动态链接,也可以被称为晚期绑定


3.10,方法的返回地址

存储的是该方法的程序计数器的值, 在方法退出之后,都会返回到该方法被调用的位置,方法正常退出时,调用者的程序计数器的值就作为返回地址,如果是异常退出,那么返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息的。


本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置程序计数器值等。


正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何返回值


当一个方法开始执行时,只有两种方式可以退出这个方法


执行引擎遇到return,会有返回值传递给上层方法的调用者,简称正常完成出口

在方法执行过程中遇到了异常,并且这个异常没有被处理,也会导致方法退出,简称异常完成出口

3.11,虚拟机栈的5道面试题

1,举例栈溢出情况


当往栈空间中不断的加栈帧,当栈空间满的时候,就会出现这个StackOverflowError的情况。可以通过这个-Xss设置栈空间的大小,如果设置的是固定的大小,当栈空间不足就会直接的抛栈溢出的错误;如果是设置的动态的大小时,当栈空间不断扩大,最终会抛出OOM的异常。


2,调整栈大小,就能保证不出现溢出情况吗


不能保证。如果某个方法是死循环,无限的增加栈帧,最终还是会出现这个栈溢出的情况的


3,分配栈内存越大越好吗


理论上越大,出现的这个栈溢出的概率就会变小。但是如果栈变大,会导致其他的资源变少


4,垃圾回收是否会涉及到虚拟机栈


不会。栈不需要GC,只需通过出栈的方式,栈帧就像垃圾一样被清除了。


5,方法中定义局部变量是否为线程安全


有可能存在线程不安全的问题,如果变量的生命周期在方法背部产生并且在内部消亡,那么属于线程安全,否则,都是线程不安全的。


4,本地方法接口

本地方法:该方法由非java语言实现,比如C语言,指的就是一个Java调用非Java代码的接口。

public native void test(int x); 

为什么要用native


java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者对程序的效率很在意时,就可以考虑使用这个native了


4.1,与Java环境外交互

有时java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。如操作系统或者某些硬件交换信息时的情况。本地方法就是这样的一种交流机制:提供一个简洁的接口,而且无需去了解Java应用之外的繁琐的细节


4.2,与操作系统交互

操作系统的底层都是使用这个c或者c++编写的,有时为了解决效率上的问题,可以直接使用一些本地方法,从而实现这个jre和操作系统底层的交互,并且在jvm中,有一些接口就是直接使用这个C来编写的。如果要使用一些java语言本身就没有提供封装的操作系统的特性时,我们也需要使用这个本地方法


4.3,Sun`s Java

Sun的解释器是由C实现的,这使得他像普通的C一样与外部交互。jre大部分是Java实现的,但是也会通过一些本地方法与外界交互。例如类Java.lang.Thread的 setPriority() 方法就是用Java实现的,但是他的实现调用的是该类的本地方法setPriority()


5,本地方法栈

在运行时数据区中,还存在一个线程私有的区域,就是本地方法栈。Java虚拟机栈是用于管理Java方法的调用,而本地方法栈是用于管理本地方法栈的调用。本地方法也是通过C语言实现


在运行时数据区中,本地方法栈也是允许被实现成固定或者是可动态扩展的内存大小


如果线程请求分配到的栈容量超过本地方法栈允许的最大容量的时候,会抛出一个StackOverflowError

如果是动态扩展的,并且无法申请到足够的内存,那么会抛出一个OOM的异常

本地方法栈主要是和本地方法接口和本地方法库打交道的,主要是对本地方法接口和本地方法库中的方法进行入栈和出栈的操作


当某个线程调用一个本地方法栈的时候,它就进入了一个全新的并且不受虚拟机限制的世界,它和虚拟机拥有相同的权限


本地方法时可以通过本地方法接口来访问虚拟机内部的运行时数据区

可以直接使用本地处理器的寄存器

可以直接从内存的堆中分配任意数量的内存

当然并不是所有的JVM都支持本地方法,因为Java虚拟机规范中也没有明确的要求本地方法栈所使用的语言等,如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。在HotSpot JVM中,直接将本地方法栈和虚拟机栈给合二为一了。


相关文章
|
1月前
|
Java
jvm复习,深入理解java虚拟机一:运行时数据区域
这篇文章深入探讨了Java虚拟机的运行时数据区域,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、元空间和运行时常量池,并讨论了它们的作用、特点以及与垃圾回收的关系。
64 19
jvm复习,深入理解java虚拟机一:运行时数据区域
|
1月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
46 3
|
3月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
27 3
|
3月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
3月前
|
消息中间件 设计模式 安全
多线程魔法:揭秘一个JVM中如何同时运行多个消费者
【8月更文挑战第22天】在Java虚拟机(JVM)中探索多消费者模式,此模式解耦生产与消费过程,提升系统性能。通过`ExecutorService`和`BlockingQueue`构建含2个生产者及4个消费者的系统,实现实时消息处理。多消费者模式虽增强处理能力,但也引入线程安全与资源竞争等挑战,需谨慎设计以确保高效稳定运行。
94 2
|
3月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
194 0
|
4月前
|
存储 算法 Java
(四)JVM成神路之深入理解虚拟机运行时数据区与内存溢出、内存泄露剖析
前面的文章中重点是对于JVM的子系统进行分析,在之前已经详细的阐述了虚拟机的类加载子系统以及执行引擎子系统,而本篇则准备对于JVM运行时的内存区域以及JVM运行时的内存溢出与内存泄露问题进行全面剖析。
|
17天前
|
存储 SQL 数据库
虚拟化数据恢复—Vmware虚拟机误还原快照的数据恢复案例
虚拟化数据恢复环境: 一台虚拟机从物理机迁移到ESXI虚拟化平台,迁移完成后做了一个快照。虚拟机上运行了一个SQL Server数据库,记录了数年的数据。 ESXI虚拟化平台上有数十台虚拟机,EXSI虚拟化平台连接了一台EVA存储,所有的虚拟机都存放在EVA存储上。 虚拟化故障: 工组人员误操作将数年前迁移完成后做的快照还原了,也就意味着虚拟机状态还原到数年前,近几年数据都被删除了。 还原快照相当于删除数据,意味着部分存储空间会被释放。为了不让这部分释放的空间被重用,需要将连接到这台存储的所有虚拟机都关掉,需要将不能长时间宕机的虚拟机迁移到别的EXSI虚拟化平台上。
94 50
|
1月前
|
安全 虚拟化 数据中心
Xshell 连接 VMware虚拟机操作 截图和使用
Xshell 连接 VMware虚拟机操作 截图和使用
54 4
|
1月前
|
Linux 虚拟化
vmware虚拟机安装2024(超详细)
vmware虚拟机安装2024(超详细)
303 6