• 新生代

    • Eden
    • S0
    • S1
  • 老年代

  • metaSpace

  • 一个Java程序运行起来是一个进程

    • 这个进程对应着一个JVM实例
      • 一个JVM实例对应着一个运行时数据区。
        • 一个运行时数据区对应着一个方法区和一个堆。
          • 一个进程中会有多个线程,这些 线程共享方法区和堆区
  • 核心概述

    • 一个JVM实例只对应一个堆内存,堆内存也是Java内存管理的核心区域。
    1. Java堆区在JVM启动的时候就被创建,空间大小也就确定了。是JVM管理的最大的一块内存空间。
    2. 堆内存大小是可以调节的,使用 -Xms 和-Xmx命令进行调节,其语法和栈空间大小一致。
    3. Java虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上他应该是被视为连续的。
    4. 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer , TLAB)。
    5. 所有的对象实例几乎都在堆空间分配,也有一些特殊情况不在堆空间分配。(比如逃逸分析,栈上分配等)
    6. 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
    7. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
    8. 堆是垃圾回收(GC)的重点区域。
  • 堆的空间大小使用-Xms 和-Xmx进行设置,其中-Xms表示的是Java启动的时候堆空间的大小,-Xmx表示的是堆空间最大的大小。
  • 堆空间大小设置 默认情况下,初始内存大小为物理电脑内存大小/64;最大内存大小为物理电脑内存大小/4

我们可以通过一下代码查看堆空间的起始大小和最大空间大小,并计算出我们的物理内存:

 public static void main(String[] args) {
        //堆起始内存
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //堆最大内存
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms:" + initialMemory + "M");
        System.out.println("-Xmx:" + maxMemory + "M");

        System.out.println("系统内存大小为:" + initialMemory * 64 / 1024 + "G");
        System.out.println("系统内存大小为:" + maxMemory * 4 / 1024 + "G");
    }

堆空间划分

  • Java7及之前的堆内存逻辑上分为三部分:新生区 + 养老区+永久代

  • Young Generation Space 新生区 Young/new 又被划分为Eden区和Survivor区

  • Tenure Generation Space 养老区 Old/Tenure

  • Permanent Space 永久区 Perm

  • Java8及之后的堆内存逻辑上分为三部分:新生区 + 养老区+元空间

  • Young Generation Space 新生区 Young/new 又被划分为Eden区和Survivor区

  • Tenure Generation Space 养老区 Old/Tenure

  • Meta Space 元空间 Meta

alt

  • 堆的内存结构
  • **年轻代空间是两个幸存者区和Eden区,而幸存者区大小一样并且只会有一个有数据。**也就是说幸存者区在某一时刻必定会有一个是没有数据的,也就是说不占空间,所以在计算的时候只算了一个幸存者区的空间

年轻代和老年代

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

对象的分配过程

  1. new的对象先放在伊甸园 eden区,此区有大小限制。
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中不再被其他对象所引用对象进行销毁,再加载新的对象放到伊甸园区。
  3. 然后将伊甸园区的剩余对象移动到幸存者0区,此时对象的存活年龄+1。
  4. 如果再次触发垃圾回收,会将伊甸园区和幸存者0区的不再被其他对象所引用对象进行销毁,此时伊甸园区和幸存者0区存活的对象都会转移到幸存者1区,此时幸存者0区没有对象。
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区,每次年龄都会+1。

alt

  1. 那什么时候会去养老区呢?可以设置次数,默认15次,也就是年龄达到16 的时候就会转到养老区了。可以设置参数 -XX:MaxTenuringThreshold来进行设置。
  2. 如果(s0/ s1 )幸存者区中相同年龄的所有对象大小的总和 大于 幸存者区空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold的要求年龄。
  3. 在养老区相对悠闲,当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
  4. 若养老区进行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。
