JVM内存结构--堆

JVM内存结构

JVM内存结构指的是JVM运行时数据区结构,它主要包含以下几个部分:

  • 堆(Heap):线程共享。

  • 方法区(Method Area):线程共享。

  • 虚拟机栈(VM Stack):线程私有。

  • 程序计数器(Program Counter Register):线程私有。

  • 本地方法栈(Native Method Stack):线程私有。

堆(Heap)

JVM堆(Heap)是Java虚拟机中的一块内存区域(所有线程共享),主要用于存储对象实例和数组。堆被划分为三个部分:年轻代、老年代和永久代(在JDK8中取消了永久代),其中,年轻代又被划分为Eden区Survivor区(含:S0和S1)。

默认情况下,年轻代和老年代的比例为1:2,即:年轻代占整个堆空间的1/3,老年代占整个堆空间的2/3。Eden区与Survivor区的比例为8:1:1。

字符串常量池

字符串常量池是Java中的一个特殊的存储区域,用于存储字符串常量。在Java中,字符串常量是不可变的,因此可以被共享。这样可以减少内存的使用,提高程序的性能。在JDK8中,字符串常量池存储在中。(String s="字符串"  先去字符串常量池中查找有无“字符串”对象,如果有的话,直接把该对象的地址赋值给s,如果没有的话,会去字符串常量池中创建对象:“字符串”,然后把字符串常量池中“字符串”的地址赋值给s。)

线程本地分配缓冲区(TLAB)

堆是所有线程所共享的区域,在多线程的环境下,在堆上创建对象是线程不安全的,故引出TLAB。堆内存Eden区划分出来的一块专用于原始线程进行对象分配的区域

它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。

TLAB是私有的,私有“只是指在”内存分配“上是线程独享的,也就是线程只能在自己的TLAB上给对象分配内存空间,但是仍然可以读取其他线程的TLAB

  • TLAB是如何分配的

tlab剩余可用空间>tlab可浪费空间,当前线程不能丢弃当前TLAB,本次申请交由Eden区分配空间。

tlab剩余可用空间<tlab可浪费空间,在当前允许可浪费空间内,重新申请一个新TLAB空间,原TLAB交给Eden(原TLAB的生命周期停止了)。

  • 创建的时机:

线程初始化时会创建TLAB。

gc时,在对象扫描完成之后,当线程第一次尝试分配对象的时候会创建TLAB。

  • TLAB生命周期
  • 当前 TLAB 不够分配,并且剩余空间小于最大浪费空间限制,那么这个 TLAB 会被退回 Eden,重新申请一个新的

  • 发生 GC 的时候,TLAB 被回收。

GC类型

Minor GC(又称Young GC):主要用于收集年轻代中的非存活对象。Minor GC在Eden区空间不足时触发。

Major GC:主要用于收集老年代中的非存活对象。Major GC在老年代空间不足时触发。注意:Major GC一般都会伴随一次Minor GC,Major GC的速度一般会比Minor GC慢10倍,因此,STW的时间会更长(应尽量避免Minor GC)。目前只有CMS会有单独收集老年代的行为。

Mixed GC(混合收集):主要用于收集年轻代以及部分老年代中的非存活对象。目前只有G1 GC会有这种行为。

Full GC:主要用于收集整个堆(含:年轻代和老年代)中的非存活对象,即:Major GC+Minor GC组合

Full GC触发条件

  • Minor GC后,存活对象的大小超过了老年代的剩余空间。

  • 内存使用率超过设定的阈值:

对象进入老年代触发条件

  • 对象的年龄达到15岁时。默认的情况下,对象经过15次Minor GC后会被转移到老年代中。对象进入老年代的Minor GC次数可以通过JVM参数:-XX:MaxTenuringThreshold进行设置,默认为15次。

  • 动态年龄判断。当一批存活对象的总大小超过Survivor区内存大小的50%时,按照年龄的大小(年龄大的存活对象优先转移)将部分存活对象转移到老年代中。

  • 大对象直接进入老年代 

  • 空间分配担保

    假如在Young GC之后,新生代仍然有大量对象存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。

对象分配过程

