JVM五:内存分区之线程共享区—堆

The heap is the run-time data area from which memory for all class instances and arrays is allocated
《Java 虚拟机规范》中对堆的描述为:所有对象实例以及数组都应当在运行时分配在堆上。
《Java 虚拟机规范》规定:堆可以处于物理上不连续的内存空间中,但在逻辑上它应该是被视为连续的。其大小是可以调节的。
所有的线程共享 Java 堆内存,也可以在内划分线程私有的缓冲区(Thread Local Allocaltion Buffer,TLAB)
方法结束后、堆中的对象不会马上被清理,仅在垃圾回收时才会被清理。堆是垃圾回收的重点区域。

1. 堆内存划分

1.1 逻辑划分

在这里插入图片描述

约定:新生代(区)<=> 年轻代、老年代(区)<=> 养老区、永久代 <=> 永久区

1.2 堆内存大小设置

我们常说的堆区的内存设置指的是新生代老年代的设置、对于永久代或者元空间的内存大小是需要单独设置的。

  1. -Xms 用于设置堆区的初始内存,等价于 -XX:InitialHeapSize、默认主机内存大小 / 64
  2. -Xmx 用于设置堆区的最大内存,等价于 -XX:MaxHeapSize、 默认主机内存大小 / 4
  3. 通常会把两个参数设置成一样的、可以在 Java 垃圾回收之后不再重新计算分配内存,以此提高性能。

2. 新生代和老年代

