【JVM底层原理,JVM架构详解】

1. JVM简介

1.1 什么是JVM?

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

主流虚拟机:

虚拟机名称介绍
HotSpotOracle/Sun JDK和OpenJDK都使用HotSPot VM的相同核心
J9J9是IBM开发的高度模块化的JVM
JRockitJRockit 与 HotSpot 同属于 Oracle,目前为止 Oracle 一直在推进 HotSpot与 JRockit 两款各有优势的虚拟机进行融合互补
Zing由Azul Systems根据HostPot为基础改进的高性能低延迟的JVM
DalvikAndroid上的Dalvik 虽然名字不叫JVM,但骨子里就是不折不扣的JVM

1.2 JVM与操作系统

为什么要在程序和操作系统中间添加一个JVM?
Java 是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要 JVM 进行一番转换。
在这里插入图片描述
从图中可以看到,有了 JVM 这个抽象层之后,Java 就可以实现跨平台了。JVM 只需要保证能够正确执行 .class 文件,就可以运行在诸如 Linux、Windows、MacOS 等平台上了。

Java 跨平台的意义在于一次编译,处处运行,能够做到这一点 JVM 功不可没。比如我们在 Maven 仓库下载同一版本的 jar 包就可以到处运行,不需要在每个平台上再编译一次。

现在的一些 JVM 的扩展语言,比如 Clojure、JRuby、Groovy 等,编译到最后都是 .class 文件,Java 语言的维护者,只需要控制好 JVM 这个解析器,就可以将这些扩展语言无缝的运行在 JVM 之上了。

应用程序、JVM、操作系统之间的关系:
在这里插入图片描述
我们用一句话概括 JVM 与操作系统之间的关系:JVM 上承开发语言,下接操作系统,它的中间接口就是字节码

1.3 JVM、JRE、JDK 的关系

在这里插入图片描述
JVM 是 Java 程序能够运行的核心。但是需要注意,JVM 自己什么也干不了,你需要给它提供生产原料(.class 文件) 。

仅仅是 JVM,是无法完成一次编译,处处运行的。它需要一个基本的类库,比如怎么操作文件、怎么连接网络等。而 Java 体系很慷慨,会一次性将 JVM 运行所需的类库都传递给它。JVM 标准加上实现的一大堆基础类库,就组成了 Java 的运行时环境,也就是我们常说的 JRE(Java Runtime Environment)

对于 JDK 来说,就更庞大了一些。除了 JRE,JDK 还提供了一些非常好用的小工具,比如 javac、java、jar 等。它是 Java 开发的核心,让外行也可以炼剑!

我们也可以看下 JDK 的全拼,Java Development Kit。我非常怕 kit(装备)这个单词,它就像一个无底洞,预示着你永无休止的对它进行研究。JVM、JRE、JDK 它们三者之间的关系,可以用一个包含关系表示。
在这里插入图片描述

1.4 Java虚拟机规范和Java语言规范的关系

在这里插入图片描述
左半部分是 Java 虚拟机规范,其实就是为输入和执行字节码提供一个运行环境。右半部分是我们常说的 Java 语法规范,比如 switch、for、泛型、lambda 等相关的程序,最终都会编译成字节码。而连接左右两部分的桥梁依然是Java 的字节码。

如果 .class 文件的规格是不变的,这两部分是可以独立进行优化的。但 Java 也会偶尔扩充一下 .class 文件的格式,增加一些字节码指令,以便支持更多的特性。

我们可以把 Java 虚拟机可以看作是一台抽象的计算机,它有自己的指令集以及各种运行时内存区域,学过《计算机组成结构》的同学会在课程的后面看到非常多的相似性。

最后,我们简单看一下一个 Java 程序的执行过程,它到底是如何运行起来的。
Java程序执行过程
这里的 Java 程序是文本格式的。比如下面这段 HelloWorld.java,它遵循的就是 Java 语言规范。其中,我们调用了System.out 等模块,也就是 JRE 里提供的类库。

public class HelloWorld {public static void main(String[] args) {System.out.println("Hello World");}
}

使用 JDK 的工具 javac 进行编译后,会产生 HelloWorld 的字节码。
我们一直在说 Java 字节码是沟通 JVM 与 Java 程序的桥梁,下面使用 javap 来稍微看一下字节码到底长什么样子。

