Java17中的内存分布

一、概述

在Java 17中,虚拟机的内存分布依然遵循Java虚拟机规范,但相较于之前版本,在某些区域和特性上有所调整和优化。Java虚拟机的内存结构主要分为以下几个区域:堆(Heap)、方法区(Metaspace,替代了之前的永久代)、虚拟机栈(Java Virtual Machine Stacks)、本地方法栈(Native Method Stacks)和程序计数器(Program Counter Register)。这些区域共同协作,支持Java应用程序的高效执行。

二、内存分布介绍

1. 堆(Heap)

(1)特点

堆是Java虚拟机管理的最大一块内存区域,主要用于存储对象实例。堆内存是由垃圾回收器(Garbage Collector)管理的,当对象不再被引用时,垃圾回收器会自动回收其占用的内存。堆内存是由所有线程共享的,因此不需要考虑线程安全问题。堆内存的大小可以通过JVM启动参数来指定,也可以根据应用程序的需要进行动态调整。

(2)内存划分

堆内存细分为年轻代(Young Generation)和老年代(Old Generation)两个区域。

  • 年轻代(Young Generation):年轻代用于存储新创建的对象。大多数对象在年轻代内被创建和销毁。年轻代进一步分为Eden区和两个Survivor区(S0和S1)。新的对象分配是首先放在Eden区,Survivor区作为Eden区和老年代的缓冲,在Survivor区的对象经历若干次收集仍然存活的,就会被转移到老年代。

    • Eden区:大部分的对象会在Eden区中生成。当Eden区没有足够的空间容纳新对象时,会触发Young Garbage Collection(YGC),将存活的对象移动到Survivor区。

    • Survivor区(S0和S1):每次进行YGC的时候,会将存活的对象复制到未使用的那块内存空间(S0和S1交替使用),然后将当前正在使用的空间完全清除掉。如果YGC要移送的对象Survivor区无法容纳,那么就会将该对象直接移交给老年代。每一个对象都有一个计数器,当每次进行YGC的时候,计数器加1。通过-XX:MaxTenuringThreshold参数可以配置当计数器的值到达某个阈值时,对象就会从新生代移送至老年代。该参数的默认值为15,也就是说对象在Survivor区中的S0和S1内存空间交换的次数累加到15次之后,就会移送至老年代。

  • 老年代(Old Generation):老年代主要存放生命周期较长的对象,从新生代晋升过来的对象存储在这里。当老年代满时会触发Full GC(Major GC),进行整理和回收。

(3)内存分配策略

  • 对象优先在Eden区分配:大多数情况下,对象在新生代的Eden区中分配。

  • 大对象直接进入老年代:大对象是指需要大量连续内存空间的Java对象,典型大对象有长字符串和数组。虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样可以避免在Eden及两个Survivor区间发生大量的内存复制。

  • 根据对象年龄判定进入老年代:虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁。当它的年龄增加到一定程度(默认为15岁,可通过-XX:MaxTenuringThreshold参数设置),就会被晋升到老年代中。

  • 动态对象年龄判断:为了更好地适应不同程序的内存情况,虚拟机不是永远要求对象的年龄必须达到阈值才能晋升老年代。如果在Survivor区中的相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

  • 空间分配担保:如果发生Minor GC时,老年代的连续空间大于新生代对象总大小或历次晋升的平均大小,就会进行Minor GC;否则,将进行Full GC。

(4)异常

堆内存区域存在OutOfMemoryError异常的可能性。如果不断产生新对象且引用均有效(如放入list中),导致无法回收,就会触发OutOfMemoryError异常。

2. 方法区(Metaspace)

(1)特点

在Java 17中,方法区替代了之前的永久代(PermGen)。方法区用于存储已加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。方法区在JVM启动时创建,所有线程共享。方法区使用本地内存而不是JVM内存,这避免了永久代内存溢出的问题。

(2)内存分配

如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。方法区的大小可以通过-XX:MaxMetaspaceSize参数来指定,如果不指定,则默认没有上限,只受本地内存限制。

(3)内存管理

方法区中的类信息、常量、静态变量等数据在程序运行过程中会被动态加载和卸载。垃圾回收器会对方法区中的数据进行垃圾回收,回收那些不再被使用的类信息、常量等。

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

(1)特点

虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

(2)栈帧

栈帧是方法运行的基本结构,包括局部变量表、操作数栈、动态链接、方法出口等。

  • 局部变量表:用于存放编译器可知的各种Java虚拟机数据类型,包括基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,64位的long和double类型的数据会占用两个槽,其余的数据类型都只占用一个槽。如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。

  • 操作数栈:是一个后入先出(Last In First Out, LIFO)栈,用于存储方法执行过程中产生的中间结果。操作数栈的最大深度在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。

  • 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。

  • 方法出口:当一个方法开始执行后,只有两种方式退出这个方法:正常调用完成(即执行到方法返回语句)和异常退出。无论哪种方式退出,都需要通过方法出口返回到调用者的栈帧。