新生成的对象在年轻代Eden区中分配内存,当Eden空间已满时,触发Minor GC,将不再被其他对象所引用的对象进行回收,存活下来的对象被转移到Survivor0区。
Survivor0区满后触发Minor GC,将Survivor0区存活下来的对象转移到Survivor1区,同时,清空Survivor0区,保证总有一个Survivor区为空。
经过多次Minor GC后,仍然存活的对象被转移到老年代,进入老年代的Minor GC次数可以通过参数-XX:MaxTenuringThreshold=<N>进行设置,默认为15次。
当老年代已满时会触发Major GC(即:Full GC,因此执行Major GC时会先执行Minor GC)。

为什么Survivor设置2个,而不是1个或者0个?

设置两个Survivor原因:解决内存碎片化,即:保证分配对象(如:大对象)时有足够的连续内存空间。

如果 Survivor 是 0 的话,也就是说新生代只有一个 Eden 分区,每次垃圾回收之后,存活的对象都会进入老生代,这样老生代的内存空间很快就被占满了,从而触发最耗时的 Full GC ,显然这样的收集器的效率是我们完全不能接受的。

分代收集的原因

将对象按照存活概率进行分类,主要是为了减少扫描范围和执行GC的频率,同时,对不同区域采用不同的回收算法,提高回收效率。

GC Roots的对象

  • ①虚拟机栈中引用的对象
  • ②元数据空间中类静态属性引用的对象
  • ③元空间运行时常量池中常量引用的对象
  • ④本地方法栈中JNI(native方法)中引用的对象

finalization机制

在Java中,当一个对象不再被引用时,垃圾收集器会将其标记为可回收对象,并最终回收内存。但在某些情况下,我们可能希望在对象被回收之前执行一些额外的清理操作,比如关闭文件、释放资源等。这就是Finalization机制的作用。 当一个对象被标记为可回收时,JVM会首先检查该对象是否有一个​​finalize()​​方法。如果有,JVM将会将该对象添加到一个待Finalize的队列中。随后,一个专门的Finalizer线程会在适当的时机调用这些待Finalize对象的​​finalize()​​方法。

Finalization机制的注意事项

尽管Finalization机制提供了一种在对象被回收前执行清理操作的机制,但它也存在一些注意事项:

  • Finalization机制的执行时间是不确定的,无法保证在对象不再被引用时立即执行。因此,不能依赖于它来释放关键资源,比如数据库连接等。最好使用显式的资源释放方法(例如​​close()​​)来确保资源的及时释放。
  • 对象的​​finalize()​​方法只会被调用一次。如果对象在被回收前重新被引用,​​finalize()​​方法将不再被调用。因此,不要在​​finalize()​​方法中做与对象状态相关的操作。
  • Finalizer线程的调度时间是不确定的,可能会导致对象的回收延迟。这可能会对系统的性能和资源使用产生负面影响。因此,在大多数情况下,建议使用显式的资源释放方法

finalize方法

即使通过可达性分析算法判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与 GCRoots 的引用链,它将被第一次标记。随后进行一次筛选(如果对象重写了finalize方法),我们可以在 finalize 中去拯救

垃圾回收算法

  1. 标记-清除算法

见名知义,标记-清除(Mark-Sweep)算法分为两个阶段:

  • 标记 : 标记出所有需要回收的对象
  • 清除:回收所有被标记的对象

缺点:

执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。

内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

     2.复制算法

将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,或经过一定时间,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。

优点:

相对于标记–清理算法解决了内存的碎片化问题。
效率更高(清理内存时,记住首尾地址,一次性抹掉)

缺点:

内存利用率不高,每次只能使用一半内存,浪费空间。

    3.标记整理算法

因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。

  • 标记:对需要回收的进行标记
  • 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。

优点:不会产生碎片

缺点:每次标记再前移效率偏低

      4.分代回收算法

新生代:一般使用复制算法,因为在新生代中的对象几乎绝大部分都是朝生夕死的,每次GC发生后只会有少量对象存活,这种情况下采用复制算法无疑是个不错的选择,付出一定的内存空间开销以及少量存活对象的移动开销,换取内存的整齐度以及可观收集效率,这很明显是个“划得来的买卖”。

年老代:一般采用标-整算法或标-清算法,但绝大多数年老代GC器都会选择采用标-整算法,因为毕竟标-清算法会导致大量的内存碎片产生,在年老代对象分配时,内存不完整可能会导致大对象分配不下而持续触发GC。而标-整算法虽然效率较低,但胜在GC后内存足够整齐,再加上年老代的GC并没有新生代频繁,所以年老代空间采用标-整算法无疑也是个不错的选择。

