深入理解JVM笔记-运行时数据区域

运行时数据区域        

程序计数器

 一块较小的内存空间,可以看作当前线程做执行的字节码的行号指示器。

为例线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的计数器程序,各个线程之间的计数器互不影响,独立存储。是线程私有的内存。

如果线程执行的是个java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是本地方法,则计数器值为空

这个内存区域是唯一一个没有任何OutOfMemoryError情况的区域

Java虚拟机栈

Java虚拟机栈由一系列栈帧(Stack Frame)组成,线程私有,每个栈帧对应一个正在执行的方法。栈帧包含了局部变量表、操作数栈、动态链接、方法返回地址以及一些额外的辅助数据。当一个新方法被调用时,Java虚拟机会为该方法创建一个新的栈帧,并将其压入虚拟机栈顶。当方法执行完毕时,相应的栈帧会被弹出,恢复到调用该方法的栈帧上,程序继续执行。

局部变量表是一个类似数组的数据结构,其中的每个元素都存储一个局部变量的值。局部变量可以是各种数据类型,包括基本数据类型(boolean、char、byte、short、int、long、double、float)、引用数据类型(如对象引用)、returnAddress类型。局部变量表中的变量在方法执行期间被使用,直到方法执行结束,局部变量表也随之销毁。

这些数据类型在局部变量表中以局部变量槽(slot)表示,其中64位的long和double类型会占用两个槽,局部变量表内存空间分配在编译期间完成,运行期间不会改变局部变量表的大小

如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常(栈深度溢出),如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常(栈扩展失败)。

本地方法栈

本地方法栈(Native Method Stack)是Java虚拟机(JVM)中的一部分,线程私有,用于支持调用本地(Native)方法。本地方法是指用其他语言(如C、C++等)编写的方法,通过JNI(Java Native Interface)机制与Java程序进行交互。

本地方法栈与Java虚拟机栈的区别在于,本地方法栈中的栈帧包含的是本地方法的信息,而不是Java方法的信息。本地方法栈的栈帧包含了本地方法的参数和局部变量,以及用于调用本地方法的一些辅助信息。

如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常(栈深度溢出),如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常(栈扩展失败)。

HotSpot虚拟机将虚拟机栈和本地方法栈合二为一。

Java堆

Java堆(Java Heap)是Java虚拟机(JVM)中最大的一块内存区域,用于存储对象实例和数组。Java堆是被所有线程共享的内存区域,在虚拟机启动时创建,用于存储程序运行时动态创建的对象实例。

由于即时编译技术(JIT)发展,逃逸分析技术强大,栈上分配标量替换优化手段,使得对象实例分配都分配在堆上不那么绝对。(逃逸分析是一种编译优化技术,用于分析对象的生命周期及其在方法中的使用情况。如果编译器可以确定一个对象在方法内部不会逃逸到方法外部,即不会被其他线程或方法引用,那么这个对象就可以被分配在栈上而不是堆上,从而避免了在堆上进行动态分配和垃圾回收。标量替换是一种优化技术,用于将对象实例的字段(如int、float等基本数据类型)拆分成独立的局部变量,从而使得对象实例的字段可以被分配在栈上而不是堆上。栈上分配,一些对象实例,特别是一些临时对象或者对象生命周期较短的情况下,可能会被直接分配在栈上而不是堆上。)

Java堆是垃圾收集器管理的内存区域。Java堆中存储的对象实例由Java垃圾收集器进行管理。垃圾收集器会周期性地检查堆中的对象,将不再被引用的对象进行回收,释放其占用的内存空间。

