JVM运行时数据区---堆(对象分配过程)

堆—对象分配过程

概念

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

下面几点说明:

  • new的对象先放伊甸园区,此区有大小限制;
  • 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器对伊甸园区进行垃圾回(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区;
  • 然后将伊甸园中的剩余对象移动到幸存者0区;
  • 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区(0区、1区互相换);
  • 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
  • 啥时候能去养老区呢?可以设置次数。默认是15次;
  • 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理;
  • 若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。

可以设置参数(次数):-Xx:MaxTenuringThreshold= N进行设置。

图解过程

  1. 我们创建的对象,一般都是存放在Eden区的,当Eden区满了后,就会触发GC操作,一般被称为 YGC / Minor GC操作;
    在这里插入图片描述

  2. 当我们进行一次垃圾收集后,红色的将会被回收,而绿色的还会被占用着,存放在S0(Survivor From)区。同时给每个对象设置了一个年龄计数器,一次回收后就是1;

  3. 同时Eden区继续存放对象,当Eden区再次存满的时候,又会触发一个MinorGC操作,此时GC将会把 Eden和Survivor From中的对象 进行一次收集,把存活的对象放到 Survivor To区,同时让年龄 + 1;
    在这里插入图片描述

  4. 我们继续不断的进行对象生成和垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,即将年轻代中的对象晋升到老年代中;
    在这里插入图片描述
    幸存区区满了后?

  • 特别注意,在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发 MinorGC 操作;
  • 如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代。

举例:以当兵为例,正常人的晋升可能是 : 新兵 -> 班长 -> 排长 -> 连长。

但是也有可能有些人因为做了非常大的贡献,直接从 新兵 -> 排长。

扫描二维码关注公众号,回复: 13026815 查看本文章

对象分配的特殊情况
在这里插入图片描述
代码演示对象分配过程

示例程序:不断的创建大对象添加到 list 中:

public class HeapInstanceTest {
    
    
    byte [] buffer = new byte[new Random().nextInt(1024 * 200)];
    public static void main(String[] args) throws InterruptedException {
    
    
        ArrayList<HeapInstanceTest> list = new ArrayList<>();
        while (true) {
    
    
            list.add(new HeapInstanceTest());
            Thread.sleep(10);
        }
    }
}

然后设置JVM参数:

-Xms600m -Xmx600m

在这里插入图片描述
之后打开VisualVM工具,通过执行上面代码,通过VisualGC进行动态化查看:
在这里插入图片描述
最终,在老年代和新生代都满了,就出现OOM。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.heu.heap.HeapInstanceTest.<init>(HeapInstanceTest.java:13)
	at com.heu.heap.HeapInstanceTest.main(HeapInstanceTest.java:17)

总结

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to(s0,s1不固定);
  • 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不在永久代和元空间进行收集;
  • 新生代采用复制算法的目的:是为了减少内碎片。

Minor GC,MajorGC、Full GC

  • Minor GC:新生代的GC

  • Major GC:老年代的GC

  • Full GC:整堆收集,收集整个Java堆和方法区的垃圾收集

  • 我们都知道,JVM的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW(stop the word)的问题,而 Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上。

  • JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。针对 Hotspot VM 的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)。

部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:

  • 新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集。
  • 老年代收集(MajorGC/o1dGC):只是老年代的圾收集。
    - 目前,只有CMSGC会有单独收集老年代的行为;
    - 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。
    - 目前,只有G1 GC会有这种行为。

整堆收集(FullGC):收集整个java堆和方法区的垃圾收集。

Minor GC

  • 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC,(每次Minor GC会清理年轻代的内存);
  • 因为Java对象大多都具备 朝生夕灭 的特性,所以Minor GC非常频繁,一般回收速度也比较快;
  • Minor GC会引发STW(stop the word),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
    在这里插入图片描述

Major GC

Majoy GC指发生在老年代的GC,对象从老年代消失时,就说 “Major Gc” 或 “Full GC” 发生了。

  • 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程),也就是在老年代空间不足时,会先尝试触发 Minor GC。如果之后空间还不足,则触发Major GC;
  • Major GC的速度一般会比MinorGc慢10倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了。