为什么年老代不考虑复制算法呢?

一方面是因为年老代空间中的对象普遍存活率都比较高,第二方面是没有新的空间为年老代做分配担保,所以复制算法是明显并不适合年老代的

GC为什么需要STW

一个是尽量为了避免浮动垃圾产生,第二个则是为了确保一致性。

如果GC不停下用户线程,意味着可能刚标记完一块区域的对象,下一时刻可能用户线程就产生“垃圾”,带来的后果则是:会给GC线程造成很大的负担,GC算法的实现难度也会增加,因为GC机制很难去准确判断哪些是垃圾

GC发生时,可达性分析算法的工作必须要在一个能够确保一致性的内存快照中进行。也就是指:在整个分析期间,JVM看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性无法得到保证。这也是GC进行时,必须停止所有用户线程的其中一个重要原因。

安全点与安全区域

当GC发生时,必然会出现程序停顿,也就是需要停止所有用户线程。但问题在于:用户线程停止的时机必须合理,不然在恢复线程后,有可能会导致最终的执行结果出现不一致,因此用户线程必然需要在一个安全的位置暂停,故引出安全点和安全区域。

JVM中对于安全点的定义主要有如下几种:

  • ①循环结束的末尾段
  • ②方法调用之后
  • ③抛出异常的位置
  • ④方法返回之前

当JVM需要发生GC、偏向锁撤销等操作时,如何才能让所有线程到达安全点阻塞或停止?

  • ①主动式中断(JVM采用的方式):不中断线程,而是设置一个标志,而后让每条线程执行时主动轮询这个标志,当一个线程到达安全点后,发现中断标志为true时就自己中断挂起。
  • ②抢断式中断:先中断所有线程,如果发现线程未执行到安全点则恢复线程让其运行到安全点位置。

当Java程序需要停下所有用户线程时,某些线程可能处于中断或者休眠状态,从而无法响应JVM的中断请求走到安全点位置挂起了,所以出现了安全区域的概念。

程序计数器

程序计数器(Program Counter Register)也被称为PC寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成。

Java虚拟机栈

  Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。Java虚拟机栈描述的是Java方法执行的线程内存模型:方法执行时,JVM会同步创建一个栈帧,用来存储局部变量表、操作数栈、动态连接等。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

Java 虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展

方法区

在JDK8及之前,方法区属于永久代,而在JDK8之后,永久代被移除,方法区被移到了本地内存中,即:元空间(Meta Space)。

元空间逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫非堆。元空间是各个线程共享的内存区域,它主要存储两部分内容:

  • 类信息。

类信息指的是加载到方法区中的Class文件信息,Class文件信息中除了有类的版本、字段、方法、接口等描述信息外, 还包含一项常量池表(又称静态常量池)信息,常量池表主要用于存放编译器生成的各种静态常量(又称字面常量或字面量)和符号引用。其中:

  • 运行时常量池。

运行时常量池主要用于程序运行时为常量池表中的静态变量、符号引用等静态信息分配内存地址。

  • 为什么需要方法区

一个 Java 类型的元数据信息都需要在虚拟机运行时动态的生成、存储,并被追踪和使用。这个元数据信息包括:类的全名、父类、实现的接口等信息、类的字段、方法信息、常量池、方法的字节码等。

而且,在运行时的栈、堆的生命周期基本上都是与线程绑定的,而方法区则是被多个线程共享的,所以能够更好地实现性能优化、内存回收等方面的权衡。

在 Java 中,每一个类都需要被加载到虚拟机中才能被使用。而 Java 是一门支持反射机制的语言,
反射机制需要在运行时根据类的元数据来动态地创建类的实例、访问属性、调用方法等操作。
因此,Java 需要一块专门的内存区域来存放类的元信息,这个内存区域就是方法区。

对象一定分配到堆上么

不是所有的对象和数组,都是在堆上分配的。由于即时编译的存在,如果JVM发现某些对象没有逃逸,就很有可能被优化到栈上分配。

逃逸分析具体情况:

当对象被赋给了成员变量或静态变量时,能被外部所访问,此变量就发生了逃逸。

当对象被return返回时,此时对象能被外部所访问,那么这个对象就发生了逃逸。

逃逸分析的三个优点:对象可能分配到栈上,分离对象(标量替换),消除同步锁。

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值