JVM学习--JVM运行时数据区图文详解

Posted 肖帆咪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习--JVM运行时数据区图文详解相关的知识,希望对你有一定的参考价值。

JVM组合拳往期文章

JVM运行时数据区

1.运行时数据区组成概述

java8虚拟机规范规定,java虚拟机所管理的内存将会包含以下几个运行时数据区域:

1.1程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器

1.2java虚拟机栈(Java Virtual Machine Stacks)

描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个线帧用于存储局部变量表,操作数栈,单台连接,方法出口等信息,每个方法调用到执行完成的过程就是一个线帧在虚拟机栈中入栈出栈的过程

1.3本地方法栈(Native Method Stack)

与虚拟机栈的作用是一样的,只不过虚拟机栈是服务java方法的,而本地方法栈是虚拟机调用Native方法服务的

1.4java堆(Java Heap)

java虚拟机中内存最大的一块,被所有线程共享,在虚拟机启动时创建,java堆唯一的目的就是存放对象实例.

1.5方法区(Methed Area)

用于存储被虚拟机加载的类信息,常量,静态变量,即时变异后的代码等.

内存区域是硬盘和CPU的中间桥梁,承载着操作系统和应用程序的实时运行.

JVM内存布局规定了Java在运行过程中内存申请,分配,管理策略,保证JVM的高效稳定.

不同的JVM对内存的划分方式和管理机制存在着部分差异,以HotSpot虚拟机为例
在这里插入图片描述

红色代表线程共享,灰色代表单线程私有

**线程间共享:**堆,对外内存

**线程独立:**程序计数器,栈,本地方法栈
在这里插入图片描述

2程序计数器(Program Counter Register)

2.1概述

JVM中的程序计数寄存器中的Register命名来源于CPU寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装在到寄存器才能运行.

2.2作用

程序计数器用来存储下一条指令的地址,由执行引擎读取下一条指令

在这里插入图片描述

  1. 他占很小的内存空间,也是运行速度最快的存储区域
  2. 每个线程都有自己的程序计数器,是线程私有的,生命周期和线程的生命周期一致
  3. 任何时间,一个线程只有一个方法执行,程序计数器存储当前方法的JVM指令地址,如果在执行native方法,则是未指定值(undefined)
  4. 他是程序控制流的指示器,分支,循环,跳转,异常处理,线程回复等基础功能都需要依赖这个计数器完成
  5. 字节码解释器工作时通过改变计数器的值来选取下一条需要执行的字节码指令
  6. 唯一一个在JVM中没有规定任何OutOfMemoryError情况的区域

程序计数器的作用位置
在这里插入图片描述

2.3面试题

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

    因为CPU不停的切换各个线程,在切换回来时候,需要知道从哪个位置继续向下执行.

    JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令

  2. 程序计数器为什么被设定为线程私有的

    所谓的多线程 其实是在一段特定的时间执行其中一个线程,因为执行的时间段,PCU切换快,所以在用户看来就是多线程,在切换过程中必然导致中断或恢复,如何保障呢个分毫不差?

    为了精确记录正在执行的字节码指令地址,最好的办法是为每一个线程分配一个程序计数器,这样每个线程都可以独立计算,互不干扰

3java虚拟机栈(Java Virtual Machine Stacks)

3.1虚拟机栈出现的背景

由于跨平台的设计,java的指令根据栈设计,不同平台CPU架构不同,所以设计为基于栈的指令设计

优点:跨平台,指令集小,编译器易实现

缺点:性能下降,同样的功能需要的指令集多

3.2分清栈和堆

**栈:**运行时单位,解决程序的运行问题

**堆:**存储的单位,解决数据存储的问题

在这里插入图片描述

3.3java虚拟机栈是什么

Java Virtual Machine Stack,也叫java栈,每个线程在创建时都会创建一个虚拟机栈,内部保存一个个的栈帧,对应一次方法的调用

Java虚拟机栈是线程私有的,生命周期和线程一样

3.4作用

主观Java程序的运行,保存局部变量(8中基本数据类型,对象的引用地址),部分结果,参与方法的调用和返回
在这里插入图片描述

3.5栈的特点

  1. 快速的分配存储方式,访问速度仅次于程序计数器
  2. JVM对java栈的操作有两种:调用方法,进栈----->执行结束,出栈
  3. 不存在垃圾回收问题

在这里插入图片描述

3.6栈中出现的异常

  1. StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
  2. OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存

3.7栈中存储什么

每个线程都有自己的栈,栈的数据都以栈帧为单位存储

在这个线程正在执行的每一个方法都对应一个栈帧

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

