一、概述
1、概念
- 一个JVM实例只存在一个堆空间,它也是 Java 内存管理的核心区域
- 堆在启动时被创建,大小也被确定了
- 所有线程共享堆空间数据。但是在堆空间上为每个线程划分了各自对应的私有的缓冲区(Thread Local Allocation Buffer,TLAB)
- 在 hotspot 虚拟机中所有的对象都是分配在堆空间中的
2、堆空间细分
- java7 之前:新生代+老年代+永久区
- java8 及以后:新生代+老年代+元空间
- 设置堆空间大小:-Xms 堆空间起始内存,-Xmx 堆空间最大内存。默认起始内存为 电脑物理内存/64,最大内存为 电脑物理内存/4。(这里设置的为 新生代 + 老年代的内存大小)
3、新生代与老年代
- 新生代分为:Eden 空间、Survivor0 空间、Survivor1 空间 (也叫 from 区、to 区)
- 默认新生代、老年代的比例为 1: 2,即 新生代占整个对空间的 1/3。-XX:NewRatio=2 (默认)
- 在新生代中 Eden :Survivor0 :Survivor1 为 8:1:1。-XX:SurvivorRatio=8 (默认)
4、对象分配过程
如上图所示(没有按实际比例)
- new 的对象先放在 Eden 中,放不下进行 Minor GC,还放不下放到老年代,老年代放不下则进行Major GC/Full GC,如果还是放不下则 OOM
- Eden 满的时候进行 Minor GC (Young GC)将 Eden 区中没有被其他对象所引用的对象销毁,幸存的对象放入 幸存者0区(Survivor0),Survivor0 区中对象的年龄计数 + 1。如果 Survivor区放不下,放入老年代。
- 当 Eden 再次满的时候继续进行 Minor GC,将 Eden 区、Survivor0 区中没有被其他对象所引用的对象销毁,幸存的对象放入 Survivor1 区。并将 幸存的对象年龄计数 +1。如果对象的年龄 > 15,则放入老年代 。
- 当 老年代 满的时候进行 Major GC (Old GC)对老年代进行内存清理
- 如果老年代 GC 后任然没有空间,则 OOM
总结:频繁在新生代进行垃圾回收,很少在老年代进行,几乎不在永久代/元空间收集
5、TLAB
- 在 Eden 区中为每个线程分配了一个私有的缓存区域,包含在 Eden 区中
- 产生原因:堆空间是所有线程共享的,在并发环境下为了避免多个线程同时操作同一个地址,需要使用加锁机制,进而影响分配速度。使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,我们将这种内存分配方式称为快速分配策略
- JVM 将 TLAB 作为内存分配的首选,一旦对象在 TLAB 中分配失败,JVM 就会通过使用锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存
- TLAB 空间非常小,只占 Eden 的 1%
6、逃逸分析
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
7、标量替换
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过 JIT 优化,就会把这个对象拆解成基本的变量来替换。
public class JavaTest {
public static void main(String[] args) {
add();
}
public static void add() {
User user = new User("张三", 20);
System.out.println("name:" + user.getName() + "age:" + user.getAge());
}
}
class User {
private String name;
private Integer age;
public User(String name,Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
}
经过逃逸分析发现,User 对象只在 add() 方法中使用,没有发生逃逸,这时 JVM 就会进行标量替换。重新替换后的 add () 方法如下:
public static void add() {
String name = "张三";
int age = 20;
System.out.println("name:" + name + "age:" + age);
}
经过标量替换后,两个变量就会被分配在栈帧中的局部变量表。这样就减少了堆空间的内存占用