jvm之堆(详解)

1.堆的核心概述:一个jvm实例只存在一个堆内存,堆也是Java内存管理的核心区域,java堆区在jvm启动的时候即被创建,其空间大小也就被确定了,是jvm管理的最大一个内存空间,堆内存的大小是可以调节的,在《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,所有的线程共享Java堆,在这里还可以划分线程私有的缓存区(TLAB)。

数组和对象可能永远不会在存储在栈上,因为栈帧中保存引用,这个引用指向对象在堆中的位置。在方法执行结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

内存细分:现代垃圾收集器大部分基于分代收集理论设计,堆空间细分为在java7及之前的内存逻辑上分为:新生区+用老区+永久区,在java8及之后内存逻辑上分为三个部分:新生区+养老区+元空间。

堆空间内部结构图

3.设置堆内存大小与OOM:java堆内存存储java对象实例,那么堆的大小在启动时就设定好了,大家可以通过选项“-Xmx”和“-Xms”来进行设置。

①“-Xms”用于表示堆的起始内存大小,等价与-XX:InitialHeapSize

②“-Xmx”用于表示堆内存最大内存,等价于-XX:MaxHeapSize

一旦堆内存的大小超过“-Xmx”所指定的内存时,将会抛出OutOfMemoryError异常。

通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后,不需要重新分隔计算堆区的大小,从而挺高性能(一句话说就是没有堆区动态扩展堆区的大小减少用户进程等待时间,从而挺高性能)。

举例:

public class Test3 {
    public static void main(String[] args) {
        ArrayList<Picture> objects = new ArrayList<>();
        while (true){
            objects.add(new Picture(new Random().nextInt(1024*1024)));
        }
    }
}
class Picture{
    int size;

    public Picture(int size) {
        this.size = size;

    }

 上面就是一个对空间内存异常的例子,首先是编译代码,然后设置堆内存大小参数如下图

4.新生代和老年代:存储在jvm中的java对象可以划分为两类,一种是生命周期比较短的瞬时对象,这类对象的创建和消亡都非常迅速,另一种是生命周期非常长,在某些的情况下还能够与jvm生命周期保存一致。

Java堆内存一步细分的话,可以划分为年轻代和老年代,其中年轻代又可划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫from区、to区)。

配置新生代与老年代在堆的结构的占比。

①默认-XX:NewRation=2,表示新生代占一个,老年代占2,新生占整个堆的1/3。

②可以修改-XX:NewRation=4,表示新生代占1老年代占4,新生代占整个堆的1/5。

在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1,当然开发人员可以通过选项“-XX:SurvivorRation”调整这个空间比例。比如-XX:SurvivorRation=8.

几乎所有的java对象都是在Eden区被new出来的,绝大部分的java对象的销毁都在新生区进行了,IBM公司的专门研究表明,新生代中的80%的对象都是“朝生夕死”的,可以使用选项“-Xmn”设置新生代最大内存大小。

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

①new的对象先放在伊甸园区,此区有大小限制。

②当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放在伊甸园区。

③然后将伊甸园的剩余的对象移动到幸存者0区。

④如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区

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

⑥啥时候能去养老区呢?可以设置此时。默认时15次。(可以设置参数:-XX:MaxTenuringThreshold=<N>进行设置。

⑦在养区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC, 进行养老区内存清理。

⑧若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。

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

6.常用调优工具:JDK命令行、Eclipse:Memory Analyzer Tool、Jconsele、VisualVM、Jprofiler、Java Flight Recorder、GCViewer、GC Easy。

7.Minor GC、Major GC与Full GC:针对HotSpot VM的实现,它里面的GC按照回收区域有分为两种类型:一种是部分收集,一种是整堆收集。

部分收集:不是完整收集整个Java堆的垃圾收集,其中又分为:①新生代收集:只是新生代的垃圾收集。②老年代收集:只是老年代的收集。目前只有CMS GC会有收集老年的行为。注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨老年代回收还是整堆回收。

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

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

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

①当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Edendai满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存)。

②因为java对象大多数具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较块。这一定义既清晰又易于理解。

③。Minor GC会引发STW,暂替其他用户的线程,等垃圾回收结束,用户线程 才恢复运行。

老年代GC(Major GC/Full GC)触发机制:只发生在老年代的GC,对象从老年代消失时,我们说“Major”或“Full GC”发生了。出现了Major GC,经常会伴随至少一次的Minor GC,也就是在老年代空间不足时,会尝试触发Minor GC.如果空间还不足,则触发Major GC。

Major GC的速度一般会比Minor GC慢10以上,STW的时间更长。如果Major GC后,内存还不足,就会报OOM了。

8.堆空间分代的思想:经研究,不同对象的生命周期不同,70%-99%的对象时临时对象。新生代:有Eden、两块大小相同的Survivor构成,to总为空。老年代:存放新生代中经历多次GC仍然存放的对象。

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

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

①优先分配到Eden。

②大对象直接分配到老年区,尽量避免程序中出现过多的大对象。

③长期存活的对象分配到老年代。

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

⑤空间担保,-XX:HandlePromotionFailure.

10.为对象分配内存:TLAB

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。

由于对象实例的创建再JVM非常频繁,因此再并发环境下从堆区划分内存空间是线程不安全的,为了避免多个线程操作同一个地址,需要使用加锁机制,进而影响分配速度。

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

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

据我所知所有OpenJDK衍生出来的JVM都提供了TLAB。

 尽管不是所有的对象实例都能够在TLAB成功分配内存,但JVM确实是将TALAB作为内存分配的首选。

在程序中,开发人员可以通过选项”-XX:UserTLAB“设置是否开启TLAB空间。

默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项”-XX:TLABWasteTargetPercent“设置TLAB空间所占用Eden空间的百分比大小。

一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配对象内存。

11.对空间参数设置

-XX:PrintFlagsInitial:查看所有的参数默认值。

-XX:PrintFlagsFinal:查看所有参数最终值(可能会存在修改,不再是初始值)

-Xms:初始堆空间内存(默认为物理内存的1/64)

-Xmx:设置最大堆空间内存(默认为物理内存的1/4).

-Xmn:设置新生代的大小。(初始值及最大值)

-XX:NewRation:配置新生代与老年代在堆结构的占比。

-XX:SurvivorRation:设置新生代中Eden和s0/s1的比例。

-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄。

-XX:PrintGCDetails:输出详细的GC处理日志,打印简要信息:①-XX:PrintGC  ②-verbose:gc

-XX:HandlePromotionFailure:是否设置空间分配担保。

12.堆是分配对象存储的唯一选择嘛?

随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆也就逐渐变得不那么绝对了。

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识,但是,有一中特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的对外存储技术。

此外,基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH技术实现off -heap,将生命周期较长的java对象从堆中移至堆外,并且GC不能GCIH内部的对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

逃逸分析的基本行为就是分析对象动态作用域:

①当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。

②当一个对象在方法被定义后,它被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其它地方中。

没有发生逃逸的对象,则分配到栈上,随着方法执行的结束,栈空间就被移除。

选项"-XX:+DoEscapeAnlysis"显式开启逃逸分析,通过选项"-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果。

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

使用逃逸分析,编译器可以对代码做如下优化:

①栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,将可能是栈分配的候选,而不是堆分配。

②同步省略。如果一个对象被发现只能从一个线程访问到,那么对于这个对象的操作可以不考虑同步。

③分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在cpu寄存器中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值