public class OutOfMem {

    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            int i = new Random().nextInt(1024 * 200);
            Picture picture = new Picture(i);
            list.add(picture);
        }
    }
}


class Picture {
    private byte[] img;

    public Picture(int length) {
        this.img = new byte[length];
    }
}

"C:\Program Files\Java\jdk1.8.0_251\bin\java.exe" -Xms500m -Xmx500m "-javaagent:D:\iiid\IntelliJ IDEA 2020.2.4\lib\idea_rt.jar=3180:D:\iiid\IntelliJ IDEA 2020.2.4\bin" -Dfile.encoding=UTF-8 
 com.osvue.fblock.jvm.OutOfMem
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.osvue.fblock.jvm.Picture.<init>(OutOfMem.java:33)
	at com.osvue.fblock.jvm.OutOfMem.main(OutOfMem.java:20)
  • 一开始Eden区不断增长,等Eden空间满了之后发生一次YGC(此时Eden是空的),将存活的对象放在了幸存者区,之后不断有新对象创建,放在Eden区,等Eden空间满了之后再次发生YGC,此时由于幸存者区空间不够,会把对象放到老年代,而s1,s2会交替复制存活的对象

  • 老年代则会一直接收从新生代过来的对象,直到老年代再也放不进去对象,进行FGC/Major GC.还是没有足够的空间,报错:java.lang.OutOfMemoryError: Java heap space

  • 这也就解释了为什么Eden是逐渐增长,幸存区交替出现,老年区台阶式的增长

  • `新对象创建放在Eden,不断放就会一直增长,Eden满了触发YGC,Eden变空,存活对象进入幸存区,之后创建的对象又会放在Eden,慢慢Eden又满了,再次触发YGC,此时想要放到幸存区,发现空间不够,所以幸存区(S0/S1)放到老年区,然后新对象放在另一半的幸存者区,此时老年区就会有对象,所以有了第一级台阶,如此反复,直到堆区空间占满。而元空间(Metaspace)存放的是类和其他的一些信息,一直不会变,所以元空间(Metaspace)基本保持不变。

  • 总结

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to.
  • 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。

  • JVM在进行GC时,并非每次都对三个内存(新生代,老年代,方法区)区域一起回收,大部分时候回收的都是指新生代。

针对HotSpot虚拟机,他里边的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC). 部分收集不是完整收集整个Java堆的垃圾收集。其中又分为:

  • 新生代收集 (Minor GC/Young GC):只是新生代(Eden,s0,s1)的垃圾收集
    • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,幸存者区满不会引发GC。每次Minor GC会清理年轻代的内存。
  • 老年代收集(Major GC/ Old GC)只是老年代的垃圾收集。(很多时候Major GC和Full GC混淆使用,需要具体分辨是老年代还是整堆收集),CMS GC 会单独手机老年代的行为。
  • 混合收集(Mixed GC)手机真个新生代以及部分老年代的垃圾收集,G1会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

逃逸分析和栈上分配

  • 在JVM中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能 优化成栈上分配。这样就无需在堆内存 上分配内存,也无需进行垃圾回收了。这也是最常见的堆外存储技术
  • 发生逃逸的情况:给成员变量赋值,方法返回值,实例引用传递
  • 避免发生逃逸
  • 就看new的对象实体是否有可能在方法外被调用。
  • 方法中能使用局部变量的,就不要使用在方法外定义。

由淘宝定制的TaoBaoVM 其中创建的GCIH(GC Invisible Heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外 并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

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

    • 逃逸分析的基本行为就是分析对象动态作用域:
      • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
      • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
public void stack(){
    User user = new User();
    
    user = null;
}
  • 上边没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。
public StringBuffer say(String name){
    StringBuffer sb = new StringBuffer();
    sb.append(name);

    return sb;   /**  会发生逃逸  */
    return sb.toString(); // 因为StringBuffer是重写了toString,重新new了一个对象,所以返回的对象并不是sb本身。

}