JVM-运行时数据区-堆

堆空间包含:新生区 老年区 永久区(元空间)

其中新生区,包含:伊甸园区 ,幸存者1区,幸存者2区

一、堆的核心概述

首先回忆一下JVM的架构

在一个JVM进程中,会产生一个Runtime类,也就是运行时数据区;其内部的线程,都独享一个程序计数器、本地方法栈、虚拟机栈而一个JVM进程,只有一个方法区和堆,因此,所有的线程都共享一个方法区和堆。

1.1、堆的核心概述

  • 一个JVM实例能存在一个堆内存,堆也是JAVA内存管理的核心区域。
  • JAVA堆区在JVM启动的时候被创建,其空间大小也就确定了。是JVM管理的最大一块的内存空间。
    • 堆内存的大小是可以调节的。
  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB)

二、设置堆内存大小与OOM

-Xms128m

-Xmx128m

三、年轻代与老年代

3.1、年轻代与老年代概述

  • 储存在JVM中的Java对象可以被划分为两类:
    • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
    • 另一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一直。
  • Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OladGen)。
  • 其中年轻代又可以划分为Eden空间、Survivor0空间Survivor1空间(有时也叫作from区、to区)。

3.2、参数的调整

3.2.1、--XX:newRation=ration

该条指令设置新生代与老年代的比例,默认值是2

  • 配置新生代与老年代在堆结构的占比。
    • 默认 --XX:newRation=2,表示新生代占1,老年代占2,新生代占整个堆的1/3.
    • 可以修改--XX:NewRation=4,表示新生代占1,老年代占4,新生代占整个堆的1/5。

什么时候需要调整--XX:newRation参数?

当程序当中有许多生命周期较长的类,他们都会存储在老年代中,此时我们可以通过扩大老年代的大小供更多的类进行存储。

3.2.2、--XX:SurvivorRation=ration

该条指令设置新生代中的Eden区域Survivor区的比例。

新生代中Eden区域Survivor区的比例默认为8,但是一般在实际中为6,因为在实际中自动开启了自适应内存分配策略,如果想关闭,则使用--XX:-UseAdaptiveSizePolicy指令来关闭。

3.2.3、--Xmn(一般不设置)

-Xmn设置新生代的空间大小。

四、图解对象与分配过程

堆空间大体分为两个区域,新生区和永久区;其中新生区分为 伊甸园区和幸存者1区和幸存者2区。

  • 当我们new一个新对象的时候,它会被放到伊甸园区;
  • 当伊甸园区满的时候,这个时候会执行YoungGC/MinorGC,会清理掉伊甸园区一大部分空间,这时没有被清理掉的对象就会被分配到幸存者区(哪个区没满就往哪个区放,因为幸存者区有2个)。
  • 当对象被放到幸存者区的时候,他会被标记一个age变量,age为1
  • 当一个幸存者区满的时候,就会去交换,并且age加一
  • 当一个对象age等于15的时候(默认值为15,可以通过-XX:MaxTenuringThreshold来设置),将该对象放入老年代。

总结:关于垃圾回收,频繁发生在新生代,很少在老年代收集,几乎不在永久区收集

五、Minor GC、Major GC、Full GC

针对Hotspot对JVM的实现,它里面的GC主要分为两部分:部分收集(Partical GC),整堆收集(Full GC)。

  • 部分收集
    • 新生代收集(Minor GC / Young GC)。只是对新生代的收集。Eden S0 S1,上述区域都会被收集到
    • 老年代收集(Major GC / Old GC)。只是对老年代的收集。
      • 目前,只有CMS GC会有单独收集老年代的行为
    • 混合收集(Mixed GC),收集整个年轻代和部分老年代
  • 整堆收集(Full GC)收集整个Java堆和方法区的垃圾收集

3.1、年轻代GC(Minor GC)触发机制

  • 当年轻代空间不足时,就会触发Minor GC, 这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次 Minor GC 会清理年轻代的内存。)
  • 因为 Java对象大多都具备朝生夕灭的特性,所以 Minor Gc 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor oc会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

3.2、Full GC触发机制

当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

六、堆空间分代思想

为什么需要把Java堆分带?不分代就不能正常工作了吗?

  • 其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那么所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而且很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块储存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间来。

七、内存分配策略

  • 如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次MinorGC,年龄就增加一岁,当他的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升(Pmotion)到老年代中。

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

  • 优先分配到Eden
  • 大对象直接分配到老年代
    • 尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或的部分与该对象可以直接进入老年代,无须等到MaxTenuringThreshold中年龄的要求。
  • 空间分配担保
    • --XX: HandlePromotionFailur

总结:

如果当当前存入的对象内存太大,大到新生代区都放不下的时候,则直接放入老年代。否则先放入新生代(伊甸园区)。

八、为对象分配内存:TLAB

TLAB全称为Thread Local Allocation Buffer,指的是线程本地分配缓冲区