3.8栈的运行原理

  1. JVM对栈的操作:对栈帧的压栈和出栈,遵循"先进后出"原则
  2. 在一条活动的线程中的一个时间点上,只会有一个活动栈,即只有当前在执行的方法的栈帧是有效的,这个栈帧被称为当前帧,对应的方法叫做当前方法,定义方法的类叫当前类
  3. 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  4. 若在该方法中国调用其他方法,对应的新的栈帧就会创建出来,放在栈的顶端,成为新的当前栈帧
    在这里插入图片描述

在一个栈中不可能引用另一个线程的栈帧(方法)

3.9栈帧的内部结构

局部变量表(Local Variables)

​ 局部变量表十一组变量存储空间,存放方法参数和方法内部定义的局部变量.对于基本数据类型的变量,直接存储他的值,对于引用类型的变量,则存的是指向对象的引用

操作数栈(Operand Stack)或表达式栈

​ 栈最典型的一个应用就是用来对表达式求值,一个线程执行方法的过程就是不断执行语句的过程,归根到底是进行计算的过程.程序中所有计算过程都是借助操作数栈来完成的

动态链接(Dynamic Linking)

​ 在方法执行的过程中有可能需要用到类中的常量,所以必须有一个引用指向运行时常量

方法返回地址(Return Address)

​ 当一个方法执行完毕后,要返回调用它的地方,因此在栈帧中必须保存一个方法返回地址

附加信息

在这里插入图片描述

3.10面试题

  1. 什么情况会出现栈溢出?

    方法执行时创建的栈帧超过了栈的深度,最优可能的就是方法递归调用产生这种结果

  2. 通过调整栈大小,就可以保证不出现溢出吗?

    不能

  3. 分配的栈内存越大越好吗?

    并不会,只能延缓这种现象的出现,可能会影响其他内存空间

  4. 垃圾回收机制会涉及到虚拟机栈吗

    不会

4本地方法栈(Native Method Stack)

  1. java虚拟机栈管理java方法的调用,本地方法栈用于管理本地方法的调用

  2. 本地方法栈线程私有

  3. 允许被实现成固定或者可动态扩展的内存大小,内存溢出方面也是相同的

    ​ 若线程申请分配的栈容量超过本地方法栈允许的最大容量抛出StackOverflowError

    ​ 若本地方法可以动态扩展,在扩展时无法申请到足够的内存抛出OutOfMemoryError

  4. 本地方法栈使用c语言编写

  5. 具体实现是在Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库

5java堆内存

5.1堆内存概述

在这里插入图片描述

  1. 一个JVM实例只存在一个堆内存,堆也是java内存管理的核心区域

  2. java堆区在JVM启动时被创建,其空间大小也就被确定,是JVM管理的最大一块内存空间

  3. 堆内存大小可以调节

    ​ -Xms10m(堆起始大小) -Xmx30m(堆最大内存大小)

  4. 对在物理上是不连续的,在逻辑上应该被视为连续

  5. 所有的线程共享java堆,在这还可以划分线程私有的缓冲区

  6. 所有的对象实例都应在运行时分配在堆上

  7. 方法结束后,堆中的对象不会马上移除,仅仅垃圾收集的时候才会被移除

  8. 堆,是GC执行垃圾回收的重点区域

5.2对内存区域划分

Java8之后堆内存分为:新生区(新生代)+老年区(老年代)

新生区被分为Eden(伊甸园)区和Survivor(幸存者)区
在这里插入图片描述

5.3为什么分区

将对象根据存活概率分类,时间长的对象,放在固定区,减少扫描垃圾的时间及GC频率.针对不同的地区使用不同的回收算法,对算法扬长避短

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

5.4对象创建内存分配过程

为新对象分配内存是一件非常严谨和复杂的任务,不仅需要考虑内存如何分配,在哪分配等问题,由于内存分配算法与内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片.

  1. new 的新对象先放到伊甸园去,此区大小有限制
  2. 当伊甸园填满时,又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将不再被其他对象所引用的对象进行销毁.再加载新的对象放到伊甸园区.
  3. 然后将伊甸园区中的剩余对象移动到幸存者 s0 区
  4. 如果再次出发垃圾回收,此 时上次幸存下来存放到幸存者 s0 区的对象,如果没有回收, 就会被放到幸存者 s1 区,每次会保证有一个幸存者区是空的
  5. 如果再次经历垃圾回收,此时会重新放回幸存者 s0 区,接着再去幸存者 s1 区
  6. 默认是 15 次会存放在老年区,可以通过-XX:MaxTenuringThreshold=设置参数
  7. 老年区只有内存不够使才会触发GC:Major GC,进行养老区的内存清理
  8. 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space

在这里插入图片描述

5.5新生区与老年区配置比例

配置新生代与老年代在堆结构的占比

  1. 默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
  2. 可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
  3. 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来
    进行调优

在这里插入图片描述

在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发
人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如-XX:SurvivorRatio=8
新生区的对象默认生命周期超过 15 ,就会去养老区养老

在这里插入图片描述

5.6分带收集思想Minor GC,Major GC,Full GC

