JVM 运行时数据区

运行时数据区的划分

在这里插入图片描述

  • 方法区
  • 程序计数器(PC 寄存器)
  • 本地方法栈
  • Java 虚拟机栈

程序计数器(PC 寄存器)

PC 寄存器用来存储指向下一条指令的地址,也就是将要执行的指令代码。由执行引擎读取下一条指令。

  • PC 寄存器是很小的一块内存空间,也是运行速度最快的存储区域
  • 每个线程都有它的 PC 寄存器,使线程私有的,生命周期与线程一致。
  • 任何时间一个线程都只有一个方法在执行,这就是所谓的当前方法。程序计数器会存储线程执行的方法的 JVM 指令地址。
    • 如果当前方法为 native ,则是未指定值(undifend)
  • PC 寄存器是唯一没有 GC 和 OOM (OutOfMemoryError) 的区域。

PC 寄存器的作用

CPU 需要不停的切换各个线程,当切换到某个线程的时候,CPU 得知道上次执行到哪儿了,从哪儿继续开始执行。CPU 通过改变 PC 寄存器的值来明确下一条要执行的指令是什么。

Java 虚拟机栈

  • 每个线程都对应一个 Java 虚拟机栈,其内部分为一个一个的栈帧(stack Frame),对应着一次一次的 Java 方法调用。Java 虚拟机栈就是我们常说的堆空间和栈空间中的栈空间。
  • 主管 Java 程序的运行,他保存方法的局部变量、部分结果,并参与方法的调用和返回。
  • Java 虚拟机栈也是没有 GC 的,栈是存在 OOM 的。
  • Java 虚拟机栈的大小可以是动态的也可以是固定不变的
    • 如果是固定的:当线程请求分配到栈的容量超过这个大小,Java 虚拟机将抛出 StackOverflowError 异常。
    • 如果是动态大小:当没有足够的内存使用的时候,Java 虚拟机将抛出 OutOfMemoryError 异常。
  • 可以使用 JVM 参数 -Xss 设置虚拟机栈的大小,比如:-Xss2m 表示 2M,2g 表示 2G。

关于相关命令可查看官方文档 https://docs.oracle.com/javase/8/
在这里插入图片描述

栈帧(stack Frame)

每个线程都有自己的虚拟机栈,栈中的数据都是以栈帧的格式存在,线程上执行的每个方法各自对应一个栈帧。栈帧中存储这方法执行过程中的各种数据信息。某个时间点上只能执行某一个方法,此方法我们可以称作当前方法,其对应的栈帧称为当前栈帧。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。如果该方法中调用了另一个方法,那么另一个方法对应的栈帧将被压栈到栈的顶端,成为当前栈帧。

在这里插入图片描述

  • 如果有方法调用其他方法,当其他方法返回的时候,当前栈帧会传回此方法的执行结果给前一个栈帧,然后丢弃当前栈帧,是前一个栈帧成为当前栈帧。

栈帧的内部结构

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)或者表达式栈
  • 动态链接(Dynamic Linking)或 指向运行时常量池的方法引用
  • 方法返回地址(Return Address)
  • 附件信息
局部变量表

局部变量表定义为一个数字数组, 主要用于存储方法参数和定义在方法中的局部变量(基本数据类型、对象引用),以及返回值(returnAddress 类型)。局部变量表所需的内存大小是编译期确定的,其在方法运行期间不会改变。

  • 参数值从局部变量数组的第 0 位开始存储
  • 局部变量表的基本单位是 Slot(槽),比如第 0 位,即是第一个 Slot
  • 局部变量表中存储编译期可知的各种基本数据类型、引用类型、retrunAddress 类型的变量。
  • 32 位以内的类型(byte、short、char、int、boolean、float)占用一个 Slot,64位的类型(long、double)占据两个 Slot。
  • 如果当前栈帧是由构造方法或示例方法对应的栈帧,那么当前对象的引用 this 将被存放在第 0 位的 Slot 中,其余的参数安装参数表的顺序存放。
  • JVM 通过局部变量表来实现方法的调用以及值的传递。
操作数栈

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入或提取数据,即入栈 push 和出栈 pop。其是由数组或链表来实现的。

  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。
  • 每一个操作数栈都拥有一个明确的栈深度来存储数值,最大深度在编译器确定。
  • 操作数栈中任何一个元素都可以是任意的 Java 类型(32bit 占一个栈深度、64bit 占两个栈深度)
  • 操作数栈只能通过入栈和出栈操作来访问数据。
  • 如果被调用的方法有返回值,其返回值将会被压入当前栈帧的操作数栈中

栈顶缓存技术,JVM 将栈顶的数据缓存在物理CPU的寄存器中,依此降低内存的读写次数,提升执行引擎的执行效率。

动态链接

