暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Java虚拟机内存结构(JVM)

陈晨辰呀 2021-07-21
1827
  • JVM内存结构

  • 方法区

  • 虚拟机栈

  • 本地内存栈

  • 程序计数器

  • 内存优化

JDK体系结构

  • JDK:Java Develpment Kit,Java开发工具,包括Java语句,命令工具集和JRE

  • JRE:Java Runtime Environment,即Java运行时环境,包括JDK和Java核心类库

  • JVM:Java Virtual Machine,即Java虚拟机

简化版

完整版


JVM内存结构

JVM内存主要分为堆、虚拟机栈、本地方法栈、方法区、程序计数器等。

  • 类信息、常量、静态变量存放在方法区

  • 类创建的对象存放在

  • 堆中对象的调用方法时会使用到虚拟机栈,本地方法栈,程序计数器

  • 虚拟机栈对应的是方法的执行过程,本地方法栈是用来调用本地方法的执行过程

  • 线程下一条将要执行的指令存放在程序计数器

  • 方法执行时每行代码由解释器逐行执行

  • 和操作系统打交道需要调用本地方法接口

  • 热点代码由JIT编译器即时编译

  • 垃圾回收机制回收堆中资源

各个分区介绍

名称特征作用配置参数异常
程序计数器占用内存小,线程私有字节码行号指示器
虚拟机栈线程私有,使用连续的内存空间java方法存储的内存模型,存储局部变量表、操作数栈、动态链接、方法出口等-XssOOM,stackOverFlow
本地方法栈线程私有为虚拟机使用到的本地方法服务,方便与外界环境交互OOM,stackOverFlow
线程共享保存对象实例,所有对象实例都要在堆上分配-Xmn-Xms-XmxOOM java heap space
方法区线程共享存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据-XX:PermSize:16M-XX:MaxPermSize64M1.7 OOM PermGen space1.8 OOM Metaspace
运行时常量池方法区的一部分字面量及符号引用


Java8 和 Java7中JVM内存模型有什么区别

Java7及以前版本方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。

永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。

在Java7中永久代中存储的部分数据已经开始转移到Java Heap或Native Memory中了。比如,符号引用(Symbols)转移到了Native Memory;字符串常量池(interned strings)转移到了Java Heap;类的静态变量(class statics)转移到了Java Heap。

在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。


本地内存(Native memory)

本地内存(Native memory)也称为C-Heap,是供JVM自身进程使用的线程共享区域。本地内存也是通常说的堆外内存,包含元空间和直接内存

当堆空间不足时会触发GC,但本地内存空间不够却不会触发GC。元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中“java.lang.OutOfMemoryError: PermGen space”这种错误。

默认情况下元空间是可以无限使用本地内存的,但为了不让它如此膨胀,JVM同样提供了参数来限制它使用的使用。

  • -XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。

  • -XX:MaxMetaspaceSize,可以为class metadata分配的最大空间。默认是没有限制的。

  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。

  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。


方法区(Method Area)

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

方法区在1.8之前被称为永久代,1.8使用本地内存的元空间作为方法区的实现,存储的类信息、编译之后的代码数据都直接占用的本地内存(但StringTable还是放在堆中)。


永久代为什么被替换了

表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。

当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。同时也不用担心运行性能问题了,在覆盖到的测试中, 程序启动和运行速度降低不超过1%,但是这点性能损失换来了更大的安全保障。


堆(Java Heap)

线程共享,主要用于分配实例对象。

Java堆是垃圾收集器管理的主要地方,因此很多的时候也被称为GC堆,Java堆还可以分为年轻代和老年代,年轻代又可以分为Eden空间、From Survivor空间、To Survivor空间,默认是8:1:1的比例。

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。


Java堆的区域都是线程共享的吗?

堆是全局共享的,但是会存在一些问题,那就是多个线程在堆上同时申请空间,如果在并发的场景中,两个线程先后把对象引用指向了同一个内存区域,那可能就会出现问题。

解决方案是每个线程在堆中都预先分配一小块内存,然后再给对象分配内存的时候,先在这块“私有内存”进行分配,这块用完之后再去分配新的“私有内存”,这就是TLAB分配

它是从堆内存划分出来的,有了TLAB技术,堆内存并不是完完全全的线程共享,每个线程在初始化的时候都会去内存中申请一块TLAB。TLAB区域的内存其它线程也是可以读取的,只不过无法在这个区域分配内存而已


所有对象实例都存储在堆中吗?

Java对象实例和数组元素不一定都是在堆上分配内存,满足特定的条件的时候,它们可以在栈上分配内存。

JVM中的Java JIT编译器有两个优化,叫做逃逸分析和标量替换。

逃逸分析通过分析对象引用的作用域,来决定对象的分配地方(堆 or 栈)