0 getstatic #2 <java/lang/System.out> // getstatic 获取静态字段的值3 ldc #3 <Hello World> // ldc 常量池中的常量值入栈5 invokevirtual #4 <java/io/PrintStream.println> // invokevirtual 运行时方法绑定调用方法8 return //void 函数返回

Java 虚拟机采用基于栈的架构,其指令由操作码和操作数组成。这些 字节码指令 ,就叫作 opcode。其中,getstatic、ldc、invokevirtual、return 等,就是 opcode,可以看到是比较容易理解的。

JVM 就是靠解析这些 opcode 和操作数来完成程序的执行的。当我们使用 Java 命令运行 .class 文件的时候,实际上就相当于启动了一个 JVM 进程。

然后 JVM 会翻译这些字节码,它有两种执行方式。常见的就是 解释执行,将 opcode + 操作数翻译成机器代码;另外一种执行方式就是 JIT,也就是我们常说的 即时编译,它会在一定条件下将字节码编译成机器码之后再执行。

2. java虚拟机的内存管理

2.1 JVM整体架构

JVM分为五大模块: 类装载器子系统运行时数据区执行引擎本地方法接口垃圾收集模块
在这里插入图片描述


根据 JVM 规范,JVM 内存共分为虚拟机方法区程序计数器本地方法栈五个部分。
JVM整体架构

名称特征作用配置参数异常
程序计数器占用内存小,线程私有,生命周期与线程相同大致为字节码行号指示器
虚拟机栈线程私有,生命周期与线程相同,使用连续的内存空间Java 方法执行的内存模型,存储局部变量表、操作栈、动态链接、方法出口等信息-XssStackOverflowError / OutOfMemoryError
线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址保存对象实例,所有对象实例(包括数组)都要在堆上分配-Xms -Xsx -XmnOutOfMemoryError
方法区线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据-XX:PermSize:16M-
XX:MaxPermSize64M/-
XX:MetaspaceSize=16M-
XX:MaxMetaspaceSize=64M
OutOfMemoryError
本地方法栈线程私有为虚拟机使用到的Native 方法服务StackOverflowError / OutOfMemoryError

2.2 JVM运行时内存

Java 虚拟机有自动内存管理机制,如果出现面的问题,排查错误就必须要了解虚拟机是怎样使用内存的。
在这里插入图片描述

Java7和Java8内存结构的不同主要体现在方法区的实现。

方法区是java虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。

我们通常使用的Java SE都是由Sun JDK和OpenJDK所提供,这也是应用最广泛的版本。而该版本使用的VM就是HotSpot VM。通常情况下,我们所讲的java虚拟机指的就是HotSpot的版本。

  1. JDK7 内存结构
    在这里插入图片描述
  1. JDK8 的内存结构
    在这里插入图片描述
  1. 针对JDK8虚拟机内存详解:
    在这里插入图片描述
  1. JDK7和JDK8变化小结:
    在这里插入图片描述

线程私有的:

  • ①程序计数器
  • ②虚拟机栈
  • ③本地方法栈


线程共享的:

  • ①堆
  • ②方法区
    直接内存(非运行时数据区的一部分)

对于Java8,HotSpots取消了永久代,那么是不是就没有方法区了呢?
当然不是,方法区只是一个规范,只不过它的实现变了。
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。

方法区Java8之后的变化:

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

Java8为什么要将永久代替换成Metaspace?

  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • Oracle 可能会将HotSpot 与 JRockit 合二为一,JRockit没有所谓的永久代。

2.2.1 PC 程序计数器

  1. 什么是程序计数器?
    程序计数器(Program Counter Register):也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

  2. PC寄存器的特点

    • 区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存,虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。
    • 当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
    • 程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
    • 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

    在这里插入图片描述
    Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。

    因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