基于分代收集理论,Java堆通常会被划分为年轻代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,JDK8之前)。年轻代用于存储新创建的对象实例,而老年代则用于存储存活时间较长的对象实例。永久代用于存储类信息、常量池等数据,在JDK8之后被元数据区(Metaspace)所取代。如今HotSpot也出现了不采用分代设计的新垃圾收集器。(其中最知名的就是Z Garbage Collector(ZGC)和Shenandoah。这些新型收集器更注重于实现更低的停顿时间和更好的吞吐量,以适应现代大型内存应用程序和云环境的需求。ZGC是一种低延迟、高吞吐量的垃圾收集器,它的目标是将停顿时间控制在10ms以内。ZGC不仅可以处理大型堆内存,还可以处理非常小的堆内存,因此它非常适合云环境中的大型Java应用程序。Shenandoah也是一种低停顿时间的垃圾收集器,它的设计目标是将停顿时间降低到毫秒级别,并在大型堆内存和多核处理器上表现出色。这些新的垃圾收集器不再将堆内存划分为年轻代和老年代,而是更加关注整体的垃圾收集过程,采用不同的算法和技术来实现低延迟和高吞吐量。)

Java堆的内存分配通常采用指针碰撞(Bump the Pointer)或者空闲列表(Free List)等策略。指针碰撞适用于堆内存连续且被所有线程共享的情况,而空闲列表则适用于堆内存非连续或者被多个线程私有的情况。

为了提升对象分配的效率,更好回收内存,所有线程共享的Java堆可以划分出多个线程私有的分配缓冲区TLAB。

Java堆的大小直接影响了程序能够创建的对象实例的数量和大小,如果Java堆的空间不足,就会抛出内存溢出异常(OutOfMemoryError)

方法区

它是Java虚拟机的一部分,用于存储类的结构信息、静态变量、常量以及编译器编译后的代码缓存等数据。方法区是各个线程共享的内存区域。

类的结构信息: 方法区存储了每个类的完整结构信息,包括类的名称、父类的名称、类的修饰符、字段信息、方法信息等。这些信息在类加载时被加载到方法区中,并且在整个生命周期内都保持不变。

静态变量: 方法区存储了类的静态变量,这些变量在类加载时被分配内存空间,并且在整个程序执行期间存在,直到程序结束或类被卸载。

常量池: 方法区还包含了每个类的常量池,其中存储了类中使用的字面量、符号引用和其他常量。常量池是类加载过程中生成的,用于存储类中的常量信息,并且在整个生命周期内都保持不变。

方法字节码: 方法区存储了编译器编译后的方法字节码,即类中每个方法的具体实现代码。这些字节码在类加载时被加载到方法区中,并且在方法调用时被虚拟机执行。

在一些早期的JVM实现中,方法区被称为“永久代”(Permanent Generation,PermGen),但在JDK 8之后,Oracle JDK和OpenJDK移除了永久代,而是采用了元数据区(Metaspace)来替代,用于存储类的结构信息、方法字节码等数据。

如果方法区无法满足新的内存分配需求,就会抛出内存溢出异常(OutOfMemoryError)

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,它是在类加载过程中根据Class文件中的常量池表生成的,用于存储类中使用的符号引用(类接口全限定名、字段方法名和描述符)、字面量(字符串常量、数值常量、布尔常量其他常量(动态生成的常量、注解信息。与类文件中的常量池相比,运行时常量池的内容更加动态,可以包含更多的信息。Java虚拟机还会将类文件中的常量池中的符号引用解析为直接引用,并且将解析后的结果存储到运行时常量池中,以供程序在运行期间使用。

如果运行时常量池无法满足新的内存分配需求,就会抛出内存溢出异常(OutOfMemoryError)

直接内存

直接内存(Direct Memory)是Java中一种特殊的内存分配方式,与Java虚拟机的堆内存和栈内存不同,它是通过操作系统的本地内存(Native Memory)来分配的。直接内存并不受Java堆大小的限制,因此可以用于处理大量的数据或者需要频繁读写的数据,可以提高程序的性能和效率(避免一些GC导致的停顿时间过长的问题)。不属于虚拟机运行时数据区,但也会抛出内存溢出异常(OutOfMemoryError)。

Java中直接内存通常通过java.nio.ByteBuffer(基于通道和缓冲区的IO方式)的allocateDirect()方法来分配。从而避免了在Java堆和本地内存之间的数据复制和拷贝,提高了IO操作的效率。

优点:零拷贝、减少GC影响、提高IO性能

缺点:分配成本高、不受JVM控制(可能内存泄漏)

  • 36
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值