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

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

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

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

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

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

堆:是一种原材料仓库,就像饭店的冰箱。

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

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

栈:是一种临时处理区域,就像厨师的条案。

Q: JVM的堆的概述

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

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

> 堆内存的大小是可以调节的。

-Xms         堆的初始值大小 等价于:-XX:InitialHeapSize

-Xmx      堆的最大值大小  等价于:-XX:MaxHeapSize

比如 

设置堆的初始值为6兆

-Xms6291456
-Xms6144k
-Xms6m

一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutofMemoryError异常


通常会将 -ms 和 -mx两个参数配置相同的值,其目的是为了能够在java垃圾收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。


默认情况下,初始内存大小:物理电脑内存大小 /64最大内存大小:物理电脑内存大小 /4

查看设置的参数:方式一: jps 查看当前进程号,   jstat -gc 进程id


方式=: -XX:+PrintGCDetails


《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对象可以被划分为两类:

 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速

  另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。


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”设置新生代最大内存大小
这个参数一般使用默认值就可以了。

查看具体设置的参数值

jps

jinfo -flag  参数名 进程号

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

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

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

另一个参数:

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

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

-XX:-UseAdaptiveSizePolicy

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

Q:对象分配过程

1、new的对象先放伊甸园区。此区有大小限制。
2、当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对年轻代进行垃圾回收(Minor GC/youngGC),将伊园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊园区
3、然后将伊甸园中的剩余对象移动到幸存者0区。

4、如果再次触发垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区。
5、如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区

6、啥时候能去养老区呢?可以设置次数。默认是15次。
可以设置参数: -XX:MaxTenuringThreshold=<N>进行设置

7.在养老区,相对悠闲。当养老区内存不足时,再次触发GC: Major GC,进行全堆的内存清理。
8.若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常

        java.lang.OutofMemoryError: Java heap space

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

注意:

  1. Eden区满,触发YoungGC/MinorGC,回收整个年轻代。
  2. 老年代满,触发Major GC
  3. 触发Full GC的情况:

  1. 方法区空间不足。
  2. 调用System.gc()(可能触发),
  3. 老年代空间不足,通过Minor GC后进入老年代的平均大小大于老年代的可用内存,
  4. 由Eden区、survivor space (From Space) 区向survivor space1 (Topace)区复制时,对象大小大于To Space可用内存,则把该对象晋升到老年代,且老年代的可用内存小于该对象大小。

GC是会触发STW的 FUllGC时间最长,开发中尽量避免使用FULL GC

Q:内存分配策略

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

  •   优先分配到Eden
  •   大对象直接分配到老年代(尽量避免程序中出现过多的大对象)
  •   长期存活的对象分配到老年代
  •    动态对象年龄判断

              如果survivor 区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold 中要求的年龄。

  • 空间分配担保   

        -XX:HandlePromotionFailure

关于空间分配担保:

Q:TLAB

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


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


2、什么是TLAB?


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

  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选

  • 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。

  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。

  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

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

官网:

java (oracle.com)

-xx:+PrintFlagsInitial 查看所有的参数的默认初始值

-xx:+PrintFlagsFinal 查看所有的参数的最终值 (可能会存在修改不再是初始值)
-Xms 初始堆空间内存(默认为物理内存的1/64)
-xmx 最大堆空间内存(默认为物理内存的1/4)
-Xmn 设置新生代的大小。(初始值及最大值):
-xx:NewRatio配置新生代与老年代在堆结构的占比


-XX:survivorRatio  设置新生代中Eden和so/s1空间的比例
-XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的GC处理日志
打印gc简要信息:  1、-XX:+PrintGC    2、 -verbose:gc

-XX:HandlepromotionFailure: 是否设置空间分配担保

 

思考两个问题:

  • 如果参数-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、栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。


2、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。


3、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分 (或全部) 可以不存储在内存,而是存储在CPU寄存器中。

JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
常见的栈上分配的场景
在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值实例引用传递。

1、逃逸分析

逃逸分析示例代码

-XX:+DoEscapeAnalysis 开启逃逸分析

2、同步省略(锁消除)

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


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

3、标量替换

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值