在 Java 源文件被编译为字节码文件时,所有的变量和方法引用都会作为符号引用(Symbolic Reference)保存在 class 文件的常量池里。动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

方法返回地址

存放调用该方法的 PC 寄存器的值。( 在方法执行结束时 PC 寄存器的值,其对应的是调用该方法的指令的下一条指令的值)

本地方法栈

本地方法

Java 中用 native 关键字修饰的方法为本地方法,主要用于调用 Java 以外的语言,如 C/C++ 语言的程序。Java 在要调用底层操作系统提供的功能时,即需要这类方法。比如 Object 类中的 getClass 方法即为本地方法

public final native Class<?> getClass();

本地方法栈用于管理本地方法,由线程私有,可以被设定为固定大小或者动态扩展的内存,本地方法在本地方法栈中记录,由执行引擎调用。

  • 一个 JVM 实例只存在一个堆内存,堆是 Java 内存管理的核心区域
  • Java 堆区在 JVM 启动的时候就创建,其空间大小也就确定了,堆是 JVM 管理的最大的一块内存空间。
    • -Xms512m -Xmx512m 表示,初始堆空间为 512m,最大的堆空间为 512m
  • 物理上堆可以处于不连续的内存空间,但在逻辑上它需要是连续的
  • 所有的线程共享堆空间
  • 所有的对象实例,数组等引用类型对象都是在运行时分配到堆中,然后在栈(Java 虚拟机栈,更确切点是在栈帧中)保存对象的引用地址。
  • 堆中的对象在使用完毕不会被马上清除,而是通过 GC (Garbage Collection,垃圾收集器)进行回收。堆是 GC 的重点区域。

堆空间的内存划分(分代)

Java 1.7 分代

  • Young Generation Space 新生区(年轻代 或 新生代)
    • Eden 区(伊甸区)
    • Survivor 区(幸存者区)
  • Tenure Generation Space 养老区(老年区 或 老年代)
  • Permanent Space 永久区(永久代)

Java 1.8 分代

  • Young Generation Space 新生区(年轻代 或 新生代)
    • Eden 区(伊甸区)
    • Survivor 区(幸存者区)
  • Tenure Generation Space 养老区(老年区 或 老年代)
  • Meta Space 元空间

设置堆空间大小

堆空间在 JVM 启动时确定,我们可以通过 -Xmx -Xms 来设置大小

  • -Xms :用来设置堆的初始化内存大小。等价于 -XX:InitialHeapSize。比如:-Xms20m
  • -Xmx:设置堆的最大可用内存大小。等价于 -XX:MaxHeapSize。比如:-Xmx20m
  • -Xmn:直接设置新生代内存大小。建议新生代的大小保持在堆总大小的一半到四分之一之间。

当堆被使用的空间超过 -Xmx 设置的大小时,就会出现 OOM 异常(OutOfMemoryError)

一般情况下 -Xms 和 -Xmx 设置为同一大小,是为了在 Java GC 之后不再重新计算堆区的大小,以提高性能。

默认情况下 -Xms 为 电脑内存大小的 1/64。-XmX 为电脑内存的 1/4。

-Xms 和 -Xmx 设置的堆空间大小是 年轻代 + 老年代的总和,不包括持久代(元空间)
在这里插入图片描述

调整年轻代和老年代比例

-XX:NewRatio=2,表示老年代空间大小 : 新生代空间大小 = 2;实际就是新生代占1/3,老年代占2/3。这是默认比率,通过 -XX:NewRatio 参数来设置。其官方说明如下: 在这里插入图片描述

调整年轻代中Eden区和两个survivor区的比率

-XX:SurvivorRatio=8,默认为 Eden:From:To = 8:1:1。与其对应的还有一个参数 -XX:InitialSurvivorRatio 设置初始化幸存者空间比率。但默认情况下,此比率受自适应大小策略影响,也就是说,实际情况下,默认的幸存者空间比率不一定是8。官网在 InitialSurvivorRatio 参数中说明如下:

默认情况下,通过使用-XX:+UseParallelGC和-XX:+UseParallelOldGC选项,吞吐量垃圾收集器会启用自适应大小调整,并且根据应用程序行为从初始值开始调整生存空间的大小。如果禁用了自适应大小调整(使用-XX:UseAdaptiveSizePolicy选项),则应使用-XX:SivitorRatio选项为应用程序的整个执行设置幸存者空间的大小。

官方说明如下:

-XX:InitialSurvivorRatio=ratio
Sets the initial survivor space ratio used by the throughput garbage collector (which is enabled by the -XX:+UseParallelGC and/or -XX:+UseParallelOldGC options). Adaptive sizing is enabled by default with the throughput garbage collector by using the -XX:+UseParallelGC and -XX:+UseParallelOldGC options, and survivor space is resized according to the application behavior, starting with the initial value. If adaptive sizing is disabled (using the -XX:-UseAdaptiveSizePolicy option), then the -XX:SurvivorRatio option should be used to set the size of the survivor space for the entire execution of the application.

