JVM是什么?

目录:

目录

目录:

1,Jvm概述

2,Java代码的编译:

3,Java的代码执行(类加载)

4,JVM的内存管理

1,程序技术器

1,概念

2,结构

3,作用和常见问题

2,Java虚拟机栈

1,概念

2,结构

3,本地方法栈

4,堆

 1,对象如何在堆中分配?

2,对象在堆中的声明周期

3,对象分配过程

5,元空间(方法区)

1,方法区概念

2,方法区内部结构

3,栈、堆、方法区的交互关系图如下:

5,JVM的垃圾回收机制


1,Jvm概述

        Jvm是Java Virtual Machine(Java虚拟机):虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。简单来说JVM是用来解析和运行Java程序的。

        Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够**“一次编译,到处运行”**的原因

        从Java平台的逻辑结构上来看,我们可以从下图来了解JVM:

        从上图能清晰看到Java平台包含的各个逻辑模块,也能了解到JDK与JRE的区别

对于JVM自身的物理结构,我们可以从下图鸟瞰一下:

对于JVM的学习,在我看来这么几个部分最重要:

  • Java代码编译和执行
  • JVM的内存管理
  • JVM垃圾回收机制

2,Java代码的编译

JVM(一)一文读懂Java编译全过程_gonghaiyu的博客-CSDN博客

3,Java的代码执行(类加载)待补充

Java类加载器总结_swimming_in_it_的博客-CSDN博客

JVM之类加载阶段详解_jvm类加载过程_北洋~的博客-CSDN博客

JVM-类加载详解_jvm类加载过程_晴天的空间的博客-CSDN博客

4,JVM的内存管理

        Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。

        下图就是 Java 虚拟机定义的各种运行时数据区域:

         JVM的所有内存区域在下面会一一介绍,这里要明确一点:JDK1.8之后和JDK1.7最大的却别是:元数据(Metaspaces)取代了永久代(Perm Gen),元空间的本质和永久代类似,都是对JVM规范中的方法区的实现。其元空间和永久代之间的最大区别在于:元数据空间不在虚拟机中,而是在本地内存中(对于JVM而言,JVM内存之外的部分叫做本地内存 )

1,程序技术器

1,概念

        程序计数寄存器(Program Counter Register, PC),Register 的命名源于 CPU 的寄存器,CPU 只有把数据装载到寄存器才能够运行。在介绍 Java 的计数器前,我们先来了解一下 CPU 中的 PC。

        在 CPU 中 PC 是一个物理设备,在任何时候,PC 中存储的都是内存地址,而 CPU 就根据 PC 中的内存地址,到相应的内存取出指令然后执行,并且更新 PC 的值。在计算机通电后这个过程会一直不断的反复进行。

        而 Java 中 PC 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

2,结构

  1. 他是一块很小的内存空间,几乎可以忽略不计。但是也是速度最快的存储区域。
  2. 在jvm的规范中,每一个线程都有自己的程序技术器,是线程私有的并和线程的生命周期保持一致。
  3. 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序技术器会存储当前线程的正在执行的java的防范的jvm指令地址,如果是执行的是native方法在,则程序技术器未指定值(undefined)
  4. 程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都依赖这个计数器来完成
  5. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  6. 它是唯一一个在Java虚拟机规范中没有规定任何OutOtMemoryError情况的区域

3,作用和常见问题

程序计数器的作用是:在class文件加载到JVM后,每一行代码都会有一个对应指定地址,而程序计数器就是用来记录当前线程下一个执行命令的地址。

常见问题:

1,使用 PC 寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?

        1,因为CPU需要不停的切换各个线程,如果一个方法切换到另一个方法,这时候切换回来以后,就得知道接着从哪开始继续执行。

        2,JVM 的字节码解释器就需要通过改变 PC 寄存器的值来明确下一条应该执行什么样的字节码指令。

2,PC寄存器为什么会被设定为线程私有?

        多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个 PC 寄存器,每个线程都独立计算,不会互相影响。

2,Java虚拟机栈

