再战JVM (5) 运行时数据区-堆

26 篇文章 0 订阅

一. 堆?

  • 堆是运行时数据区域中最大的一块,也是垃圾收集器的重点对象

  • “几乎” 所有类的实例和数组的内存都在这里分配

  • 堆随着虚拟机启动而创建,并且空间大小在启动时就确定了

  • 堆在物理内存中不一定是连续的,但他在逻辑上被视为连续的

  • 堆是所有线程共享的,但是他可以划分线程私有的缓冲区(Thread Local Allocation Buffer TLAB

  • 内存不足时会抛出OutOfMemoryError

堆空间细分为(jdk1.8):

  • 老年代
  • 新生代
  • 元空间

在这里插入图片描述

Java默认的初始化堆区空间大小是物理内存的1/64,默认最大堆空间是物理内存的1/4
可以使用参数调整默认堆空间大小:

设置初始化堆区空间为1024m
–Xms1024m
设置最大堆空间为1024m
–Xmx1024m

二. 堆的分代收集

1. 分代收集理论

分代收集名为理论,实质是一套垃圾收集的经验法则,它建立在两个分代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储

如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用

为了减少对存活时间长的对象的gc频率,垃圾收集器对堆空间进行的空间分代收集分成了新生代和老年代默认情况下新生代占堆空间的1/3,剩下的老年代占2/3,老年代一般用来存放存活时间长的对象和内存占用大的对象,通过以下jvm参数可以调整新生代和老年代的堆空间占比

堆空间分成5份 设置老年代占4 新生代占1
-XX:NewRatio=4
堆空间分成7份 设置老年代占6 新生代占1
-XX:NewRatio=6
设置新生代固定内存大小(优先级比上面的高)
-Xmx256m

垃圾收集的名词:

  • 新生代收集(Minor GC/Young GC):对新生代的垃圾收集
  • 老年代收集(Major GC/Old GC):对老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为,其他收集器只有Full GC 才会收集老年代
  • 混合收集(Mixed GC):对整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

新生代又在可以划分为 Eden区,Survivor0区和Survivor1Survivor0区和Survivor1区它们两块区域的大小相等,默认的大小比例是 8:1:1

2. 为什么要有Survivor区?

先不去想为什么有两个Survivor区,第一个问题是,设置Survivor区的意义在哪里?

Survivor的存在意义,就是 减少被送到老年代的对象,进而减少老年代垃圾回收的发生,Survivor的预筛选保证,只有经历16次新生代垃圾回收还能在新生代中存活的对象,才会被送到老年代

如果没有Survivor区,Eden区每进行一次垃圾收集,存活的对象就会被送到老年代,那么老年代会很快被填满,虚拟机会频繁的进行老年代的垃圾回收,老年代的内存空间远大于新生代,进行一次垃圾回收消耗的时间比新生代垃圾回收的时间长得多

3. 为什么要设置两个Survivor区

问题来了,一个Survivor区行不行?
设置两个Survivor区就是解决了内存碎片化

因为当新生代和 Survivor0区执行垃圾回收之后,此时Survivor0区也有一些存活对象,此时Survivor0区的对象存储也是零散的,如果硬要把新生代的存活对象放到Survivor0区的话,肯定有一部分对象所占有的内存是不连续的,也就导致了内存碎片化,非常的损耗Java程序的性能

如果两个的话,第一次垃圾回收之后,将新生代存活的对象复制到Survivor0区,第二次,将新生代和Survivor0区存活的对象复制到Survivor1区,第三次,将新生代和Survivor0区存活的对象复制到Survivor0区。

这样循环下来就能保证有个空闲的Survivor区来存放存活的对象,但是这样新生代的内存可用率只有90%,问题又来了?10%的Survivor区不够存放存活对象怎么办?

也不用担心,这里涉及一个机制叫做担保机制,当Survivor区无法存放再多存活对象时,那么放不开的存活对象将提前进入老年代

4. Survivor为什么不分更多块呢?

为什么不将Survivor分成三个、四个、五个?
如果Survivor区再细分下去,每一块的空间就会比较小,很容易导致Survivor区满,内存利用率会变低,并且变得很难管理

三. 对象的内存分配

对象的内存分配主要是发生在新生代的Eden区中,少数情况下是在老年代中(比如大对象,直接进入老年代)

在这里插入图片描述

1. 新生代的对象如何进入老年代

如果对象在 Eden区 出生并经过第一次垃圾回收 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为1。对象在 Survivor 空间中每“熬过”一次 垃圾回收,年龄就增加 1 岁,当它的年龄到达一定程度(默认最大为 15 岁),就将会被晋升到老年代。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置

注意: 如果年龄阈值过大,这样对象老化的机制就失效了,空间分代就没有意义了。如果过小,过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能。

2. 大对象直接进入老年代

jvm有个-XX:PretenureSizeThreshold参数可以设置大对象的大小,如果对象超过设定的参数大小,对象直接进入老年代

3. 空间分配担保

新生代在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象之和(或者历次晋升老年代对象的平均大小)。如果这个条件不成立,那么虚拟机将直接进行 Full GC 动作;

如果这个条件成立,那么虚拟机就会进行一次 Minor GC 操作,但是这次 Minor GC 是有风险的,因为比较的值是平均值,可能出现极端的情况 —— 大量对象在 Minor GC 后还存活,这时就只好在失败后重新发起一次 Full GC

4. TLAB 分配

TLAB,全称Thread Local Allocation Buffer, 即:线程本地分配缓存

TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是每个线程专属的。在虚拟机的TLAB功能启动的情况下(默认是开启的),在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率,TLAB的空间大小默认是eden区的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小

注意:TLAB是堆内存的一部分,他在读取上是线程共享的,但是在内存分分配上,是线程私有的

在这里插入图片描述

为什么需要TLAB?

这是为了加速对象的分配。由于对象一般分配在堆上,而堆是线程共用的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降。考虑到对象分配几乎是Java中最常用的操作,因此JVM使用了TLAB这样的线程专有区域来避免多线程冲突,提高对象分配的效率,大但是大对象无法进行TLAB分配,只能直接分配到堆上

如果有一个100KB的TLAB区域,已经使用了80KB,当需要分配一个30KB的对象时,TLAB是如何分配的呢?

此时,虚拟机有两种选择:第一,废弃当前的TLAB(会浪费20KB的空3.4 间);第二,将这个30KB的对象直接分配到堆上,保留当前TLAB(当有小于20KB的对象请求TLAB分配时可以直接使用该TLAB区域)。

JVM选择的策略是:在虚拟机内部维护一个叫refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,反之,则会废弃当前TLAB,新建TLAB来分配新对象

默认情况下,TLAB和refill_waste都是会在运行时不断调整的,使系统的运行状态达到最优

常用参数:

参数作用备注
-XX:+UseTLAB启用TLAB默认启用
-XX:TLABRefillWasteFraction设置允许空间浪费的比例默认值:64,即:使用1/64的TLAB空间大小作为refill_waste值
-XX:-ResizeTLAB禁止系统自动调整TLAB大小 
-XX:TLABSize指定TLAB大小单位:B

5. 栈上分配

Java的对象一般都是分配在堆内存中的,而JVM开启了栈上分配后,允许把线程私有的对象(其它线程访问不到的对象)打散分配在栈上。这些分配在栈上的对象在方法调用结束后即自行销毁,不需要JVM触发垃圾回收器来回收,因此提升了JVM的性能。

把对象打散是什么意思?
在这里插入图片描述
比如方法中的user引用,就是方法的局部变量,new的User()实例在堆上,对象打散的意思就是:假设实例user中有两个字段,就把这个实例认作它内部的两个字段以局部变量的形式分配在栈上也就是打散,这个操作称为:标量替换

什么情况下会在栈上分配对象

jvm通过逃逸分析(判断对象的作用域是否超出函数体),来决定对象要不要在栈上分配,如果我们将这个user对象return出去,这时候我们对于这个对象来说就不会受用栈上分配,因为后续的代码可能还需要使用这个对象实例,可以说只要是多个线程共享的对象就是逃逸对象,那么这个对象不会在栈上分配

关于逃逸分析目前还是处于不稳定的阶段,因为无法保证逃逸分析的性能消耗一定高于其节省的性能。简单来说就是可能执行了逃逸分析,结果发现都是逃逸出方法的对象,这样逃逸分析并没有提高性能,同时执行逃逸分析也消耗了一定的性能,造成得不偿失。所以,逃逸分析在 JVM 中没有实现 栈上分配的功能的,但是其还是在 JIT 中起到了优化作用。所以可以说对象都是创建在堆上的。而我们一般所说的对象创建在栈上,实际情况是因为标量替换的作用

什么是同步消除?

同步消除是java虚拟机提供的一种优化技术。通过逃逸分析,可以确定一个对象是否会被其他线程进行访问
如果对象没有出现线程逃逸,那该对象的读写就不会存在资源的竞争,不存在资源的竞争,则可以消除对该对象的同步锁

常用参数

启用逃逸分析(默认打开)
-XX:+DoEscapeAnalysis
标量替换(默认打开)
-XX:+EliminateAllocations
打印标量替换情况
-XX:+PrintEliminateAllocations
开启同步消除
+XX:+EliminateLocks

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值