首页 > 技术文章 > 堆

tang321 2021-04-24 12:01 原文

一个JVM实例只存在一个堆内存
Java堆区在JVM启动的时候即被创建,其空间大小也确定下来了。是JVM管理的最大一块内存空间(堆内存的大小是可以调节的)
堆可以处于物理上不连续的内存空间,但在逻辑上应该被视为连续的
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除

 

Java 7及之前堆内存逻辑上分为三部分:新生代(Young Generation Space)+老年代(Tenure Generation Space)+永久代(Permanent Space)
Java 8及之后堆内存逻辑上分为三部分:新生代(Young Generation Space)+老年代(Tenure Generation Space)+元空间(Meta Space)

 

堆空间大小设置

-Xms 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize
-Xmx 用于表示堆区的最大内存,等价于 -XX:MaxHeapSize

一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError
通常会将 -Xms 和 -Xmx 两个参数配置相同的值,目的是为了能够在Java回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
默认情况下,初始内存大小=物理内存大小/64,最大内存大小=物理内存大小/4

//返回堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory();
//返回最大堆内存
long maxMemory = Runtime.getRuntime().maxMemory();

  

查看设置的参数
方式一:jps --> jstat -gc 进程id
方式二:-XX:+PrintGCDetails

配置新生代和老年代在堆结构的占比:
默认 -XX:NewRatio=2 ,表示新生代占1,老年代占2
修改 -XX:NewRatio=4 ,表示新生代占1,老年代占4
配置Eden和Survivor空间比例
-XX:SurvivorRatio=8

 

对象分配的特殊情况

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

注意:右边虚线框中是判断YGC后Survivor能否放下所有存活的对象

 

 

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

 

如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。

 

 

TLAB(Thread Local Allocation Buffer)

JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此将这种内存分配方式称之为快速分配策略
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%
一旦对象在TLAB空间分配内存失败时,JVM会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存

 

 

逃逸分析

在Java虚拟机中,对象是在堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收了。
基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GC Invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的

通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上

逃逸分析的基本行为就是分析对象动态作用域:
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中

 

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

 

使用逃逸分析,编译器可以对代码做如下优化:
栈上分配:将堆分配转化为栈分配
同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中

 

同步省略

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

 

分离对象或标量替换

标量(Scalar)是指一个无法再分解成更小的数据的数据,Java中的原始数据类型就是标量
相对地,还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,可以分解成其他聚合量和标量
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个标量来代替。这个过程就是标量替换

public static void main(String[] args) {
	alloc();
}
private void alloc() {
	Point point = new Point(1, 2);
}
class Point {
	private int x;
	private int y;
	Point(int x,int y) {
		this.x = x;
		this.y = y;
	}
}

  

private void alloc() {
	int x = 1;
	int y = 2;
}

  

使用的参数:
-server   启动Server模式,因为在Server模式下,才可以启用逃逸分析
-XX:+DoEscapeAnalysis   启动逃逸分析
-XX:+PrintGC   打印GC日志
-XX:+EliminateAllocations   开启了标量替换(默认打开),允许将对象打散分配在栈上,比如对象拥有id和name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配

 

推荐阅读