The following formula can be used to calculate the initial size of survivor space (S) based on the size of the young generation (Y), and the initial survivor space ratio (R):

S=Y/(R+2)
The 2 in the equation denotes two survivor spaces. The larger the value specified as the initial survivor space ratio, the smaller the initial survivor space size.

By default, the initial survivor space ratio is set to 8. If the default value for the young generation space size is used (2 MB), the initial size of the survivor space will be 0.2 MB.

The following example shows how to set the initial survivor space ratio to 4:

-XX:InitialSurvivorRatio=4

# 禁用自适应,需要和 -XX:SurvivorRatio 选项一起使用
-XX:+UseAdaptiveSizePolicy
Enables the use of adaptive generation sizing. This option is enabled by default. To disable adaptive generation sizing, specify -XX:-UseAdaptiveSizePolicy and set the size of the memory allocation pool explicitly (see the -XX:SurvivorRatio option).

Java 代码获取堆空间大小

public class PrintMemory {

    public static void main(String[] args) {

        Runtime runtime = Runtime.getRuntime();

        // 初始化内存大小
        long initMem = runtime.totalMemory();
        // 最大内存大小
        long maxMem = runtime.maxMemory();
        // 剩余内存大小
        long freeMem = runtime.freeMemory();

        System.out.printf("initMem:%dM,maxMem:%dM,freeMem:%dM",toM(initMem),toM(maxMem),toM(freeMem));
    }

    public static final long toM(long size){
        return size / 1024 / 1024;
    }

}

对象在堆中的分配

  1. 对象被 new 之后,首先放在 Eden 区。

  2. 当 Eden 区内存空间不足(满了),没有空间存放新 new 的对象时,JVM 会进行垃圾回收(此时的垃圾回收称为 YGC或 Minor GC),此时会将 Eden 区不再使用的对象(有多种判断方式,这个我们后面再说)进行销毁,还在使用的对象移动到 Survivor 0 区,并为其标记年龄(初次放入幸存者区的对象年龄标记为 1)
    在这里插入图片描述

在这里插入图片描述

如果按 From 区和 To 区来区别,那么当前哪个幸存者区为空,谁就是 To 区。

  1. 当 Eden 区再次空间不足再次触发 YGC,当前 Eden 区存活的对象,移动到 s1 区,记录当前年龄为 1,此时需要判断“对象3”,如果其还存活,则“对象3”也会移动到 s1 区,且年龄加1。
    在这里插入图片描述

在这里插入图片描述

每次 YGC s0 或 s1 都会被清空,等待下次 YGC 时存入对象,然后清空另一个。

  1. 当前经历多次 YGC后,“对象3”如果依然存活,且其年龄达到 15,“对象3”将被移动到老年代(元空间)。

特殊情况:
当进行 YGC 时,如果s0或s1区无法存放下所有存活对象,此时,这些对象即便年龄没到15,也会被直接申请放入老年代(元空间)
当新new的对象太大,导致整个Eden 区都放不下时,对象也会直接被申请放入老年代(元空间)

当对象被申请放入老年代时,如果老年代空间不足,则会触发 FGC ,如果 FGC 之后,老年代依然放不下这些对象,则会报 OOM。

我们可以通过 -XX:MaxTenuringThreshold=15,来设置这个阈值。CMS 收集器,此值默认为6.
在这里插入图片描述

Minor GC、Major GC、Full GC

Hotspot 虚拟机的 GC,一种为部分回收(Partial GC),一种为整堆回收(Full GC)

  • Partial GC
    • Minor GC: Minor GC 就是 YGC ,新生代的垃圾收集。在 Eden 区空间不足时,就会触发 Minor GC。
    • Major GC:Major GC 就是 OGC(Old GC),老年代的垃圾收集。老年代空间不足时,会先尝试进行 Minor GC,如果之后空间还不足,才进行 Major GC。

Major GC 时 Stop The world 的时间比 Minor GC 长很多。如果Major GC 后内存还不足,就报OOM。

  • Full GC:收集整个 Java 堆和方法区

不同的垃圾回收器,可能有所不同,可根据具体情况而定。如我们上面的图中,FGC 可以认为是 Major GC。因为 FGC 和 Major GC 在很多地方都是混同使用的。Full GC 对服务器资源的消耗是很大的,在实际使用中,我们会尽量的避免出现 Full GC,下面几种情况下会触发 Full GC

  • 调用 System.gc()时,调用不等于会立即执行GC,执行时是 Full GC。
  • 老年代空间不足时(YGC 后存活对象进入老年代时),会进行 Full GC
  • 方法区空间不足时,会进行 Full GC