Full GC

触发 Full GC 执行的情况有如下五种:

  1. 调用System.gc()时,系统建议执行Full GC,但是不必然执行;
  2. 老年代空间不足;
  3. 方法区空间不足;
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
  5. 由Eden区、survivor spacee(From Space)区向survivor spacel(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小(即年轻代、老年代的内存大小装不下该对象)。

说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些。

GC 举例

编写一个OOM的异常,不断的创建字符串示例:

public class GCTest {
    
    
    public static void main(String[] args) {
    
    
        int i = 0;
        try {
    
    
            List<String> list = new ArrayList<>();
            String a = "mogu blog";
            while(true) {
    
    
                list.add(a);
                a = a + a;
                i++;
            }
        }catch (Exception e) {
    
    
            e.getStackTrace();
        }
    }
}

设置JVM启动参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails

打印出的日志:

[GC (Allocation Failure) [PSYoungGen: 2038K->500K(2560K)] 2038K->797K(9728K), 0.3532002 secs] [Times: user=0.01 sys=0.00, real=0.36 secs] 
[GC (Allocation Failure) [PSYoungGen: 2108K->480K(2560K)] 2405K->1565K(9728K), 0.0014069 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2288K->0K(2560K)] [ParOldGen: 6845K->5281K(7168K)] 9133K->5281K(9728K), [Metaspace: 3482K->3482K(1056768K)], 0.0058675 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 5281K->5281K(9728K), 0.0002857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 5281K->5263K(7168K)] 5281K->5263K(9728K), [Metaspace: 3482K->3482K(1056768K)], 0.0058564 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2560K, used 60K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0f138,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 5263K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 73% used [0x00000000ff600000,0x00000000ffb23cf0,0x00000000ffd00000)
 Metaspace       used 3514K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 388K, capacity 390K, committed 512K, reserved 1048576K
  
  Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.<init>(String.java:207)
	at java.lang.StringBuilder.toString(StringBuilder.java:407)
	at com.heu.heap.GCTest.main(GCTest.java:20)
[GC (Allocation Failure) [PSYoungGen: 2038K->500K(2560K)] 2038K->797K(9728K), 0.3532002 secs] 
  • [PSYoungGen: 2038K->500K(2560K)]:年轻代总空间为 2560K ,当前占用 2038K ,经过垃圾回收后剩余500K;
  • 2038K->797K(9728K):堆内存总空间为 9728K ,当前占用2038K ,经过垃圾回收后剩余797K。

触发OOM的时候,一定是进行了一次Full GC,因为只有在老年代空间不足时候,才会爆出OOM异常。

堆空间分代思想

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

经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。

  • 新生代:有Eden、两块大小相同的survivor(又称为from/to或s0/s1)构成,to总为空。
  • 老年代:存放新生代中经历多次GC仍然存活的对象。

其实不分代完全可以,分代的唯一理由就是优化GC性能。

  • 如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。(性能低)
  • 而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。(多回收新生代,少回收老年代,性能会提高很多)

对象内存分配策略

  1. 如果对象在Eden区出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。
  2. 对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代。
  3. 对象晋升老年代的年龄阀值,可以通过选项**-XX:MaxTenuringThreshold**来设置。

针对不同年龄段的对象分配原则如下所示:

  1. 优先分配到Eden:开发中比较长的字符串或者数组,会直接存在老年代,但是因为新创建的对象都是朝生夕死的,所以这个大对象可能也很快被回收,但是因为老年代触发Major GC的次数比 Minor GC要更少,因此可能回收起来就会比较慢。
  2. 大对象直接分配到老年代:尽量避免程序中出现过多的大对象。
  3. 长期存活的对象分配到老年代。
  4. 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  5. 空间分配担保: -XX:HandlePromotionFailure 。

猜你喜欢

转载自blog.csdn.net/qq_33626996/article/details/113954578