运行时数据区 - 堆

① 堆的核心概念
  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  • Java堆区在JVM启动时就被创建,其空间大小也确定下来(允许参数设置)。是JVM管理的最大一块内存空间。
  • <<Java虚拟机规范>>规定,堆可以处于物理机上不连续的内存空间中,但逻辑上应该被视为连续的。
  • 虽然所有线程共享堆,但是堆空间中还有一部分区域被划分为了线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。这是为了提升并发效率而设计的。
  • 如果说一个栈空间维护了一个堆中对象的引用,这个方法结束时,堆中对象不会马上被移除,而是在被GC识别为垃圾的时候才会被移除。
  • 堆是GC执行垃圾回收的重点区域。
② 设置堆内存大小和OOM

Java堆在JVM启动时就已经设定好了,但在启动前可以通过-Xms和-Xmx进行设置。

  • -Xms设置堆的起始内存。
  • -Xmx设置堆的最大内存。一旦堆内存超过设置的最大内存时,就抛出OutOfMemoryError异常。

在生产环境中,-Xms-Xmx最好设置为同样大小,可以提升性能。

**单位:**当无后缀的时候表示byte例如-Xms62291456k表示千字节例如-Xms6144km表示兆例如-Xms6m

默认情况:

在不去人为设置堆空间大小的时候。

  • 初始内存大小:物理机内存大小 / 64
  • 最大内存大小:物理机内存大小 / 4
③ 新生区与养老区

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

  • 生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
  • 生命周期较长的对象,在某些极端情况下还能够与JVM的生命周期保持一致。

在这里插入图片描述

其中年轻代又可以划分为Eden空间、Survivor0空间和Suvivor1空间。也可以成为from区,to区。

④ 图解对象分配过程

在这里插入图片描述

详细步骤:

在这里插入图片描述

红色表示需要被GC的对象,绿色表示依旧存活的对象。

当年轻代满时触发MinorGC/YGC,将不被需要的对象销毁,将继续使用的对象加入到S0或S1区,假设加入到S0区,则S1区为空,此时S1区称为to区。****

在这里插入图片描述

当年轻代重新满时再次触发MinorGC/YGC,将不被需要的对象销毁,将继续使用的对象加入到to区。然后再检测from区,将不再需要的对象销毁,将继续使用的对象移动至to区。并且对移动的对象进行age + 1操作。此时由于移动,from区该销毁的销毁,该移动的移动。曾经的from区为空,to区不为空。此时曾经的from区变为to区,to区变为from区。

在这里插入图片描述

经过持续的GCto区会出现一些对象的age属性达到了某一阈值。这里假设阈值为15,于是将达到阈值的对象加入到老年代。

自定义阈值:

-XX:MaxTenuringThreshold=<N>
Minor GC、Major Gc、Full GC

由于GC线程会阻塞用户线程,所以在调优时,我们需要在保证安全的情况下尽可能减少GC频率,减少阻塞时间。

JVM在进行GC时,并不是每次都对三个内存区域(新生代,老年代,方法区)一起回收,而绝大多数回收的都是年轻代。

  • 新生代收集:对新生代,S0S1区域进行收集,使用Minor GC/Yong GC
  • 老年代收集:只对老年代,使用Major GC/Old GC
  • 整堆收集:对整个Java堆和方法区进行收集。使用Full GC

年轻代Minor GC触发机制:

  • 当年轻代空间不足时会触发Minor GCSurvivor0/1满并不会引发GC。但Minor GC一旦触发也会清理S0/1区域。
  • 因为Java对象大多数都是转瞬即逝的,所以**Minor GC的触发会非常频繁。**
  • **Minor GC会引发STW => Stop The World,暂停其他用户线程,**等垃圾回收结束后,用户线程才会恢复。

老年代Major GC或Full GC触发机制:

  • **出现Major GC时,经常会伴随至少一次的Minor GC``(但并不是绝对的)。**当老年代空间不足时,会先尝试触发Minor GC。如果发现空间还不足,则触发Major GC
  • Major GC的速度比Minor GC10倍以上。STW时间更长。
  • 如果Major GC后内存还不足,则会OOM Java Heap Space

在这里插入图片描述

⑥ 堆空间分代思想
  • 经研究,Java中的对象**70% ~ 99%都是临时对象**。

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

其实不分代是完全可以的。但这么做是为了优化GC性能。将多次没有被GC的对象放到老年代,可以避免持久对象进行多次无用的GC检查。

