0
点赞
收藏
分享

微信扫一扫

【JVM】运行时数据区之 堆——自问自答

上善若水山西太原 2023-09-26 阅读 39
jvm

Q:堆和栈,在设计上有何用义?

此处我们不说数据结构的概念。

堆本身是一种存储结构,在代码的内存层面来看,无论是c++ 操作的原生内存,还是Java 背后的JVM,堆的作用都是进行持久存储的。

这个持久存储并不是像数据库那样,而是指在程序运行的生命周期里,在一个进程映像中。堆负责存储一些程序运行时生命周期较长的变量、对象等。

这些变量、对象 把他们的地址空间暴露给我们,方便操作,同时,也需要在不用的时候进行堆内存的释放。比如C++的new 和delete, java的对象创建与垃圾回收。

而栈更像是一个临时空间,当我们需要执行具体的操作,执行一些方法或函数时,栈可以临时存储一些变量,指令。当我们执行结束的时候,这些临时变量就会随着栈顶元素的弹出而销毁。(这就是在C/C++中为什么不允许返回局部变量的地址或引用)。

所以,栈是不需要进行回收操作的。

Q: JVM的堆的概述

一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域Java 堆区在JVM 启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。

堆整体上分为 年轻代+老年代


《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区 (ThreadLocal Allocation Buffer, TLAB)。

《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated我要说的是:“几乎”所有的对象实例都在这里分配内存。一从实际使用角度看的。


数组和对象可能永远不会存储在栈上,因为栈中保存引用,这个引用指向对象或者数组在堆中的位置。


在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。


堆,是GC ( Garbage Collection,垃圾收集器) 执行垃圾回收的重点区域。

 现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

简而言之:

元空间是jdk8之后的落地实现。

逻辑上来说,元空间也算是堆,但实际管理上,方法区有自己的管理方式

Q: 年轻代与老年代

存储在JVM中的Java对象可以被划分为两类:


Java堆区进一步细分的话,可以划分为年轻代 (YoungGen)和老年代(oldGen)其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区).

默认比例:

配置新生代与老年代在堆结构的占比。
        默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3

        可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
       

 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
当然开发人员可以通过选项“-xx:SurvivorRatio”调整这个空间比例。比
如-xX:SurvivorRatio=8

几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。

IBM公司的专门研究表明,新生代中 80的对象都是“朝生死”的可以使用选项”-xmn”设置新生代最大内存大小
这个参数一般使用默认值就可以了。

通过jstat -gc PID 来看看每个区域的内存使用情况

xxC 表示总量 ,xxU 表示已使用

我们注意到以下问题,幸存区和Eden区比例并不是 1:1: 8,而是1:6,其实这个还是要手动去设置的,(你jinfo查出来的参数骗了你)在官网也可以看到具体描述

另一个参数:

UseAdaptiveSizePolicy,即使用自适应策略。 (这个参数跟垃圾回收设置有关,在后面讲)

当我们不想使用自适应,要求按照我们自己设置的比例时,请带上这个参数:

-XX:-UseAdaptiveSizePolicy

参数表达中 :-,表示关闭某一参数,:+ 表示开启某一参数

Q:对象分配过程

总结:
针对幸存者s0,s1区的总结: 复制之后有交换,谁空谁是to.
关于垃圾回收: 频繁在新生区收集,很少在养老区收集,几乎不在永久区元空间收集。

Q:内存分配策略

针对不同年龄段的对象分配原则如下所示:

关于空间分配担保:

Q:TLAB

1、为什么有TLAB ( Thread Local Allocation Buffer ) ?


堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。


2、什么是TLAB?


从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略
据我所知所有openJDK衍生出来的JVM都提供了TLAB的设计。

Q:堆空间相关参数设置:

思考两个问题:

  • 如果参数-XX: SurviorRatio 参数设置的特别大会怎样?

    此参数表示新生代中,Eden区和Survior区的比例,一般默认设为8,表示

    Eden:s1:s0 =8:1 :1

    若设置的过大:

这种情况下,当触发YGC时,说明Eden区已经无法容纳新对象,此时,新生代的垃圾被回收,Eden区的一部分对象进入to区,同样幸存区的from的对象也进入to。

但是由于Eden很大,S区很小,可能出现很多对象无法进入S区,只能直接被晋升进养老代。

结论:Eden设置的比例过大,会让YGC/Minor GC失去意义,GC分代的思想也不能很好的体现。(对象没到阈值就直接晋升)

  • 如果Eden区设置的过小呢?

这种情况下,由于Eden区很小,很快就会满,一旦满了,就要触发YGC /Minor GC, GC线程与用户线程一般来说是串行执行,会造成STW,这样程序执行效率就很低

Q:堆优化之 逃逸分析,标量替换,同步省略

使用逃逸分析,编译器可以对代码做如下优化:

1、逃逸分析

逃逸分析示例代码

-XX:+DoEscapeAnalysis 开启逃逸分析

2、同步省略(锁消除)

线程同步的代价是相当高的,同步的后果是降低并发性和性能。


在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除

3、标量替换

举报

相关推荐

0 条评论