(3)异常

  • StackOverflowError异常:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出此异常。这通常发生在递归方法中,当递归调用层次过深时,会导致栈溢出。

  • OutOfMemoryError异常:如果虚拟机栈可以动态扩展,但在扩展时无法申请到足够的内存,就会抛出此异常。

4. 本地方法栈(Native Method Stacks)

(1)特点

本地方法栈与虚拟机栈基本类似,但它是为执行本地方法(Native Method)服务的。本地方法是指在编程语言中调用由底层操作系统或其他外部库提供的函数。本地方法的实现通常使用其他编程语言(如C、C++)编写,而不是使用当前编程语言(如Java)。当程序调用本地方法时,当前线程的执行状态需要从Java虚拟机(JVM)切换到底层操作系统或外部库中,通过本地方法栈来保存所需的执行环境和数据。

(2)内存管理

本地方法栈的大小是在JVM启动时预先确定的,并且可以根据需要进行调整。它通常比Java虚拟机栈的大小要大,因为本地方法的执行可能需要更多的内存空间。本地方法栈的内存管理由操作系统负责,当本地方法执行完毕后,其占用的内存空间会被释放。

5. 程序计数器(Program Counter Register)

(1)特点

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。
(2)功能与作用

  • 线程切换与恢复:程序计数器是线程私有的,每个线程都有自己独立的程序计数器。当线程切换时,程序计数器会保存当前线程的执行位置,以便在下次切换回该线程时,能够从正确的位置继续执行。这是多线程环境中保证线程安全执行的重要机制。

  • 异常处理:在Java虚拟机中,异常处理是通过异常表来实现的。当程序执行过程中遇到异常时,程序计数器会查找异常表,确定异常的类型和处理方式,然后跳转到相应的异常处理代码块执行。

  • 指令执行与跳转:程序计数器负责跟踪当前线程执行的字节码指令。它记录了当前线程正在执行的字节码的行号,以及下一条要执行的字节码指令的地址。在字节码解释器执行指令时,程序计数器会根据指令的类型和操作数,计算出下一条指令的地址,并跳转到该地址继续执行。

(3)内存分配与垃圾回收

程序计数器是一块较小的内存空间,通常不需要进行垃圾回收。因为它只存储了当前线程执行的字节码指令的地址和行号,这些信息在线程执行过程中是不断变化的,而且占用的内存空间非常小。因此,程序计数器不需要像堆、方法区等区域那样进行复杂的内存管理和垃圾回收。

(4)异常

虽然程序计数器本身不会抛出异常,但它与线程的执行密切相关。如果线程在执行过程中遇到无法恢复的错误或异常,如栈溢出、内存溢出等,可能会导致线程终止执行。在这种情况下,程序计数器也会失去其意义,因为已经没有需要执行的字节码指令了。

三、内存分布的优化与调整

在Java 17中,虚拟机的内存分布和垃圾回收机制都经过了优化和调整,以提高应用程序的性能和稳定性。以下是一些常见的优化和调整策略:

1. 堆内存大小的调整

根据应用程序的需求和服务器的内存容量,合理设置堆内存的大小。可以通过-Xms和-Xmx参数来指定堆内存的初始大小和最大大小,以避免堆内存的频繁扩展和收缩。

2. 垃圾回收器的选择

Java 17提供了多种垃圾回收器,如G1、ZGC、Shenandoah等。根据应用程序的特点和性能要求,选择合适的垃圾回收器可以提高垃圾回收的效率和减少停顿时间。

3. 方法区大小的调整

方法区用于存储已加载的类信息、常量、静态变量等数据。可以通过-XX:MaxMetaspaceSize参数来指定方法区的最大大小,以避免方法区内存溢出。

4. 虚拟机栈和本地方法栈的调整

虚拟机栈和本地方法栈的大小可以通过-Xss参数来指定。根据应用程序的递归深度和本地方法的调用情况,合理设置栈的大小可以避免栈溢出和性能问题。

5. 程序计数器的优化

程序计数器是线程私有的,不需要进行特别的调整和优化。但是,在编写Java代码时,应尽量避免出现无法恢复的错误或异常,以保证线程的正常执行和程序计数器的正确性。

四、总结

Java 17虚拟机的内存分布包括堆、方法区、虚拟机栈、本地方法栈和程序计数器等多个区域。这些区域各自承担着不同的功能和任务,共同协作支持Java应用程序的高效执行。了解并掌握这些区域的内存分配、垃圾回收和优化调整策略,对于提高Java应用程序的性能和稳定性具有重要意义。在实际开发中,应根据应用程序的特点和需求,合理设置和调整虚拟机的内存参数和垃圾回收器选项,以达到最佳的性能和稳定性效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拾光编程

您的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值