1,概念

        Java 虚拟机栈(Java Virtual Machine Stacks)是描述 Java 方法执行的内存区域。每个线程在创建的时候,都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame)对应着一次方法的调用,是线程私有的,生命周期和线程一致

        作用:主管 Java 程序的运行,每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

        特点:

                1,栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器;
                2,JVM 直接对虚拟机栈的操作只有两个
                        每个方法执行,伴随着入栈(进栈/压栈)
                        方法执行结束出栈;
                3,虚拟机栈中内存会随着线程的销毁而清空,所以不存在垃圾回收问题。
        栈中可能出现的异常:Java 虚拟机规范允许 Java 虚拟机栈的大小是动态扩展和收缩的或者是固定不变的。

                1,如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常;
                2,如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
可以通过参数 -Xss 来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

2,结构

        1,栈的存储单位是栈帧(Stack Frame),一个栈帧代表着一个方法,且栈帧是一块内存区域,里面包含了各种执行过程中的数据信息。

        2,栈运行原理:

  1. JVM对Java栈的操作只有两个:入栈和出栈,遵循先进后出的原则。
  2. 在一条活动的线程上,一个时间点只有一个活动的栈帧。即当前正在执行的栈帧,被称为当前栈帧,当前栈帧对应的方法为当前方法,类也是当前类。
  3. 执行引擎运行的所以字节码指令都是对某一个线程内的栈帧进行操作
  4. 如果在该方法中调用了其他方法,对应的新栈帧会创建出来,放在栈的顶端,称为当前栈帧。
  5. 不同线程内的栈帧是互相隔离的。
  6. 如果当前方法调用了其他方法,其他方法返回之前,当前栈帧会将此方法的返回结果传给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新变成当前栈帧。
  7. Java方法有两种返回函数方式:不管哪种方式都会导致当前栈栈出栈。
    1,正常返回使用return语句。
    2,抛出异常

        3,栈帧的内部结构:栈帧中除了以下四种结构信息,还允许携带与 Java 虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息,但这些信息取决于具体的虚拟机实现。

  • 局部变量表(局部变量数据或者本地变量表):存放方法参数和局部变量的区域。
  1. 其是一组变量存储空间,主要用于存储方法参数和定义在防范内的局部变量,包含基本数据类型(boolean,char,short,intfloat,byte,double,long),引用类型和retrunAddress【类型由Java虚拟机的jsr,ret和jsr_w指令(§jsr,§ret,§jsr_w)使用。returnAddress类型的值是Java虚拟机指令的操作码的指针。与数字原语不同类型,则returnAddress类型与任何Java编程语言类型都不对应,并且不能由正在运行的程序进行修改】
  2. 局部变量存在于当前线程中的私有数据,所以不存在数据安全问题。
  3. 局部变量的容量大小是在编译期确定下来的,并保存在方法的Code属性的Maximum local variables中,其在运行期间的大小不会改变
  4. 栈的大小由方法的嵌套调用和局部变量决定,嵌套和局部变量大小都会导致栈变大。
  5. 局部变量只在当前方法(当前栈帧)中有效,方法结束后,会随着栈帧都销毁而销毁。
  6. 参数值存放在局部变量数组的index0的位置开始,到数组长度-1的索引结束。
  7. 槽(Slot):局部变量的最基本存储单位,在java虚拟机规范各种没有明确slot占用的内存大小。

                槽(Slot概念)如下;                

  1. byte(8),short(16),boolean(8),int(32),char(16)和引用类型(32位)占用一个slot,且都被转成int类型来保存,其数据宽度都是32位(4字节)但是在堆中变量是以数组存放的,所以导致堆内存中基本数据变量的实际宽度就是其本身的有效宽度(boolean:8位),其中long和double占用两个Slot。
  2. Jvm会为每一个Slot分配一个索引,通过索引来找到对应的值。如果访问lang或者double类型的数据只需要前一个Slot的索引即可。
  3. 当前方法被调用的时候,其方法参数和局部变量的值都会按照顺序被复制在对应的每一个Slot上。
  4. 如果当前帧是由构造方法和实例方法创建的,那么该对象的this指针将会存在idenx0的位置上,后续参数按照参数表顺序排列,在静态方法中是不存在this变量的
  5. Slot是可以复用的当局部变量过了作用域后,申请新的局部变量可能会服用之前局部变量的Slot
  6. 局部变量表中的变量也是垃圾回收的根结点,如果被局部变量直接或者间接引用的对象是不会被回收的。
  • 操作数栈:也称为表达式栈,主要用于保存计算过程中的中间结果,同时作为激素过程中的变量临时存储空间。
  1. 是Jvm执行引擎的一个工作区,在栈帧被创建出来的时候,就创建了一个空的操作数栈,其栈深度在编译期的时候就确定好了,并存储在方法的Code属性的max_stack中
  2. 其存储的数据类型可以是任意一个:32bit的类型占用一个栈单位的深度64占用两个,这里的栈深度和局部变量中Slot的存储单位一致。
  3. 操作数栈的不是采用访问索引的方式去访问数据,只能通过标准的进栈出栈来进行数据访问
  4. 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
  5. 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
  6. 另外,我们说 Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
  • 动态链接:指向运行时常量池的方法引用。

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

                在 Class 文件里面,一个方法若要调用其他方法或者访问成员变量,需要通过符号引用(Symbolic Reference:保存在 Class 文件的常量池中)来表示。动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

 1,JVM是如何执行方法调用的?

        方法调用不同于方法执行,方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。Class 文件的编译过程中不包括传统编译器中的连接步骤,一切方法调用在 Class 文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。也就是需要在类加载阶段,甚至到运行期才能确定目标方法的直接引用。