一个子程序分配了一个对象并且返回了该对象的指针,那么这个对象在整个程序中被访问的地方无法确定,任何调用这个子程序的都可以拿到这个对象的位置,并且调用这个对象,这种情况就是对象逃逸;

若指针存储在全局变量或者其它数据结构中,全局变量也可以在子程序之外被访问到,这种情况也是对象逃逸;

而方法内的局部变量始终在方法内没有逃逸,在方法执行完之后自动销毁,则可将方法变量和对象分配到栈上,不需要垃圾回收的介入,提高系统的性能。

public StringBuilder getBuilder1(String a, String b) {
  StringBuilder builder = new StringBuilder(a);
  builder.append(b);
  // builder通过方法返回值逃逸到外部
  return builder;
}
public String getBuilder2(String a, String b) {
  StringBuilder builder = new StringBuilder(a);
  builder.append(b);
  // builder范围维持在方法内部,未逃逸,toString方法会new一个String用来返回
  return builder.toString();
}


标量替换是什么?

逃逸分析只是栈上内存分配的前提,接下来还需要进行标量替换才能真正实现降低堆内存的压力。

标量,就是指JVM中无法再细分的数据,比如int、long、reference等。相对地,能够再细分的数据叫做聚合量,比如对象

如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解为纯标量表示时,程序执行时可能不创建这个对象,而改为直接创建方法使用到的标量来代替。拆散后的标量可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了,相当于对象就在栈上分配了。

public static void main(String[] args) throws Exception {
  long start = System.currentTimeMillis();
  for (int i = 0; i < 10000; i++) {
      allocate();
  }
  System.out.println((System.currentTimeMillis() - start) + " ms");
  Thread.sleep(10000);
}
// 聚合量(对象),由两个标量a、b组成
public static class MyObject {
// 标量(原始数据类型)
  int a;
  double b;
  MyObject(int a, double b) {
      this.a = a;
      this.b = b;
  }
}
public static void allocate() {
  MyObject myObject = new MyObject(2019, 2019.0);
}
/**
* 通过逃逸分析,JVM会发现myObject没有逃逸出allocate()方法的作用域
* 标量替换过程就会将myObject直接拆解成a和b
**/
static void allocate() {
  int a = 2019;
  double b = 2019.0;
}


同步消除(锁消除)是什么?

锁消除是Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析

线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。(锁和锁块内的对象不会逃逸出线程就可以把这个同步块取消)。


对象的创建的主要流程

划分内存的方法

“指针碰撞”(Bump the Pointer)(默认用指针碰撞)

如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

“空闲列表”(Free List)

如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录


JVM并发情况下多个对象内存分配:

  • CAS(compare and swap)

  • TLAB(Thread Local Allocation Buffer)


初始化

内存分配完成后,虚拟机将分配到的内存空间都初始化为零值。int i默认0,boolean默认false。


设置对象头

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。


执行<init>方法

按照代码为属性赋值,执行构造方法。如int i = 100; boolean b = true


虚拟机栈(Java stack)

Java虚拟机栈属于线程私有的,生命周期和线程相同

虚拟机栈描述方法的执行过程,每个栈帧对应一个方法的入栈和出栈,包含局部变量、操作数栈、动态链接和方法出口。

本地方法栈则是用于执行本地方法的。

  • 栈:线程运行需要的内存空间

  • 栈帧:每一个方法运行需要的内存(包括参数,局部变量,返回地址等信息)

  • 每个线程只有一 个活动栈帧(栈顶的栈帧),对应着正在执行的代码

栈组成部分

局部变量表:存放了编译期可知的各种基本数据类型、对象引用(reference类型,它不等同于对象本身,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)。

操作数栈:一个后进先出(FILO)的操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。

可以看到Math.java代码反汇编后的信息是一些JVM指令。

代码中的JVM指令,对应栈中局部变量表和操作数栈的操作


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

方法出口:存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:正常执行完成、出现未处理的异常,非正常退出。

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。


常见栈问题解析

栈的异常

在Java虚拟机规范中,对栈这个区域规定了两种异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。

  • 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

栈内存溢出(StackOverflowError)主要有以下几种情况:

  • 栈帧过多,如递归调用没有正确设置结束条件

  • 栈帧过大,json数据转换,对象嵌套对象 (用户类有部门类属性,部门类有用户类属性)


本地内存栈

Java虚拟机栈于管理Java方法的调用,而本地方法栈(Native Method Stack)用于管理本地方法的调用。本地方法栈,也是线程私有的。

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

Native 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口。如notify,hashcode,wait等都是native方法。

虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。


程序计数器

程序计数器是线程私有,存放每个线程接下来要执行的指令。

程序计数器占一小块内存空间,就是当前线程的执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

