王磊的个人技术记录 王磊的个人技术记录

记录精彩的程序人生

目录
jvm1-数据结构
/  

jvm1-数据结构

Java程序运行过程

image.png

常见的jvm

image.png

jvm数据区域

image.png

image.png

image.png

栈帧:

在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。 栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)

局部变量表:

顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量) 。首先它是一个 32 位的长度,主要存放我们的 Java 的八大基础数据类 型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的 Object 对象,我们只需

要存放它的一个引用地址即可。

操作数据栈:

存放 java 方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类型,所 以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。 操作数栈本质上是 JVM 执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。

动态连接:

Java 语言特性多态

返回地址:

正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)

程序计数器

较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。 程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。 由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其
它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。 因为 JVM 是虚拟机,内部有完整的指令与执行的一套流程,所以在运行 Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号) ,如果 是遇到本地方法(native 方法),这个方法不是 JVM 来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个 会记录本地代码的执行的地址,所以在执行 native 方法时,JVM 中程序计数器的值为空(Undefined)。 另外程序计数器也是 JVM 中唯一不会 OOM(OutOfMemory)的内存区域。

本地方法栈

本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是 用 Java 实现的,而是由 C 语言实现的(比如 Object.hashcode 方法)。 本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。 虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot 直接把本地方法栈和虚拟机栈合二为一 。
方法区
方法区(Method Area)是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool) 字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法 方法区是 JVM 对内存的“逻辑划分”,在 JDK1.7 及之前很多开发者都习惯将方法区称为“永久代”,是因为在 HotSpot 虚拟机中,设计人员使用了永久 代来实现了 JVM 规范的方法区。在 JDK1.8 及以后使用了元空间来实现方法区。

元空间

方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM, 那么此时就只允许一个线程去加载它,另一个线程必须等待。 在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本 已经将方法区中实现的永久代去掉了,并用元空间(classmetadata)代替了之前的永久代,并且元空间的存储位置是本地内存。 元空间大小参数:
jdk1.7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize; jdk1.8 以后(初始和最大值) :-XX:MetaspaceSize; -XX:MaxMetaspaceSize jdk1.8 以后大小就只受本机总内存的限制(如果不设置参数的话)
JVM 参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html Java8 为什么使用元空间替代永久代,这样做有什么好处呢? 官方给出的解释是: 移除永久代是为了融合 HotSpotJVM 与 JRockitVM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。 永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError:PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有为 PermGen 分配多大的空间很难 确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

运行时常量池

运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编 译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。 运行时常量池是方法区的一部分。运行时常量池相对于 Class 常量池的另外一个重要特征是具备动态性。

堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。 堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(GarbageCollection)。 那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。 Java 的对象可以分为基本数据类型和普通对象。
对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。 当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

直接内存(堆外内存)

直接内存有一种更加科学的叫法,堆外内存。 JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的 内存也就是堆外内存。 它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;如果使用了 NIO,这块区域会被频繁使用,在 java 堆内可以用 directByteBuffer 对象直接引用并操作; 这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异
常。

小结: 1、直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小。

  1. 其他堆外内存,主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。 堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡。

堆空间分代划分

堆被划分为新生代和老年代(Tenured) ,新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 FromSurvivor 和 ToSurvivor 组成。

image.png


标题:jvm1-数据结构
作者:wanglei03
地址:https://wangleijava.com/articles/2020/09/07/1599486085627.html