2.2.2 虚拟机栈

  1. 什么是虚拟机栈?
    Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,即生命周期和线程相同。Java虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

    public class StackDemo {public static void main(String[] args) {StackDemo sd = new StackDemo();sd.A();}public void A(){int a = 10;System.out.println(" method A start");System.out.println(a);B();System.out.println("method A end");}public void B(){int b = 20;System.out.println(" method B start");C();System.out.println("method B end");}private void C() {int c = 30;System.out.println(" method C start");System.out.println("method C end");}
    }
    

    在这里插入图片描述

  2. 什么是栈帧?
    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表操作数栈动态连接方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
    栈帧
    在这里插入图片描述

  3. 设置虚拟机栈的大小
    -Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M。

    • Linux/x64 (64-bit): 1024 KB
    • macOS (64-bit): 1024 KB
    • Oracle Solaris/x64 (64-bit): 1024 KB
    • Windows: The default value depends on virtual memory
    -Xss1m
    -Xss1024k
    -Xss1048576
    

    在这里插入图片描述

  4. 局部变量表
    局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。

    其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

  5. 操作数栈
    操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

    通过以下代码演示操作站执行:

    public class StackDemo2 {public static void main(String[] args) {int i = 1;int j = 2;int z = i + j;}
    }
    
  6. 动态链接
    Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。

    动态链接的作用:将符号引用转换成直接引用

  7. 方法返回地址
    方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

    无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

2.2.3 本地方法栈

本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是 虚拟机栈为虚拟机执行Java方法(也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务

本地方法栈特点:

  1. 本地方法栈加载native的方法,native类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。
  2. 虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
  3. 是线程私有的,它的生命周期与线程相同,每个线程都有一个。


本地方法栈的两种异常类型:

  1. StackOverFlowError:线程请求的栈深度>所允许的深度。
  2. OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存。

2.2.4 堆

2.2.4.1 Java 堆概念
  1. 简介
    对于Java应用程序来说, Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。Java堆是被所 有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。
    “几乎”是指从实现角度来看, 随着Java语 言的发展, 现在已经能看到些许迹象表明日后可能出现值类型的支持, 即使只考虑现在, 由于即时编译技术的进步, 尤其是逃逸分析技术的日渐强大, 栈上分配、 标量替换优化手段已经导致一些微妙的变化悄然发生, 所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
    在这里插入图片描述

  2. 堆的特点

    • 1)是Java虚拟机所管理的内存中最大的一块。
    • 2)堆是jvm所有线程共享的。
      堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)
    • 3)在虚拟机启动的时候创建。
    • 4)唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
    • 5)Java堆是垃圾收集器管理的主要区域。
    • 6)因此很多时候 java堆也被称为“GC堆”(Garbage Collected Heap)。
      从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
    • 7)java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)
    • 8)方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
    • 9)如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  3. 设置堆空间大小
    内存大小参数:-Xmx/-Xms
    使用示例: -Xmx20m -Xms5m
    说明: 当下Java应用最大可用内存为20M, 最小内存为5M

    测试:

    public class TestVm {public static void main(String[] args) {//补充//byte[] b=new byte[5*1024*1024];//System.out.println("分配了1M空间给数组");System.out.print("Xmx=");System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");System.out.print("free mem=");System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");System.out.print("total mem=");System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");}
    }
    

    执行结果:

    Xmx=20.0M
    free mem=4.1877593994140625M
    total mem=6.0M
    

    大家可以发现,这里打印出来的Xmx值和设置的值之间是由差异的,total Memory和最大的内存之间还是存在一定差异的,就是说JVM一般会尽量保持内存在一个尽可能底的层面,而非贪婪做法按照最大的内存来进行分配。

    在测试代码中新增如下语句,申请内存分配:

    byte[] b=new byte[4*1024*1024];
    System.out.println("分配了1M空间给数组");
    

    在申请分配了4m内存空间之后,total memory上升了,同时可用的内存也上升了,可以发现其实JVM在分配内存过程中是动态的, 按需来分配的。

  4. 堆的分类
    现在垃圾回收器都使用分代理论,堆空间也分类如下:

    • 在Java7 Hotspot虚拟机中将Java堆内存分为3个部分

      • 青年代Young Generation
      • 老年代Old Generation
      • 永久代Permanent Generation

      在这里插入图片描述

    • Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不存在了,在(2018年9约25日)Java11正式发布以后,从官网上找到了关于Java11中垃圾收集器的官方文档,文档中没有提到“永久代”,而只有青年代和老年代。

      • 青年代Young Generation
      • 老年代Old Generatio

      在这里插入图片描述

2.2.4.2 年轻代和老年代
  1. JVM中存储java对象可以被分为两类

    • 年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)。
    • 年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。

    在这里插入图片描述

  2. 配置新生代和老年代堆结构占比
    默认 -XX:NewRatio=2,标识新生代占1,老年代占2,新生代占整个堆的1/3。
    修改占比 -XX:NewPatio=4,标识新生代占1,老年代占4,新生代占整个堆的1/5。

    Eden空间和另外两个Survivor空间占比分别为8:1:1。
    可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8。
    几乎所有的java对象都在Eden区创建,但80%的对象生命周期都很短,创建出来就会被销毁。
    在这里插入图片描述
    从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。

    默认的,新生代 ( Young ) 老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。

    其中,新生代 ( Young ) 被细分为 Eden两个Survivor 区域,这两个 Survivor 区域分别被命名为 fromto,以示区分。 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

    JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间