2,在 JVM 中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关:

        静态绑定:Java 虚拟机中的静态绑定指的是在解析时便能够直接识别目标方法的情况。即当一个字节码文件被装载进 JVM 内部时,被调用的目标方法在编译期可知,且运行期保持不变。
        动态绑定:如果被调用的方法在编译期无法被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态绑定。
3,虚方法和非虚方法:

        如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法,比如静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
其他方法称为虚方法。
4,虚方法表

        在面向对象编程中,会频繁的使用到动态分派,如果每次动态分派都要重新在类的方法元数据中搜索合适的目标有可能会影响到执行效率。为了提高性能,JVM 采用在类的方法区建立一个虚方法表(virtual method table),使用索引表来代替查找。非虚方法不会出现在表中。这个数据结构,便是 Java 虚拟机实现动态绑定的关键所在。

        我们之前分析类加载的准备阶段,**它除了为静态字段分配内存之外,还会构造与该类相关联的方法表。**虚方法表会在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕。

        方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。每个类中都有一个虚方法表,表中存放着各个方法的实际入口。

        在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。

  • 方法出口:方法正常退出或异常退出的地址。

        用来存放调用该方法的 PC 寄存器的值。一个方法的结束,有两种方式:正常执行完成和出现未处理的异常,非正常退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。

        方法正常退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
        而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。
        当一个方法开始执行后,只有两种方式可以退出这个方法:

  1. 第一种是执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,简称正常调用完成;

            一个方法的正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定;
            在字节码指令中,返回指令包含 ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn(long)、freturn(float)、dreturn(double)以及areturn(引用类型),另外还有一个 return 指令供声明为 void 的方法、实例初始化方法、类和接口的初始化方法使用。

  2. 另一种方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常调用完成。

             方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

        本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。正常调用完成和异常调用完成的区别在于:通过异常完成出口退出的不会给它的上层调用者产生任何的返回值。

3,本地方法栈

        和Java虚拟机栈非常像,是Java虚拟机栈用来管理Java方法的调用,本地方法栈用来管理本地方法的调用。
        1,本地方法栈也是线程私有
        2,也是允许固定或者动态扩展内存大小。同样会遇到StackOverflowError和OutofMemoryError的异常。

4,堆

        内存最大,线程共享,其目的就是存放对象,几乎所有的对象实例都在此分配.随着技术更新部分对象会被放到栈上。因为堆占用内存最大,所以是GC的主要区域。为了高兴的垃圾回收,堆分为三块区域。

  1. 新生代:新对象和没达到一定年龄的对象都在新生代
  2. 老年代:长时间使用的对象,老年代的内存空间应该要比年轻代更大
  3. 元空间(JDK1.8之前叫永久代):存储一些方法中的临时操作对象,JDK1.8之前是占用JVM内存,之后占用的是物理内存。

        Java堆可以是不连续的内存空间,逻辑上连续即可,类似磁盘空间。如果在分配实例的时候内存不足会抛出OutOfMemoryError。主流虚拟机都是可扩展的通过-Xmx和-Xms控制

 1,对象如何在堆中分配?