8.1、什么是TLAB

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内(这个私有缓存区域,就是TLAB)
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略
  • 一旦对象在TLAB空间内存分配失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

8.2、为什有TLAB

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

知乎:TLAB详解

全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) - 知乎

**详解TLAB

1、TLAB内存分配的大概流程

  • 首先判断 对象需要分配的内存空间能不能放下默认分配好的TLAB空间。
    • 如果放得下,就将该对象放置到TLAB中。
    • 如果放不下,这个时候需要判断最大浪费空间限制
      • 如果小于最大浪费空间限制,则重新在Eden区申请一个新的TLAB进行分配,之前的TLAB区域退回到Eden区。
      • 如果大于最大浪费空间限制,直接在TLAB外进行分配。这种情况,说明该对象非常大,则直接分配到老年代中。

2、TLAB的生命周期

TLAB是线程私有的,线程初始化的时候,会创建并初始化TLAB。同时,在GC扫描对象之后,线程第一次尝试分配对象的时候,也会创建并初始化TLAB。TLAB生命周期停止(注意:TLAB生命周期停止不代表说这块区域被回收掉了,而是不被这个线程所拥有了)。

3、TLAB的优点

TLAB的优点很明显,就是能够避免锁的竞争;因为堆是一块线程共享的区域,在多线程高并发场景下,如果出现多个线程同时操作堆,而且还是同时操作堆的同一块区域,那么这块区域会出现线程不安全的问题。

而TLAB可以完美的解决这个问题,TLAB将每一个线程都分配一块属于自己的内存空间,这样就可以避免线程不安全的问题。

4、TLAB的缺点

4.1、TLAB引发的内存孔隙问题

在TLAB的内存空间不足以放下目标对象后,并且不超过最大浪费空间限制,他就会在Eden区重新分配一块空间,但是原来的空间就会被标记为 dummy Object ,GC的时候遇到dummy Object就会直接标记该区域并且跳过该区域的扫描;此时问题,就出现了,在GC没有来的时候,就会产生内存孔隙问题。

4.2、TLAB在分配内存时引发的问题

一个线程进来,该线程的TLAB肯定得给这个线程分配一块空间,那么分配多少呢?这就是个问题。

而且不同业务场景下对象的大小肯定也是不一样的,用户请求较少的TLAB分配的就少一点,请求较多的就分配大一点。

float AdaptiveWeightedAverage::compute_adaptive_average(float new_sample,
                                                        float average) {
  // We smooth the samples by not using weight() directly until we've
  // had enough data to make it meaningful. We'd like the first weight
  // used to be 1, the second to be 1/2, etc until we have
  // OLD_THRESHOLD/weight samples.
  unsigned count_weight = 0;

  // Avoid division by zero if the counter wraps (7158457)
  if (!is_old()) {
    count_weight = OLD_THRESHOLD/count();
  }

  unsigned adaptive_weight = (MAX2(weight(), count_weight));

  float new_avg = exp_avg(average, new_sample, adaptive_weight);

  return new_avg;
}

九、小结 堆空间的配置参数

  • -Xms初始堆空间内存
  • -Xmx最大堆空间内存
  • -Xmn设置新生代的大小(初始值以及最大值)
  • -XX:NewRation配置新生代与老年代在堆结构的占比
  • -XX:SurvivorRation设置新生代中Eden和s0/s1的占比
  • -XX:MaxTenuringThreshold设置新生代垃圾的最大年龄
  • -XX:+PrintGCDetails输出详细GC处理日志

十、堆是分配对象的唯一选择吗

在Java虚拟机中,对象是在Java堆中分配的,这是一个普遍的尝试。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么久可能被优化成栈上分配。这样就无须在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外储存技术。

10.1、逃逸分析概述

当方法里面的对象如果被外部调用,则该对象发生了逃逸,发生了逃逸就肯定在堆中。如果没发生逃逸,则可能会在栈上分配。

public StringBuilder fun() {
    StringBuilder sb = new StringBuilder();
    sb.append("a");
    sb.append("b");
    return sb;
}
public String fun() {
    StringBuilder sb = new StringBuilder();
    sb.append("a");
    sb.append("b");
    return sb.toString();
}

因为String存在常量池里面,不算对象。

结论

开发中能使用局部变量的,就不要使用在方法外定义。

10.2、代码演示

class User{
    int age;String name;
}
public class Main {
    public static void main(String[] args) {
        for (int i = 0 ; i < 10000000 ; i++) {
            add();
        }
    }
    public static void add() {
        User user = new User();
    }
}

该段程序如果开启逃逸分析,则会触发多次GC,并且运行时间40ms左右。如果开启了逃逸分析,则一次GC都不会触发,而且运行时间2ms左右,速度提升了许多。

10.3、逃逸问题的缺点

有个极端的例子,就是经过逃逸分析后,发现没有一个对象不是逃逸的,就是说所有对象都会发生逃逸,这个逃逸分析的过程就白白的浪费掉了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值