可通过 -XX:+PrintGCDetails 参数开启程序 GC 日志打印。默认此设置为关闭。

TLAB (Thread Local Allocation Buffer)线程私有缓冲区

  • 堆是所有线程共享的区域,任何线程都可以访问堆区中的共享数据。
  • 由于对象实例创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
  • 为避免多个线程操作同一地址,需要使用加锁同步机制,导致效率降低。

TLAB包含在Eden区内,是JVM 为每一个线程分配的线程私有的内存区域,多线程情况下,TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,这种内存分配方式称为快速分配策略。

  • -XX:+UseTLAB 开启的TLAB,-XX:-UseTLAB 关闭TLAB 空间。TLAB 默认是开启的。
  • -XX:TLABSize=size,设置 TLAB 的初始化空间大小,如果设置为0,则JVM会自动设置大小。

栈上分配和代码优化

  • 逃逸分析:分析一个对象有没有逃逸出方法的技术。(当一个对象在方法内部创建,且没有方法外部的引用使用它,那么当方法结束时,其生命周期也随之结束,这种情况,该对象没有发生逃逸。反之,其发生了逃逸)

-XX:+DoEscapeAnalysis 开启逃逸分析,-XX:-DoEscapeAnalysis 关闭。默认为开启。

  • 栈上分配:如果一个对象没有发生逃逸且开启了逃逸分析,则此对象就可能被优化为栈上分配。在栈上分配的对象,在方法执行完成之后,栈空间释放,该对象也会随之释放,这些对象没有进入堆空间,就不会进行GC了。

基于逃逸分析和栈上分配,我们应该尽可能的不让局部变量发生逃逸。

  • 同步省略(锁消除):如果一个对象只能被一个线程访问到,那么对应这个对象的操作可以不用考虑同步。逃逸分析的时候,可以分析当前同步块所使用的锁对象是否只能被一个线程访问,如果是那么在编译该同步块的时候就会取消该同步锁,这个取消同步锁的过程就是同步省略。
    public void test(){
        Object obj = new Object();
        
        synchronized (obj){
            System.out.println("abc");
        }
        
    }

此方法中的 synchronized 的锁为 obj,obj又是局部变量,那么此时 synchronized 块其实是无意义的,实际使用中也不能这样写。

  • 标量替换:经过逃逸分析,如果一个对象不会被外部访问,那么在JIT 阶段,就会被 JIT 优化,就是把这个对象拆解为若干个变量来替换,这个过程就是标量替换。

标量:一个无法再分解成更小的数据单元的数据。Java 中的原始数据类型即是标量。
聚合量:与标量相对,还能再分解为更小的数据单元的数据。

方法区

  • 方法区(Method Area)是各个线程共享的区域。
  • 方法区域是在虚拟机启动时创建的。
  • 方法区域可以是固定的大小,也可以根据计算需要进行扩展,如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。
  • 方法区存储类结构,如运行时常量池(Runtime Constant Pool)、字段和方法数据,以及方法和构造函数的代码,包括特殊方法用于类和实例初始化以及接口初始化。

基于此,如果我们在运行时生成过多的类,即可能造成方法区 OOM,比如cglib动态代理生成过多的类字节等。

字节码文件中有常量池结构,其通过 ClassLoader 加载到内存后,就称为运行时常量池。常量池中包含:数值、字符串值、类引用、字段引用、方法引用。

  • 如果方法区域中的内存无法满足分配请求,Java虚拟机将抛出OutOfMemoryError 。OutOfMemoryError :PermGen space 或者 OutOfMemoryError :Metaspace
  • 永久代(JDK7 及以前,我们称方法区的内存区域为永久代),JDK 8 及以后称为元空间。

设置方法区大小

JDK 7 及以前

  • 通过 -XX:PermSize 来设置永久代初始化空间大小。
  • -XX:MaxPermSize 来设置永久代最大可用空间大小。

JDK 8 及以后

  • -XX:MetaspaceSize=size

设置元空间初始化大小,当空间用尽时,会触发一次 GC,GC后会根据元空间中的数据来增加(GC 后可用空间任然较小)或减少(GC 后可用空间剩余很多)大小。默认大小取决于当前系统的可用内存。

  • -XX:MaxMetaspaceSize=size

设置元空间最大可用大小。默认:不限制。虽然不限制,可用空间也取决于当前系统本身的可用大小。所以在实际应用中,我们一般不设置最大可用大小。

方法区存储的内容

  • 类型信息:类全名、直接父类全名、类的修饰符(public、final等)、类实现的接口列表。
  • Field 信息(域信息、属性信息):名称、类型、修饰符(public、final等)
  • 方法信息:方法名、返回值类型、参数、方法修饰符、方法的字节码、操作数栈、局部变量表、异常表等
  • 运行时常量池
  • 静态变量
  • 即时编译器编译后的代码缓存等
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值