比如如下字节码内容,在每个字节码`前面都有一个数字(行号),我们可以认为它就是程序计数器存储的内容。

Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)

程序计数器通过移位寄存器实现,此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,不会内存溢出,所以这块区域也不需要进行 GC。


查看代码执行过程

编译:把通过用高级语言编写的源程序通过编译器转变为目标程序。

汇编:把汇编源程序转变为目标程序。

反编译

 将可执行的程序经过分析转变为高级语言的源代码格式。

 一般我们拿到的jar包都是经过编译后的 .class文件,如何想查看源码,就可以通过一些反编译工具,将 .class 文件逆向成 java源代码。

反汇编

 反汇编与反编译原材料都是 .class文件。但是反编译是向上的,即根据编译后的结果,反向得到编译前的源码。

 反汇编是将可执行的文件中的二进制经过分析转变为汇编程序。是根据编译后的结果,倒推源码编译的过程,从而看出代码逻辑真实编译、执行过程的每一步,对于性能优化、问题追溯等具有十分强大的帮助。

 Java反汇编使用jdk自带的工具——javap。可以通过命令行 输入 :javap -c XXX.class 文件来查看该class文件的编译过程。


生产问题定位

CPU占用过高(定位问题)

  • 'top' 命令获取进程列表,查找占用高的进程编号pid

  • 'top -Hp pid',显示一个进程的线程运行信息列表。

  • printf '%x\n' 线程ID,将查看到的占用高的线程的线程号转化成16进制的数

  • 'jstack 线程ID' 获取线程运行的堆栈信息

  • 问题线程的最开始‘#数字’表示出现问题的行数,回到代码查看

堆内存诊断

  • ‘jps’获取运行进程号

  • ‘jmap -heap 进程号’查看当前时刻的堆内存信息

  • jconsole/jvisualvm,可视化的检测连续的堆内存信息。


参考:https://chenchenchen.blog.csdn.net/article/details/106048793


内存参数调整优化内存

  • -Xms堆内存最小值(超过初始值会扩容到最大值),minimum memory size for pile and heap。

  • -Xmx堆内存最大值(通常初始值和最大值一样,因为扩容会导致内存抖动,影响程序运行稳定性),maximum memory size for pile and heap。

  • -Xmn堆新生代的大小;

  • -Xss设置线程栈的大小(影响并发线程数大小);

  • -XX:NewRatio指定堆中的老年代和新生代的大小比例, 不过使用CMS收集器的时候这个参数会失效。

  • -XX:MeatspaceSize和-XX:MaxMetaspaceSize,设置方法区的初始大小和最大值,替代了JDK8之前的参数-XX:PermSize和-XX:MaxPermSize。


容器中配置(阿里云/k8s/docker)

MaxRAMPercentageInitialRAMPercentageMinRAMPercentage:容器模式下,给每个JVM实例所属的POD分配的内存上限占POD总内存的比例。

服务所在pod为2核4G,配置指定75%(-XX:MaxRAMPercentage=75 -XX:InitialRAMPercentage=75 -XX:MinRAMPercentage=75),就相当于设置了-Xmx3g -Xms3g。

  • 当未设置InitialHeapSize/-Xms时,-XX:InitialRAMPercentage用于计算初始堆大小

  • 当未设置MaxHeapSize/-Xmx时,-XX:MaxRAMPercentage和-XX:MinRAMPercentage都用于计算最大堆大小:

  • 对于物理内存较小的系统,MaxHeapSize估计为

    phys_mem * MinRAMPercentage / 100  (if this value is less than 96M)
  • 否则(非小物理内存)MaxHeapSize估计为

    MAX(phys_mem * MaxRAMPercentage / 100, 96M)

确切的公式要复杂一些,因为它还要考虑其他因素。用于计算初始堆大小和最大堆大小的算法取决于特定的JVM版本,控制堆大小的首选方法是显式设置Xmx和Xms。


JVM内存结构中堆和栈的区别

  • 管理方式:栈自动释放,堆需要GC

  • 空间大小:栈比堆小

  • 碎片:栈产生的碎片远少于堆

  • 分配方式:栈支持静态分配和动态分配,堆只支持动态分配

  • 效率:栈的效率比堆高



参考:

Java8 和 Java7中JVM内存模型有什么区别:https://www.cnblogs.com/july-sunny/p/12628820.html

Java8中JVM内存结构变了:https://cloud.tencent.com/developer/article/1546965

JVM 内存结构|1.7 1.8 区别详解:https://blog.csdn.net/lovely_girl1126/article/details/106806879

面试官问我平时写的Bug的存储位置:https://mp.weixin.qq.com/s/Hr2JSEZ9HH5TTowjXlbaVQ

Linux命令-查看内存、GC情况及jmap 用法:https://chenchenchen.blog.csdn.net/article/details/106048793

JVM内存参数的含义InitialRAMPercentage和MinRAMPercentage:https://www.it1352.com/1521964.html


文章转载自陈晨辰呀,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论