JVM学习总结-内存区域、内存分配和垃圾收集器

本文详细解析了Java虚拟机(JVM)的内存区域划分,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、运行时常量池、直接内存等,阐述了对象的创建、内存布局及访问定位方式,介绍了垃圾收集算法和收集器,以及对象存活判断算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

jvm内存区域:
在这里插入图片描述
程序计数器:可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。各条线程间计数器互不影响,独立存储,是“线程私有内存”。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。此区域是唯一一个没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈:线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈扩展时无法申l请到足够的内存,抛出StackOverflowError异常。

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

Java堆:Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,唯一目的就是存放对象实例。如果在堆中没有内存完成实力分配,并且也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区:与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池:是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号应用,这部分内容将在类加载后进入方法区的运行时常量中存放。当常量池申请到内存时,将抛出OutOfMemoryError异常。

直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中新加入的NIO类,引入了一种基于通道与缓冲区的I\O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

对象的创建:

  1. 虚拟机遇到一条new指令时,首先将区检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。、
  2. 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
  3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。
  4. 接下来,虚拟机要对对象进行必要的设置,例如这个对象时哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。
  5. 在上面的工作完成后,从虚拟机的视角看,一个新对象就产生了,但从Java程序的视角看,会接着执行方法进行初始化,这样一个真正的对象才算完全产生出来。

对象的内存布局:

  • 对象头:包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称为“Mark Word"。另外一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例数据:是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
  • 对齐填充:这部分不是必然存在的,仅仅起着占位符的作用。当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位:

主流的访问方式有两种:使用句柄和直接指针

Java程序需要通过栈上的reference数据来操作堆上的具体对象。

  • 使用句柄:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

在这里插入图片描述

  • 使用直接指针:那么Java堆对象的布局中就必须考虑如何放置访问累心数据的相关信息,而reference中存储的直接就是对象地址。

    在这里插入图片描述

判定对象是否存活的算法:

引用计数算法:

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

可达性分析算法:在下面介绍。

垃圾收集算法:

标记-清除算法

分为两个阶段:标记和清除,首先标记出所有的需要回收的对象,在标记完成后统一回收所有被标记的对象。

在这里插入图片描述

复制算法:

将可用内存按容量划分为大小相等的两块,每次只是用其中一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收。
在这里插入图片描述

标记-整理算法:

标记过程和之前一样,但后续步骤是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

在这里插入图片描述

分代收集算法:

只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

垃圾收集器:

Serial收集器:是一个单线程的收集器,但它的单线程的意义不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集工作时,必须暂停其他所有的工作线程。

在这里插入图片描述

ParNew收集器:就是Serial的多线程版本,许多运行在Server模式下的虚拟机中首选的新生代收集器,目前只有它能与CMS配合。

在这里插入图片描述

Parallel Scavenge收集器:是一个新生代收集器,使用复制算法,并发的多线程收集器,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式叫做GC自适应的调节策略,这也是与ParNew一个重要区别。

Serial Old收集器:Serial收集器的老年代版本,是一个单线程收集器,使用标记-整理算法。

在这里插入图片描述

Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

在这里插入图片描述

CMS收集器:是一种获取最短回收停顿时间为目标的收集器。基于标记-清除算法。

初始标记:标记一下GC Roots能直接关联到的对象。

并发标记:进行GC Roots Tracing的过程

重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录

并发清除

缺点:CMS收集器对CPU资源非常敏感,无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生,采用标记-清除算法收集结束会有大量空间碎片产生。

在这里插入图片描述

G1垃圾收集器:

G1垃圾收集器

可达性分析算法:

用来判定对象是否存活

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,即从GC Roots到这个对象不可达时,证明此对象是不可用的。

GC Roots的对象包括如下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

在这里插入图片描述

  • 堆空间被分割成一些相同大小的堆区域(Region),虽然还保留新生代和老年代的概念,但新生代和年老代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合。特定的区域集合像旧的收集器一样被指派为相同的角色(伊甸:eden、幸存:survivor、年老:old),这在内存使用上提供了更强大的灵活性。
  • 特点:并行与并发,分代收集,空间整合,可预测的停顿

G1收集器的运作大致可划分为以下几个步骤:

初始标记:初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

并发标记:并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

最终标记:最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Longs 里面,最终标记阶段需要把Remembered Set Longs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

筛选回收:最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是用Remembered Set来避免全堆扫描的。G1中的每个Region都与一个与之对应的Remembered Set。

在这里插入图片描述
内存分配与回收策略

  • 对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC.

新生代GC:指发生在新生代的垃圾收集动作,因为对象都具备朝生夕灭的特性,所以Minor GC 非常频繁,一般回收速度也快。

老年代GC(Major GC/Full GC)指发生在老年代的GC.

  • 大对象直接进入老年代,所谓大对象是指需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串以及数组。

  • 长期存活的对象进入老年代

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值