1,普通对象是在堆中创建对象,在栈中保存对象的引用。
2,基本类型中,如果是方法的局部变量则在栈内分配,其他情况堆内分配。
3,int[]:数组虽然不是基本类型但是在堆上分配的。

2,对象在堆中的声明周期

1,在 JVM 内存模型的堆中,堆被划分为新生代和老年代新生代又被进一步划分为 Eden 区和 Survivor 区,Survivor 区由 From Survivor 和 To Survivor 组成
 2,当创建一个对象时,对象会被优先分配到新生代的 Eden 区,时 JVM 会给对象定义一个对象年轻计数器(-XX:MaxTenuringThreshold)
3,Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)
        1,JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
        2,对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会 +1
4,如果分配的对象超过了-XX:PetenureSizeThreshold(年前技术器的最大值),对象会直接被分配到老年代

3,对象分配过程

1,new 的对象先放 Eden(伊甸园)区。此区有大小限制。
2,当 Eden 的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对Eden区进行垃圾回收(Minor GC),将Eden区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到 Eden 区。
3,然后将 Eden 中的剩余对象移动到幸存者 0 区。
4,如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区的,如果没有回收,就会放到幸存者 1 区。
5,如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者1区。
6,啥时候能去养老区呢?可以设置次数。默认是 15 次。可以设置参数: -XX:MaxTenuringThreshold=<N> 进行设置。
7,在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理。
8,若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常。

5,元空间(方法区)

        这里要说一下,永久代和元空间都是方法区的具体实现。所以说在讲元空间就是在描述方法区。下面来说下方法区。

1,方法区概念

  1. 方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。
  2. 它用于存储已被虚拟机加载的类型信息、常量、静态变量、JIT编译后的代码等数据。
  3. 虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。
  4. 方法区的大小和堆空间一样,可以选择固定大小也可选择可扩展,方法区的大小决定了系统可以放多少个类,如果系统类太多,导致方法区溢出,虚拟机同样会抛出 OOM 异常。

对于方法区,Java8 之后的变化

  1.         移除了永久代(PermGen),替换为元空间(Metaspace);
  2. 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
  3. 永久代中的字符串常量池(interned Strings)和类的静态变量(class static variables)转移到了 Java heap;
  4. 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)。

2,方法区内部结构

        方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

类型信息:对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在方法区中存储以下类型信息:

  1. 这个类型的完整有效名称(全名=包名.类名)
  2. 这个类型直接父类的完整有效名(对于 interface或是 java.lang.Object,都没有父类
  3. 这个类型的修饰符(public,abstract,final 的某个子集)
  4. 这个类型直接接口的一个有序列表

域(Field)信息:

  1. JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序;
  2. 域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient 的某个子集)

方法(Method)信息:

  1. 方法名称
  2. 方法的返回类型
  3. 方法参数的数量和类型
  4. 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
  5. 方法的字符码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
  6. 异常表(abstract 和 native 方法除外):每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

运行时常量池(Runtime Constant Pool)是方法区的一部分。理解运行时常量池的话,我们先来说说字节码文件(Class 文件)中的常量池(常量池表)。

  1. 运行时常量池(Runtime Constant Pool)是方法区的一部分。
  2. 常量池表(Constant Pool Table)是 Class 文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  3. 在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
  4. JVM为 每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
  5. 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
  6. 运行时常量池,相对于 class 文件常量池的另一重要特征是:具备动态性。
  7. 运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些。
  8. 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则 JVM 会抛 OutOfMemoryError 异常。

3,栈、堆、方法区的交互关系图如下:

额外说下常量池:

        一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),用于存放各种字面量和对类型、域和方法的符号引用。

        为什么需要常量池?

        避免频繁地创建和销毁对象而影响系统性能,实现对象的共享(字符串常量池);对于类共用的元数据信息,使用常量池可以共享使用,而不是不同线程、对象都创建一个副本,节省内存开销(class常量池、运行时常量池)。

        常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

5,JVM的垃圾回收机制

Jvm中GC种类和算法_main进程gc进程_swimming_in_it_的博客-CSDN博客

垃圾回收机制,如何优化程序_swimming_in_it_的博客-CSDN博客

jvm之内存调优_swimming_in_it_的博客-CSDN博客

参考文献:

        读懂 JVM 内存管理这篇就够了_jvm内存管理_徐俊生的博客-CSDN博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值