​ JVM在进行GC时,大部分是对新生区回收.针对HotSpot VM的实现,其中的GC按照回收区域分为两大类型:一种是部分收集,一种是整堆收集

**部分收集:**不是收集整个java堆的垃圾收集

  1. 新生区收集(Minor GC/Yong GC):只是新生区(Eden,s0,s1)的垃圾收集
  2. 老年区收集(Major GC/Old GC):只是老年区的垃圾收集
  3. 混合收集(Mixed GC):收集整个新生区以及老年区的垃圾

**整堆收集:**收集整个java堆和方法区的垃圾收集

​ 整堆收集出现的情况:

​ System.gc();

​ 老年区空间不足

​ 方法区空间不足

5.7TLAB机制

  1. 为什么会有TLAB(Thread Local Allocation Buffer)

    堆区是线程共享的区域,任何线程都可以访问到堆区中的共享数据

    由于对象实例创建在JVM中十分频繁,在并发环境下从堆区划分内存空间是线程不安全的

    为避免多个线程操作同一个地址,需要使用加锁等机制,进而影响分配速度

  2. 什么是TLAB

    全名Thread Local Allocation Buffer,线程本地分配缓存区

    JVM使用TLAB避免多线程冲突,再给对象分配内存时,每个线程会有TLAB,避免线程同步,提高对象分配的效率

    TLAB空间非常小,缺省情况下只占Eden的1%,通过-XX:TLABWasteTargetPercent设置占比

在这里插入图片描述

5.8堆空间的参数设置

官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html -XX:+PrintFlagsInitial 查看所有参数的默认初始值

-XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)

-Xms:初始堆空间内存(默认为物理内的 1/64)

-Xmx:最大堆空间内存(默认为物理内存的 1/4)

-Xmn:设置新生代的大小(初始值及最大值)

-XX:NewRatio:配置新生代与老年代在堆结构的占比

-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例

-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄

-XX:+PrintGCDetails 输出详细的 GC 处理日志

5.9字符串常量池

字符串常量池为什么调整位置?

​ JDK7将字符串常量池放在堆空间中.因为永久代的回收效率低,在Full GC时才会执行垃圾回收,而Full GC是老年代空间不足,永久代不足时才会触发,导致StringTable回收效率不高,在开发中有大量的字符串被创建,回收效率低,导致永久代内存不足.放在堆里,能及时回收内存

6方法区

6.1方法区的基本理解

方法区,**是线程共享的内存区域.**主要存储加载的类字节码,class/method/field等元数据,static final常量,static 变量,即时编译器编译后的代码等数据.方法区包含一个特殊的区域"运行时常量池".

方法区看做一个独立于java堆的内存空间

在这里插入图片描述

方法区在JVM启动时创建,物理内存地址是不连续的

大小可以选择固定大小或者可扩展大小

若系统定义太多的类,导致方法区溢出,抛出异常java.lang.OutMenoryError:Metaspace

关闭 JVM 就会释放这个区域的内存.

方法区,栈,堆的交互关系

在这里插入图片描述

6.2方法区大小设置

  1. 元 数 据 区 大 小 可 以 使 用 参 数 -XX:MetaspaceSize 和-XX:MaxMataspaceSize 指定,替代上述原有的两个参数
  2. 默认值依赖于平台,windows 下,-XXMetaspaceSize 是 21MB
  3. -XX:MaxMetaspaceSize 的值是-1,级没有限制.
  4. 这个-XX:MetaspaceSize 初始值是 21M 也称为高水位线 一旦触及 就会触发 Full GC
  5. 因此为了减少 FullGC 那么这个-XX:MetaspaceSize 可以设置一个较高的值

6.3方法区的内部结构

在这里插入图片描述

方法区用于存储:被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存,运行常量池

通过反编译字节码文件查看.

反编译字节码文件,并输出值文本文件中,便于查看。参数 -p 确保能查看private 权限类型的字段或方法

javap -v -p Demo.class > test.txt

6.4方法区的垃圾回收

  1. 方法区有垃圾收集行为,《Java虚拟机规范》提到对方法区的约束宽松,不要求虚拟机在方法区中实现垃圾收集

  2. 该区域的回收效果不好,在类型的卸载,条件苛刻,但是该区域的回收有时又是必须的

    方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用
    的类型。
    回收废弃常量与回收 Java 堆中的对象非常类似。(关于常量的回收比较简单,
    重点是类的回收)

下面也称作类卸载

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子
    类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加
    载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通
    过反射访问该类的方法

以上是关于JVM学习--JVM运行时数据区图文详解的主要内容,如果未能解决你的问题,请参考以下文章

JVM学习--垃圾回收机制

详解Jvm内存结构

详解Jvm内存结构

详解Jvm内存结构

JVM学习--JVM本地方法和执行引擎

JVM 运行时数据区详解