在这里插入图片描述
4. 默认-XX:NewRatio=2表示新生代占 1,老年代占 2。(如果改成 -XX:NewRatio=4则表示新生代占 1,老年代占据 4。)
5. 使用-Xmn设置新生代最大内存大小、一般都是使用默认值
6. 使用-XX:SurvivorRatio设置新生代中 EdenSurvivor 的比值。(-XX:Survivor=8 表示 Eden:Survivor = 8:2

几乎所有的 Java 对象都是在 Eden 区被 new 出来的,而且绝大多数对象的销毁也是在 Eden 区进行的。IBM 公司有研究表明,新生代中 80% 的对象都是朝生夕死的。

2.1 对象在堆空间的内存分配

  1. new 的对象先放置到 Eden
  2. Eden 区空间满了后,此时再 new 一个新的对象,JVM 的垃圾回收器对 Eden 区进行垃圾回收(MinorGC / YGC)。先将 Eden 区不再被其他对象引用的对象进行销毁,然后把新的对象放到 Eden 区。
  3. Eden 区中的剩余对象移动到幸存者 S0
  4. 再次触发垃圾回收时,上次垃圾回收丢到 S0 区的幸存者如果这次垃圾回收还没有被回收、则丢到 S1 区(此时 Eden 区幸存对象也会移入 S1 区,S0 区为空)(其实此时已经为空的 S0 应该是 S1、便于理解就不乱换名字了)
  5. 再次触发垃圾回收时,上次丢到 S1 点幸存者还没被回收,则再丢到 S0 区(此时 S1 区为空)
  6. 什么时候进入老年代呢?可通过设置幸存次数、默认 15 次(-XX:MaxTenuringThreshold=15
  7. 老年代相对悠闲,只有当老年代内存不足时,会触发 MajorGC / FullGC
  8. 当触发 MajorGC 之后,发现依然无法进行对象的存储,则会 OOM 异常。(java.lang.OutofMemoryError: Java heap space

对于幸存者 S0 、S1 区,复制之后有交换、谁空谁是 S1
对于垃圾回收、新生代频繁、老年代偶尔、永久代/元空间几乎不会

在这里插入图片描述

3. 堆区的垃圾回收

堆区的垃圾回收主要集中在新生代、并非每次都是整堆回收。Hotspot 按照回收区域主要分两种:部分回收、整堆回收

  • 部分回收:
    • 新生代回收(Minor GC / Young GC):仅代表新生代的垃圾回收行为
    • 老年代回收(Major GC / Old GC):仅代表老年代的回收
      • 很多时候 Major GC 会和 Full GC 混淆在一起,需注意分辨
    • 混合回收(Mixed GC):回收整个新生代以及部分老年代(目前只有 G1 GC 有此行为)
  • 整堆回收(Full GC):回收整个堆区(包含方法区)

3.1 新生代(Minor GC)的触发

一定是 Eden 区满的时候才会触发、幸存者 S 区满是不会触发的

因为 Java 对象大多都是朝生夕死,所以 Minor GC 特别频繁,新生代的回收速度也特别快
Minor GC 会引发 STW(暂停其他用户的线程、等垃圾回收结束后恢复)

在这里插入图片描述

S0S1 不是固定的、谁空谁为 to

3.2 老年代(Major GC)的触发

  1. 出现了 Major GC 一般(是一般、有的虚拟机就直接进行 Major GC 了)伴随着至少一次的 Minor GC、也就是说当触发了 Major GC 时会先执行 Minor GC、执行完之后空间还是不足才会执行 Major GC
  2. Major GC 的速度一般会比 Minor GC 慢 10 倍以上、STW 时间更长
  3. 如果 Major GC 之后内存还是不足、就会 OOM

3.3 Full GC 的触发

  1. 调用 System.gc() 时、不会立刻执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. Major GC 后可能会有对象要放入老年代、此时幸存者所需内存大于老年代可用内存、则触发
  5. Eden 区和 S0 区幸存者向 S1 区复制时,所需内存大于 S1 剩余可用内存,此时会把该对象转存到老年代,若此时老年代也放不下、则触发

Full GC 是开发、调优中尽量避免的、极大避免 STW 的时延

4. 堆区的分代思想

为什么要给堆区分新生代、老年代、Eden 区、S0 区、S1 区这么多区域呢。
不区分这些区域程序跑不起来么?当然可以。
但是,不同对象的生命周期是不同的,如果把短周期和长周期的都放一起,意味着每次垃圾回收的都是一次 Full GC ,需要逐个去确认对象是否有被引用,极大的损耗性能。
所以堆区分代只有一个理由便是优化 GC 性能。
朝生夕死的对象放到 Eden 区、频繁进行 Minor GC
大对象以及数次 Minor GC 还存活的对象放到老年代,偶尔进行 Full GC

5. 内存分配策略

对象初创建放到 Eden 区,经历第一次 Minor GC 之后仍然存活,如果 Survivor 区可用空间足够则移入,并将该对象年龄设置为 1 岁,没经历一次 Minor GC 则涨一岁,当增加到一定岁数后就可以去养老区。(通过 -XX:MaxTenuringThreshold 来设置)
对象分配原则如下:

  • 优先分配 Eden
  • 大对象直接分配到老年代(写代码的时候要避免大对象)
  • 长期存活的对象熬到一定岁数分配到老年代
  • 动态分配:如果 survivor 区中相同年龄的所有对象占用的总内存大于 survivor 空间的一半,则年龄等于或大于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 设置的年龄线
  • 空间分配担保:-XX:HandlerPromotionFailure

6. 私有缓冲区 TLAB(Thread Local Allocation Buffer)

6.1 为什么有 TLAB

堆区作为共享区域、是任何线程都可以访问的,也就是说在并发场景下频繁的创建实例对象并进行内存空间的划分是不安全的,而为了避免多个线程操作同一地址,需要采用加锁等机制,而这样的话问题就是会影响内存的分配速度。

6.2 TLAB 的分配

JVM 对 Eden 区继续划分,为每个线程分配了一个私有缓冲区,所以他是存在于 Eden 区的
多个线程同时分配内存时,使用 TLAB 可以避免一系列非线程安全问题,同时还能提升内存分配的吞吐量,这种方式称之为快速分配策略

  • 并非所有对象都是通过 TLAB 分配内存的,JVM 只是将 TLAB 作为首选
  • 可通过 -XX:UseTLAB 来设置是否开启 TLAB 空间
  • 默认情况下,TLAB 占用内存极低,仅占用 Eden 空间的 1%,可通过 -XX:TLABWasteTargetPercent 来设置占用百分比
  • 当 TLAB 分配内存失败的时候,JVM 会尝试采用加锁机制确保内存分配的原子性,以此在 Eden 中分配内存

7. 堆区参数设置

-Xms初始堆空间内存(默认为物理内存的1/64)
-Xmn最大堆空间内存(默认为物理内存的1/4)
-Xmx设置新生代的大小(初始值及最大值)
-XX:+PrintFlagsInitial查看所有的参数的默认初始值
-XX:+PrintFlagsFinal查看所有的参数的最终值(可能会存在修改,不再是初始值)
-XX:NewRatio配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold设置新生代垃圾的最大年龄
-XX:+PrintGCDetails输出详细的GC处理日志
-XX:+PrintGC打印gc简要信息
-verbose:gc打印gc简要信息
-XX:HandlePromotionFalilure是否设置空间分配担保

发生 Minor GC 之前,JVM检查老年代最大可用的连续空间是否大于新生代所有对象的总空间

  • 如果大于,则本次 Minor GC 是安全的
  • 如果小于,则 JVM 会查看 -XX:HandlePromotionFailure 设置值是否允许担保失败
    • 如果 -XX:HandlePromotionFailure=true 、则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代的平均对象大小
      • 如果大于:则尝试进行 Minor GC、但不得不说本次 Minor GC 是有风险的
      • 如果小于:则直接进行 Full GC
    • 如果 -XX:HandlePromotionFailure=false 、则直接进行 Full GC

JDK6 Update24 之后、-XX:HandlePromotionFailure 不在生效、并且默认为 true,也就意味着每次 Minor GC 之前都会检查老年代剩余可用最大连续内存空间:1.是否大于新生代所有对象占用的总空间、2.是否大于历次晋升到老年代的平均对象大小,只要有一个不满足,那么就直接进行 Full GC

8. 堆区是分配对象的唯一选择?

引入《深入理解 Java 虚拟机》一句话:随着 JIT 编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

一路学来、对象都是在堆区分配内存。但是、如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就有可能被优化成栈上分配。这样就不用在堆区分配,也不用进行垃圾回收。这是场景是极为常见的堆外存储技术。

还有一些其他的 JVM 实现如 TaoBaoVM,他们把一些生命周期较长的对象直接放到了堆外存储,而且不让 GC 进行管理。

8.1 关于逃逸分析

逃逸分析就是 JVM 通过分析对象的实际作用域,是在方法内还是说蔓延到方法外了,以此来判断该对象在哪儿分配内存空间。

如何将本该分配到堆区的对象分配到栈,这就需要使用逃逸分析手段。
逃逸分析的基本行为就是分析对象的动态作用域:

  • 当一个对象在方法内被定义后,对象仅仅在方法中被使用,则认为没有发生逃逸
  • 当一个对象在方法内被定义后,然后被外部方法引用(如被当作参数传递到了其他方法中、或者在方法中为成员属性对象赋值),则认为发生逃逸
// 如果想让下面方法不发生逃逸:把 return sb; 改为 return sb.toString();
public static StringBuffer contact(String s1, String s2) {
	    StringBuffer sb = new StringBuffer();
	    sb.append(s1);
	    sb.append(s2);
	    return sb;
}
public class EscapeAnalysis {

    	public EscapeAnalysis obj;
    
     	// 方法返回EscapeAnalysis对象,发生逃逸
	    public EscapeAnalysis getInstance() {
	        return obj == null ? new EscapeAnalysis() : obj;
	    }
    
	    // 为成员属性赋值,发生逃逸
	    public void setObj() {
	        this.obj = new EscapeAnalysis();
	    }
    
     	// 对象的作用于仅在当前方法中有效,没有发生逃逸
	    public void useEscapeAnalysis() {
	        EscapeAnalysis e = new EscapeAnalysis();
	    }

     	// 引用成员变量的值,发生逃逸
	    public void useEscapeAnalysis2() {
	        EscapeAnalysis e = getInstance();
	    }
}
  • 参数设置、在 JDK 6u23 之后,HotSpot 默认开启逃逸分析、在这个版本之前的可通过一下参数配置
    • -XX:+DoEscapeAnalysis 表示开启逃逸分析
    • -XX:+PrintEscapeAnalysis 表示查看逃逸分析的筛选结果

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

8.2 JVM 针对逃逸分析进行的优化操作

8.2.1 栈上分配

如果对象只在当前方法内运转,则将对象直接直接分配到栈上

8.2.2 同步省略(锁消除)

如果对象只有一个线程访问到,那么对于这个对象可以不考虑同步操作
线程同步的代价是很高的,同步的后果是降低并发性和性能。
在编译同步代码时,JIT 编译器可以借助逃逸分析来判断同步代码块所使用的锁对象是否只被一个线程所访问。如果是,那么 JIT 编译器就会在编译这块儿代码的时候取消同步,这样可以极大提升并发性和性能,这个取消同步的过程就是同步省略,也叫做锁消除。

public void f() {
		Object hellis = new Object();
		synchronized(hellis) {
		    System.out.println(hellis);
		}
}

hellis 这个对象生命周期只在 f() 方法中,并不会被其他线程访问到,所以 JIT 编译器会将其优化为如下代码

public void f() {
	    Object hellis = new Object();
	    System.out.println(hellis);
}
8.2.3 分离对象 / 标量替换

如果对象不需要连续的内存空间来存储也可以进行访问,那么这个对象的部分或者是全部可以直接存储在 CPU 的寄存器中。

  • 标量:无法在分解的最小数据单元(Java 中的基本数据类型)
  • 聚合量:还可以在分解的数据(Java 中的对象)、可以分解成其他聚合量和标量
    JIT 阶段、经过逃逸分析后,发现一个对象不会被外界访问的话,那么就会把这个对象进行拆解替换。
public static void main(String args[]) {
    	alloc();
}
private static void alloc() {
	    Point point = new Point(1,2);
	    System.out.println("point.x" + point.x + ";point.y" + point.y);
}
class Point {
	    private int x;
	    private int y;
}

point 对象仅在当前方法中使用,那么就直接分解 point 对象并替换为两个标量,这样就不用在创建对象了,也就不用在分配内存。

private static void alloc() {
	    int x = 1;
	    int y = 2;
	    System.out.println("point.x = " + x + "; point.y=" + y);
}
  • 使用 -XX:EliminateAllocaltions 表示开启标量替换(默认开启)、允许将对象分解替换
  • 使用如下配置进行调试分析 -server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
    • -server :启动 Server 模式、只有在 Server 模式下才可以开启逃逸分析
    • -XX:+DoEscapeAnalysis:启用逃逸分析
    • -Xmx10m:指定了堆空间最大为 10MB
    • -XX:+PrintGC:将打印 Gc 日志
    • -XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上,比如对象拥有 id 和 name 两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配

上述举例代码中如果进行 1 亿次 alloc 方法,假设不开启逃逸分析,一个 point 对象占据 16 字节的空间,那么 10 亿次就是 1.5GB 的内存消耗。如果堆内存不够大就直接 OOM 了。而开启了逃逸分析后,我们进行了对象分解替换,转为栈内存储两个标量,方法结束直接回收。

8.2.4 逃逸分析一定好吗

逃逸分析的理论 1999 年就已经出现、而直到 JDK6 才实现,显然有很多顾虑。
最根本的原因就是无法保证逃逸分析的带来的性能提升是否大于它本身的损耗。假设一个极端的例子,经过一系列的逃逸分析操作之后,发现没有一个对象是不逃逸的,那么逃逸分析这一波操作就白进行了。
虽然目前逃逸分析的技术还不是很成熟,但它依然是即时编译优化技术中的一个十分重要的手段

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值