1 堆的概述

一个进程代表一个jvm实例,一个进程只有唯一的一个堆,堆也是java内存管理的核心区域。所有的对象实例和数组都应该在运行时分配在堆上。

java7以前,堆逻辑上分为新生区、养老区和永久区,java8以后分为新生区、养老区和原空间。我们常说的堆空间的大小指的时新生区和养老区的总和。

2 年轻代和老年代

存储在JVM中的java对象可以划分为两类:一类时生命周期短暂的瞬时对象,这类对象的创建和消亡都非常迅速。另外一类的对象生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。

图1

如图1所示java堆区进一步细分的话,可以划分为年轻代和老年代。

年轻代又可以eden空间(对象最初创建的地方)和survivor0和survivor1区(有时也叫from,to区)(如果GC的时候还存在,放于新生者区)

默认情况下(可以更改):

年轻代:老年代=1:2

eden:from:to = 8:1:1

3 图解对象分配过程

 

 

图2 

 

1)new的对象先放在伊甸园区。

2)当伊甸园区填满的时候,程序又需要创建对象,jvm的垃圾回收器将伊甸园进行垃圾回收,将伊甸园区中的不再被其他对象引用的对象进行销毁,再加载新的对象放到伊甸园区。

3)然后将伊甸园区剩下的对象放到幸存者0区。

4)再次触发垃圾回收,此时上次幸存下来的放到0区的,如果没有被回收,就会放到幸存者1区。0区和1区会交换(没有数据的为0区)

5)如果再次经历垃圾回收,此时重新放回幸存者0区,接着再去幸存者1区。

6)默认情况下当年龄到达15的时候会区养老区

7)养老区内存不足时,再次触发major gc,进行养老区的内存清理

8)如果养老去区执行了major gc依旧无法进行对象的保存,就会产生oom异常

总结:

1)针对幸存者s0和s1区的总结:复制之后有交换,哪个是空哪个就是to。

2)垃圾回收:频繁再新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。

特殊情况:

1)如果新生代放不下就直接放到老年代,放不下就fgc,还是不够就oom了

2)如果幸存者区放不下,就直接到老年代

3)如果幸存者区满了,也直接晋升老年代

4 Minor GC、Major GC与Full GC

新生代收集:Minor GC/Young GC:只是新生代的垃圾收集

老年代收集:Major GC/ Old GC:只是老年代的垃圾收集

混合收集(Mixed GC):收集整个新生代和部分老年代的垃圾收集。目前只有G1 GC会有这种行为

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

 

年轻代(Minor GC)触发机制:

1)当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC

2)因为java对象大多具有朝生夕灭,所以Minor GC 非常频繁,一般回收速度也比较快。

3)Minor GC会引发STW,暂停其他用户线程,等待垃圾回收结束,用户线程才恢复运行。

老年代(Major GC、Full GC)的触发机制:

1)指发生在老年代的 GC,对象从老年代消失的时候,就可以说Major GC或者Full GC发声了。

2)出现Major GC,经常 伴随着至少一次Minor GC(不绝对)。也就是老年代空间不足的时候,会先尝试触发MInor GC,空间还不足的时候才会触发Major GC。

3)Major GC的速度一半比Minor GC慢十倍,STW的时间更长。

4)如果Major GC后,内存话不足,就oom了

Full GC的触发条件:

1)调用System.gc()时,系统建议执行Full GC,但是不必然执行。

2)老年代空间不足

3)方法区空间不足

4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

5)由eden区,s0向s1区复制的时候,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象的大小。

堆空间的分代思想:

经研究表面,不同对象的生命周期不同。70%-99的对象都是临时对象。其实不分代完全可以,分代的唯一理由就是为了优化GC性能。

5 内存分配策略:

1)优先分配到Eden

2)大对象直接分配到老年代

3)长期存活的对象分配到老年代

4)动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Surivor空间的一半,年龄大于或等于该年龄的对象可用直接进入老年代,无须等到需要等到的年龄

6 为对象分配内存

为什么要有TLAB?(属于线程的缓冲区)

堆区时线程共享区域,任何线程都可以访问到堆区中的共享数据。由于对象实例创建在JVM中非常频繁,因此在并发环境下从堆区划分内存空间是线程不安全的。为了避免多个线程作同一地址,需要使用加锁等机制,进而影响分配速度。

从内存模型而不是垃圾回收的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。

多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种分配方式称之为快速分配策略。

7 小结堆空间的参数设置

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于,此次Minor GC是安全的,如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。如果为true,那么就继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试一次Minor GC,如果小于,则改为一次Full GC。如果为flase,则改为一次Full GC。

在JDK6 Update24之后,HandlePromotionFailure不会再影响虚拟机的分配担保策略,默认为true。规则则为只要老年代的连续空间大于新生代对象总大小或者历次晋升老年代的平均大小就会进行Minor GC,否则进行Full GC。

8 堆是分配对象存储的唯一选择吗?

随着JIT编译期的发展于逃逸技术逐渐成熟,栈上分配、标量替换技术将会导致一些微妙的变化,所有对象都分配到堆上也渐渐变得不那么绝对了。如果经过逃逸分析之后,一个对象并没有逃逸出方法,那么就可能被优化成栈上分配。

逃逸分析概述:

这是一种可以有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。没有发声逃逸的对象,则可以分配到栈上。

1)方法返回所创建的对象,发生逃逸。

2)创建的对象为成员变量赋值,发生逃逸。

3)引用成员变量的值,发生逃逸

4)对象作用域仅在当前方法有效,没有发生逃逸。

逃逸分析:代码优化

1)栈上分配

如果一个对象并没有发生逃逸,就可能被分配到栈上。这样可以避免垃圾回收,在执行效率上避免垃圾回收的STW的时间,有很大提升。

2)同步省略

在动态编译同步代码块的时候,JIT编辑器可以借助逃逸分析来判断同步块使用的锁对象是不是只被一个线程访问而没有发布到其他的线程,如果没有,就会取消这个锁,提高并发性,也叫做锁消除。

3)分类对象或标量替换

对象可以看作聚合量,可以分为其他聚合量和标量,如果一个对象不被外界访问,就会把对象拆解成若干个成员变量来代替,这个过程就叫做标量替换。

小结:逃逸分析并不成熟,自身的分析也是相对耗时的过程,可以知道所有对象都是创建在堆上。至于标量替换,我认为是并没有在栈上创建对象,而是将其拆解为标量放置在栈,因此栈上也没有实际创建的对象。