2.2.4.3 对象分配过程

JVM设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,因此还需要考虑GC执行完内存回收后是否存在空间中间产生内存碎片。

分配过程

  1. new的对象先放在伊甸园区。该区域有大小限制。
  2. 当伊甸园区域填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊甸园区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到伊甸园区。
  3. 然后将伊甸园区中的剩余对象移动到幸存者0区,此时伊甸园区是空的。
  4. 如果再次触发垃圾回收(即伊甸园区再次满了,重复第2步操作, 然后将伊甸园区中的剩余对象移动到幸存者1区),此时上次幸存下来的放在幸存者0区的,如果没有回收,也会放到幸存者1区。
  5. 如果再次经历垃圾回收(即伊甸园区再次满了,重复第2步操作, 然后将伊甸园区中的剩余对象移动到幸存者0区),此时会重新返回幸存者0区,接着再去幸存者1区。(JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务即同一时间,0区和1区一定有一个是空的
  6. 如果累计次数到达默认的15次,这会进入养老区。
    可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N。
  7. 养老区内存不足时,会再次出发GC:Major GC 进行养老区的内存清理。
  8. 如果养老区执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常。

在这里插入图片描述
在这里插入图片描述

分配对象的流程

  1. 新对象申请时判断伊甸园区(Eden)有没有对应的空间,如果有空间,则分配空间,如果空间不足,则会清理伊甸园区,执行垃圾回收YGC(也叫Minor GC)。
  2. 在伊甸园区空间不足执行YGC算法后,再来判断下伊甸园区是否放得下,如果放得下,则在伊甸园区给新对象分配空间,如果还是放不下,则会将这个新对象放到老年区。
  3. 新创建对象在伊甸园区YGC后仍然放不下,则会放到老年区。此时,先判断老年区放不放得下,如果老年区能放下,则在老年区给新对象分配空间;如果老年区也放不下,则老年区会执行FGC(Full GC,年轻代和老年代都会进行清理)。
  4. 老年代执行FGC后再次判断老年代是否放得下,如果放得下,则在老年代分配空间,如果依旧放不下,会抛出OOM异常(java内存溢出错误)。
    在这里插入图片描述
2.2.4.4 堆GC

Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:一种是部分收集器(Partial GC)另一类是整堆收集器(Full GC)

  • 部分收集器: 不是完整收集java堆的的收集器,它又分为:
    • 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集。
    • 老年代收集 (Major GC / Old GC):只是老年代的垃圾收集 (CMS GC 单独回收老年代)。
    • 混合收集(Mixed GC):收集整个新生代及老年代的垃圾收集 (G1 GC会混合回收,region区域回收)。
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集器。

年轻代GC触发条件:

  • 年轻代空间不足,就会触发Minor GC, 这里年轻代指的是Eden代满,Survivor不满不会引发GC。
  • Minor GC会引发STW(stop the world) ,暂停其他用户的线程,等垃圾回收接收,用户的线程才恢复。

老年代GC (Major GC)触发机制:

  • 老年代空间不足时,会尝试触发MinorGC。如果空间还是不足,则触发Major GC。
  • 如果Major GC ,内存仍然不足,则报错OOM
  • Major GC的速度比Minor GC慢10倍以上。

FullGC 触发机制:

  • 调用System.gc() ,系统会执行Full GC ,不是立即执行。
  • 老年代空间不足。
  • 方法区空间不足。
  • 通过Minor GC进入老年代平均大小大于老年代可用内存。

2.2.5 元空间

在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。

它和永久代有什么不同的?
存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在 类的元信息存储在元空间中静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
在这里插入图片描述

2.2.5.1 为什么要废弃永久代,引入元空间?

相比于之前的永久代划分,Oracle为什么要做这样的改进呢?

  • 在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出。
  • 移除永久代是为融合HotSpot VM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
  • 永久代会为GC带来不必要的复杂度,并且回收效率偏低。
2.2.5.2 废除永久代的好处
  • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在时的内存溢出错误。
  • 将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。
  • 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。
2.2.5.3 Metaspace相关参数
  • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。如果没有使用该参数来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。
    但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。
    如果设置了该参数,当Metaspace剩余空间不足,会抛出:java.lang.OutOfMemoryError: Metaspace space。
  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

2.2.6 方法区

2.2.6.1 方法区的理解

方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载 的类型信息、常量、 静态变量、 即时编译器编译后的代码缓存等数据。

元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的。
在这里插入图片描述
创建对象各数据区域的声明:
在这里插入图片描述

方法区的特点:

  • 方法区与堆一样是各个线程共享的内存区域。
  • 方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续。
  • 方法区的大小跟堆空间一样 可以选择固定大小或者动态变化。
  • 方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类 导致方法区溢出虚拟机同样会跑出(OOM)异常(Java7之前是 PermGen Space (永久带) Java 8之后 是MetaSpace(元空间) )。
  • 关闭JVM就会释放这个区域的内存。
2.2.6.2 方法区结构

方法区的内部结构:
在这里插入图片描述
类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。

方法区中存储的内容:

  • 类信息(域信息、方法信息)
    • 类型信息
      对每个加载的类型(类Class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:
      ① 这个类型的完整有效名称(全名 = 包名.类名)
      ② 这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类)
      ③ 这个类型的修饰符( public, abstract,final的某个子集)
      ④ 这个类型直接接口的一个有序列表
    • 域信息
      域信息,即为类的属性,成员变量
      JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。
      域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集)
    • 方法信息
      VM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
      ① 方法名称方法的返回类型(或void)。
      ② 方法参数的数量和类型(按顺序)。
      ③ 方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集
      ④方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外)
      ⑤异常表( abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
  • 运行时常量池

在这里插入图片描述

2.2.6.3 方法区设置

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。

  1. jdk7及以前

    • 通过-xx:Permsize来设置永久代初始分配空间。默认值是20.75M。
    • -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M。
    • 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。


    查看JDK PermSpace区域默认大小:

    jps #是java提供的一个显示当前所有java进程pid的命令
    jinfo -flag PermSize 进程号 #查看进程的PermSize初始化空间大小
    jinfo -flag MaxPermSize 进程号 #查看PermSize最大空间
    
  2. JDK8以后
    元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定。
    默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。

    与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace。

    -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-xx:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,FullGC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。

    如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

    jps #是java提供的一个显示当前所有java进程pid的命令
    jinfo -flag PermSize 进程号 #查看进程的PermSize初始化空间大小
    jinfo -flag MaxPermSize 进程号 #查看PermSize最大空间
    

2.2.7 运行时常量池

字节码文件中,内部包含了常量池。
方法区中,内部包含了运行时常量池。
常量池:存放编译期间生成的各种字面量与符号引用。
运行时常量池:常量池表在运行时的表现形式。

编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。

理解为字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时常量池中取数据,被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。

它们不是一个概念,存放的位置是不同的。一个在字节码文件中,一个在方法区中。
在这里插入图片描述
对字节码文件反编译之后,查看常量池相关信息:
常量池
要弄清楚方法区的运行时常量池,需要理解清楚字节码中的常量池。

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

常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
常量池表Constant pool table:
在这里插入图片描述
在方活中对常量池表的符号引用:
在这里插入图片描述
为什么需要常量池?
举例来说:

public class Solution {public void method() {System.out.println("are you ok");}
}

这段代码很简单,但是里面却使用了 String、 System、 PrintStream及Object等结构。如果代码多,引用到的结构会更多!这里就需要常暈池,将这些引用转变为符号引用,具体用到时,采取加载。

2.2.8 直接内存

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分。

在JDK 1.4中新加入了NIO(New Input/Output) 类, 引入了一种基于通道(Channel) 与缓冲区 (Buer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的 DirectByteBuer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了 在Java堆和Native堆中来回复制数据。
在这里插入图片描述
NIO的Buer提供一个可以直接访问系统物理内存的类——DirectBuer。DirectBuer类继承自ByteBuer,但和普通的ByteBuer不同。普通的ByteBuer仍在JVM堆上分配内存,其最大内存受到最大堆内存的 限制。而DirectBuer直接分配在物理内存中,并不占用堆空间。在访问普通的ByteBuer时,系统总是会使用一个“内核缓冲区”进行操作。而DirectBuer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuer更快。
在这里插入图片描述

通过使用堆外内存,可以带来以下好处:

  1. 改善堆过大时垃圾回收效率,减少停顿。F

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/843605.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

人大金仓python驱动安装指引

最好使用虚拟环境&#xff0c;避免各种路径异常问题 在指定目录下创建虚拟环境 python3 -m venv myenv myenv可以是一个路径&#xff0c;当前命令会创建一个叫myenv的目录&#xff0c;目录里放的是虚拟环境所需的可执行文件 激活虚拟环境 source myenv/bin/activate 退出虚…

2024-05-28 服务器开发-不同vs版本的std::string的访问出错问题-记录

摘要: 有一个dll库是使用vs2010编译的, 使用这个dll动态库的工程是vs2019. 这个dll动态库返回一个结构体&#xff0c;其中有个成员使用了std::string。但是遇到了std::string的成员显示被赋值为NULL的情况。 本文对进行分析, 重点在于追踪问题的思路。 问题描述: dll使用vs20…

保护“第二生命线”,科技守护颈椎健康

脊柱支撑着人体重量&#xff0c;汇集着众多血管神经&#xff0c;素有“人体第二生命线”之称。在如今快节奏的时代&#xff0c;人们生活方式也在发生着变化&#xff0c;长期低头看手机、伏案久坐等不良生活习惯引发脊柱健康问题&#xff0c;且呈现年轻化趋势。目前&#xff0c;…

【加密与解密(第四版)】第十二章笔记

第十二章 注入技术 12.1 DLL注入方法 在通常情况下&#xff0c;程序加载 DLL的时机主要有以下3个&#xff1a;一是在进程创建阶段加载输入表中的DLL&#xff0c;即俗称的“静态输人”;二是通过调用 LoadLibrary(Ex)主动加载&#xff0c;称为“动态加载”&#xff1b;三是由于系…

核心三:正确的停止线程

3、核心三&#xff1a;正确的停止线程 3.1 如何正确的停止线程 3.1.1 原理介绍&#xff1a;使用interrupt来通知&#xff0c;而不是强制 线程中断的机制&#xff1a;一个线程来通知要中断线程&#xff08;你好&#xff0c;你现在应该停止了&#xff09;最后的决定是由要中断…

第十四届蓝桥杯c++研究生组

A 关键思路是求每个十进制数的数字以及怎么在一个数组中让判断所有的数字次数相等。 求每个十进制的数字 while(n!0){int x n%10;//x获取了n的每一个位数字n/10;}扩展&#xff1a;求二进制的每位数字 &#xff08;注意&#xff1a;进制转换、1的个数、位运算&#xff09; x…

主干网络篇 | YOLOv8更换主干网络之MobileNeXt | 新一代移动端模型MobileNeXt来了!

前言:Hello大家好,我是小哥谈。MobileNeXt是由微软研究院提出的一种高效的卷积神经网络结构,它在保持模型轻量级的同时,能够获得较高的性能。MobileNeXt采用了一种称为Inverted Residuals with Linear Bottlenecks(IRL)的结构,通过深度可分离卷积和快捷连接来减少模型的…

[Android]将私钥(.pk8)和公钥证书(.pem/.crt)合并成一个PKCS#12格式的密钥库文件

如下&#xff0c;我们有一个platform.pk8和platform.x509.pem。为了打包&#xff0c;需要将私钥&#xff08;.pk8&#xff09;和公钥证书&#xff08;可能是.pem或.crt文件&#xff09;合并成一个PKCS#12 格式的密钥库文件 1.准备你的私钥和证书文件 确保你有以下两个文件&…

设计模型-系统架构师(三)

1、按照《中华人民共和国著作权法》的权利保护期&#xff0c;署名权和修改权 是永久保护的。 发表权、使用权和获取报酬权 保护期限 作者终生和死后的50年。 2、&#xff08;重要&#xff09;面向对象的分析模型主要由 顶层架构图、用例与用例图和&#xff08;&#xff09;构成…

2024年物理化学与工程技术发展国际会议(ICPCETD 2024)

全称&#xff1a;2024年物理化学与工程技术发展国际会议&#xff08;ICPCETD 2024&#xff09; 会议网址:http://www.icpcetd.com 会议时间: 2024.06.30 截稿时间&#xff1a;2024.06.21 会议地点: 上海 投稿邮箱&#xff1a;icpcetd_info163.com 投稿标题&#xff1a;ArticleT…

前端开发框架Angular

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl Angular概述 Angular是由Google开发并维护的一款开源前端开发框架。它最初被设计为用于构建单页面应用&#xff08;SPA&#xff09;&#xff0c;但随着版本的更新和发展&am…

刚接触抖店并开通了个体店,怎么快速起店呢?运营思路参考如下

我是王路飞。 如果你刚接触抖店&#xff0c;并且开通了个体店&#xff0c;但不知道如何做店的话。 其实很简单&#xff0c;抖店的流程并没有这么复杂。 电商的核心无非就是产品&#xff0c;抖店的运营也都是围绕产品展开的。 我给你们说下抖店的运营思路你们可以作为参考&a…

【Real】[Flask]SSTI

文章目录 前言一、题目解读二、解题过程三、知识点Flask是什么SSTI是什么SSTI是如何形成的易于利用的类payload是什么 探索类型和类层次结构和方法 前言 温馨提示&#xff1a;看到哪里不懂直接跳到知识点部分&#xff0c;理解完再回到解题过程。 一、题目解读 题目是[Flask]S…

如何使用Docker快速运行Firefox并实现远程访问本地火狐浏览器

文章目录 1. 部署Firefox2. 本地访问Firefox3. Linux安装Cpolar4. 配置Firefox公网地址5. 远程访问Firefox6. 固定Firefox公网地址7. 固定地址访问Firefox Firefox是一款免费开源的网页浏览器&#xff0c;由Mozilla基金会开发和维护。它是第一个成功挑战微软Internet Explorer浏…

【Linux学习】进程间通信 (3) —— System V (1)

下面是有关进程通信中 System V 的相关介绍&#xff0c;希望对你有所帮助&#xff01; 小海编程心语录-CSDN博客 目录 1. System V IPC 1. 消息队列 msg 消息队列的使用方法 1.1 消息队列的创建 1.2 向消息队列发送消息 1.3 从消息队列接收消息 1.4 使用msgctl函数显式地…

科技查新是什么?一文了解!

本文主要解答 1、什么是科技查新&#xff1f; 2、科技查新有哪些作用&#xff1f; 3、科技查新一般应用于什么地方&#xff1f; 4、在哪能出具正规查新报告&#xff1f; 5、科技查新流程是怎样的&#xff1f; 带着这些问题阅读这篇文章相信一定会有收获&#xff01;干活内…

【启程Golang之旅】运算符与流程控制讲解

欢迎来到Golang的世界&#xff01;在当今快节奏的软件开发领域&#xff0c;选择一种高效、简洁的编程语言至关重要。而在这方面&#xff0c;Golang&#xff08;又称Go&#xff09;无疑是一个备受瞩目的选择。在本文中&#xff0c;带领您探索Golang的世界&#xff0c;一步步地了…

如何让UE4.26使用VS2022【Windows,源码下载】

使用UE5一直用的是VS2022&#xff0c;都是因为团队需要&#xff0c;只能用UE4&#xff0c;而我电脑中拥有的UE4的版本是UE4.26以及VS2022&#xff0c;我不可能去下载VS2019来为这么一个项目&#xff0c;所以就研究了一下是哪里阻止了UE4.26不让我使用VS2022. 首先下载UE4.26源码…

记一次安卓“Low on memory“崩溃问题

前言 最近再调人脸识别算法相关demo,发现调试期间总是偶发性崩溃&#xff0c;捕获不到异常的那种&#xff0c;看日志发现原因是Low on memory&#xff0c;一开始还疑惑 App内存不够应该是OOM啊,怎么会出现这种问题&#xff0c;百思不得其解&#xff0c;直到我打开了 Android s…

在MySQL数据库中进行连接查询中的外连接查询

左外连接 特征&#xff1a;以左表为主&#xff0c;显示左表所有数据&#xff0c;相关联存在相同的值时显示对应数据&#xff0c;否则显示为NULL 语法&#xff1a;> select 表名.字段名称&#xff0c;表名.字段名称 from 表名 left join 表名 on 相关联的字段…