⑦ 内存分配策略
  • 优先分配到Eden区域。

  • 大对象由于Eden区域放不下,直接分配到老年代。(但是要尽量笔迷这样的情况,因为Major GC会消耗更多性能)

  • 长期存活的对象分配到老年代。

  • 动态年龄判断:如果Survivor区域中年龄相同的对象总和大于了Survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThresh中要求的age

  • 空间分配担保:-XX:HandlePromotionFailure可以设定其参数。对于Survivor区域的对象,无法进行Promotion操作的时候,即Survivor空间不足,则可以直接晋升到老年代。

⑧ 为对象分配内存:TLAB(Thread Local Allocation Buffer)

为什么有TLAB

  • 堆是线程共享区域,任何线程都可以访问到堆区的共享数据。
  • 由于共享,可能出现线程安全问题。
  • 为了避免多个线程操作同一地址,需要使用加锁机制,但这样影响分配速度。

什么是TLAB

JVMEden区为每个线程都划分了一块线程独有的缓冲区,包含于Eden区域内。

多线程同时分配内存时,使用TLAB可以避免一些列的线程安全问题,同时还可以提升内存分配的吞吐量,这被称为快速分配策略

TLAB再说明:

  • 尽管不是所有的对象实例都能够被分配到TLAB区域,但JVM确实是将TLAB作为内存的首选。
  • 在程序中,可以通过-XX:UseTLAB设置是否开启TLAB空间。
  • 默认情况下,TLAB的区域空间非常小,仅占Eden1%,但我们也可以通过-XX:TLABWasteTargetPercent来修改百分比大小。
  • TLAB区域默认开启。
  • 一旦对象在TLAB区域分配失败,JVM就会尝试使用锁来确保数据操作的原子性,从而直接在Eden区域分配。

TLAB对象分配过程:

在这里插入图片描述

⑨ 堆是分配对象的唯一选择吗?

不是。

随着Java的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术会使原有的绝对堆上分配发生一些微妙的变化

,⑩ 逃逸分析

如果一个对象并没有逃逸出方法的话,那么就可能被优化为栈上分配。

  • 如果有个对象在方法中被定义,且只在方法内部使用,则认为它没有发生逃逸。
  • 当一个对象在方法中被定义后,它被外部方法引用,则认为发生了逃逸。

如何将一个会逃逸的对象变成非逃逸对象?

//逃逸
public StringBuilder test() {
    StringBuilder sb = new StringBuild();
    return sb;
}

//非逃逸
public String test() {
    StringBuilder sb = new StringBuild();
    return sb.toString();
}
⑩① 使用逃逸分析进行优化
1) 栈上分配
public class Main {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        /* 执行一千万次 */
        for (int i = 0; i < 10000000; i++) {
            CreateObj();
        }

        long end = System.currentTimeMillis();

        System.out.println("耗时:" + (end - start) + "ms");
    }


    public static void CreateObj() {
        /* 未发生逃逸 */
        User user = new User();
    }
}
class User { }

首先我们关闭逃逸分析。

image-20220221040217165

得到运行耗时:46ms

开启逃逸分析。

得到运行耗时:2ms

修改代码:

public class Main {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        User user = null;
        
        /* 执行一千万次 */
        for (int i = 0; i < 10000000; i++) {
            user = CreateObj();

        }
        /* Obj 可能被其他线程继续使用 */
        userObj(user);

        long end = System.currentTimeMillis();

        System.out.println("耗时:" + (end - start) + "ms");
    }


    public static User CreateObj() {
        /* 发生逃逸 */
        User user = new User();
        return user;
    }

    public static void userObj(User user) { }

}
class User {
    public int age = 5;
}

使代码发生逃逸,且开启逃逸分析。

得到运行时间:43ms

2) 同步省略

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

3) 分离对象或标量替换

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

标量指一个无法再分解成更小得数据的数据,例如基本数据类型就是标量。

聚合量指的是那些还可以分解为 标量 或 聚合量 的数据。

**标量替换:**在JIT阶段,如果经过逃逸分析,发现一个对象并没有发生逃逸,则会将这个对象拆解为多个标量的状态。

例如:

public class Main {
    public static void main(String[] args) {
        test();
    }
    
    public static void test() {
        Point point = new Point();
    }
}

class Point {
    int x;
    int y;
}

此时,经过标量替换,栈中Point实例会被替换为。

public class Main {
    public static void main(String[] args) {
        test();
    }
    
    //此处被替换
    public static void test() {
        int x;
        int y;
    }
}

class Point {
    int x;
    int y;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值