JVM学习之堆

274 阅读12分钟

一、堆的核心概念

1、进程私有or共有?

  • 方法区、堆都是进程私有。我们在启动一个main方法时,通过工具可以观察到,每个main方法对应的进程都会对应各自的方法区和堆。或者可以这样总结:一个JVM实例只存在一个堆内存。
  • 这边回顾下,内存数据区的5个划分中那几个是线程私有的:程序计数器和栈

2、内存中的地位?

  • 堆在JVM启动时就被创建,同时大小也是固定的(我理解就是不会运行时自动拓展)
  • 堆可以是物理内存空间上不连续,但是在逻辑上应该被视为连续的

3、是否都是线程共享的?

  • 一般我们认为堆并不都是线程共享的,例外是TLAB空间,这类空间是线程私有的。

4、对象实例和数组是否都是在堆上分配?

  • 一般我们认为大多数的对象实例和数组 是在堆上分配内存空间的。
  • 实际在特殊情况下,栈也可以存储对象实例,这个后面说

5、方法结束时,堆中的对象是否会被马上移除?

  • 堆中的对象被移除,一般都是由GC决定。经过GC时判断标记后,才会被回收。这个时候和方法结束没有必然关系。

6、堆是否有GC?

  • 堆是GC发生的主要场所。
  • 顺带回顾下:栈不会发生GC,但是会有OOM。

7、堆的内存细分类

7.1分代理论
  • 现代的垃圾收集器大部分都是基于分代收集理论设计。
  • 逻辑上堆的细分类为:
Java 7及之前版本Java 8及之后版本
Eden区(新生代)Eden区(新生代)
Survivor0区(新生代)Survivor0区(新生代)
Survivor1区(新生代)Survivor1区(新生代)
老年代老年代
永久代(方法区)元空间(方法区)

二、设置堆内存大小与OOM

1、设置堆空间大小的指令?

  • -Xms:设置堆(新生代、老年代)的起始内存大小
  • -Xmx:设置堆(新生代、老年代)的最大内存

2、有没有默认大小?

  • 有默认大小,常常是这样的
    • -Xms:最大可用系统内存/64
    • -Xmx:最大可用系统内存/4

3、实际中如何设置?

  • 实际开发中,我们把这两个参数设置成一样的值。那么为啥呢?主要是堆内存不够用且小于最大内存时,会不断的重新申请分配内存,这种动态的操作是会有系统开销的。

4、感受下堆内存的实际使用情况

  • 这边我写了一个简单的代码并设置了运行时的jvm的参数
//jvm参数:-Xms600m -Xmx600m
public class HeapSpaceInit {
    public static void main(String[] args) {
        long totalMemory = Runtime.getRuntime().totalMemory();
        long maxMemory = Runtime.getRuntime().maxMemory();
        //获得Xms的值
        System.out.println(totalMemory/1024/1024);
        //获得Xmx的值
        System.out.println(maxMemory/1024/1024);

        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
上面的输出结果为:
575
575
  • 与实际设置的jvm参数不相等?分析过程如下:
    • 通过命令查看gc的情况:(1)使用jps 查看进程。(2)使用jstat -gc 进程id查看gc情况。

  • 计算过程如下:
    • 理论上的堆大小=Eden区大小+S0区大小+S1区大小+老年代大小=EC+S0C+S1C+OC=(153600+25600+25600+409600)/1024=600
    • 实际上堆的大小=理论值-S0区(或S1区)大小。为什么这么算?因为在实际GC中,S0,S1只有一个区域存放对象实例。这个后续会说.....

三、年轻代与老年代

1、分代逻辑划分堆

  • 逻辑上划分成:新生代和老年代。新生代进一步划分为:Eden区、Survivor0区、Survivor1区(也叫from区、to区)

2、设置新生代和老年代的大小及比例

  • 回顾下前面说过的设置堆空间大小的命令:-Xms500m(设置堆的初始大小),-Xmx(设置堆的最大内存)
  • 新的命令来设置新生代和老年代的大小:-XX:NewRatio=2=老年代/新生代
  • 新生代中的各区域大小的默认比例为:Eden:S0:S1=8:1:1;但是实际上在运行时,并不是按照这个比例进行内存分配。主要是有自适应的内存分配策略。
  • 可以通过命令来开启或者关闭自适应的内存分配策略:
    • 开启自适应内存分配策略:-XX:+UseAdaptiveSizePolicy
    • 关闭自适应内存分配策略:-XX:-UseAdaptiveSizePolicy
  • 我们在上面使用关闭自适应内存分配策略后,哈哈哈,任然不是8:1:1;不好i用!!!!!
  • 那我们要怎么调整呢?可以通过下面这个参数进行设置:-XX:SurvivorRatio=8;表示Eden:S0:S1=8:1:1

3、简单介绍新生代和老年代

  • 几乎所有的对象都是在Eden区new出来的,绝大部分的对象都是在新生代就消亡了
  • 设置新生代的大小:-Xmn

四、图解对象分配过程

1、一般情况的分配情况

  • 对象分配是一件严谨的时,不仅需要考虑在哪里分配,分配多少的问题,也要考虑GC的问题
  • 下面将通过几个图来分析对象在堆中的生命周期

上面这个图描述了新生代和老年代中对象的分配和回收的一般的过程。
  • 图一:初始时Eden、S0、S1都没有对象。然后5个对象在Eden区创建出来,此时Eden区内存使用满了,需要进行Minor GC。其中有3个对象不在使用被系统回收,有2个对象继续被使用被移动到S0区。最终态为:Eden有0个对象,S0区有2个对象(标记经过1次MInor GC),S1区有0个对象。
  • 图二:初始时Eden有0个对象,S0区有2个对象,S1区有0个对象。然后系统在Eden区创建对象,创建5个对象,内存满了之后进行Minor GC。其中有4个对象不在使用被系统回收,有1个对象继续被使用被移动到S1区。同时系统会将S0中的初始的2个对象复制到S1区中。最终态为:Eden有0个对象,S0区有0个对象,S1区有3个对象(其中2个对象标记为经过2次MInor GC,剩下1个标记为经过1次Minor GC)。这里伴随着from区和to区的转换
  • 图三:经过不断的上面的重复的过程发展到图三的状态。初始时Eden有0个对象,S0区有0个对象,S1区有3个对象(其中2个经过15次Minor GC)。然后系统在Eden区创建对象,创建5个对象,内存满了之后进行Minor GC。其中有3个对象不在使用被系统回收,有2个对象继续被使用被移动到S0区。S1中的2个对象由于达到了阈值,此时将会移动到Old区,剩下1个对象被移动到S0区。最终态为:Eden有0个对象,S0区有3个对象,S1区有0个对象。老年代中保存着2个对象。
  • 从上面的过程中可以看出,在Minor GC发生时,S0\S1会发生相互复制,以保持始终有一个区是空的,这个也就是from区和to区的转换。

2、特殊情况的分配情况

  • 特殊情况的分配情况由下图说明

五、Minor GC、Major GC、Full GC

1、概念

  • HotSpot虚拟机GC回收的区域可以进行分类:部分GC、整堆GC

  • 部分GC:

    • Minor GC:发生在新生代
    • Major GC:发生在老年代
    • Mixed GC:对整个新生代和部分老年代进行GC
  • 整堆GC:

    • Full GC:对堆和方法区进行GC

2、Minor GC

  • 案发现场是新生代。杀人动机是Eden区空间满了,Survivor区满了并不会引起GC
  • 系统线程进行GC时,会暂停用户线程的工作,现象叫做STW。GC完成之后,用户线程才会重新开始工作。

3、Major GC

  • 案发现场是老年代。杀人动机是老年代空间满了。
  • 出现Major GC时,常常会伴随着Minor GC。也可以这样说:在老年代空间不足时,会先尝试去触发Minor GC。如果空间还不足,则触发Major Gc
  • Major GC的速度很慢,一般是Minor GC的10倍以上
  • 如果进行过Major GC后,还是没有足够的空间,那么就会出现OOM。

4、Full GC

  • 案发现场是整个堆和方法区
  • 会发生Full GC的场景:
    • 调用System.gc()
    • 老年代空间不足
    • 新生代空间不足
    • 经过Minor GC后到达老年代的对象平均大小大于老年代的可用内存大小,这种极大概念会发生对象的移动
    • from区和to区进行对象复制时,to区空间不足,移动到老年代时,老年代空间也不足时

六、堆空间分代思想

1、为什么需要进行分代?

  • 其实不分代也是可以的。进行分代的理由就是优化GC性能

  • 进行进一步的区分对象....

七、内存分配策略

说一下原则:

  • 优先分配到Eden区
  • 大对象直接分配到老年代。常见的大数组,长字符串等。
  • 长期存活的对象会慢慢移动到老年代
  • 如果survivor区相同年龄的对象大小总和大于survivor空闲空间的一半,这个时候进行from to区的相互拷贝时,效果其实是不好的,这个时候可以将相同年龄的对象进入老年代

八、为对象分配内存:TLAB

1、为什么会有TLAB?

  • 堆是线程共享的区域,线程可以访问堆上的共享数据。同时,对象实例在JVM中创建的很频繁,这就可能会引起多线程安全问题。为了解决这种情况,可以通过枷锁去保证,但是加锁会影响效率

2、TLAB是啥?

  • 从内存模型上说:系统为每个线程在Eden去分配各自的小的空间,用来为每个线程提供缓存区域

3、一些说明

  • 不是所有的对象都可以在TLAB上分配到内存。但是,jvm始终将TLAB作为对象分配空间的首选方案

4、我们new一个对象的前后操作有那些?

  1. 将java前端编译成class文件
  2. 对class文件进行文件读取,进行类加载,步骤包含:加载、链接(验证、准备、解析)、初始化
  3. 当完成类加载后,可以通过new关键字去实例化一个对象
  4. 实例化对象时,首先尝试为对象在TLAB分配内存空间,如果有足够的空间,就进行实例化的下一步;如果没有足够的空间,就在Eden区进行内存分配;如果还不够,就尝试在老年代进行分配

九、小结堆空间的参数设置

命令描述
-XX:+PrintFlagInitial查看jvm参数的默认值
-XX:+PrintFlagFinal查看所有参数的最终值(中间过程可能会修改默认值)
jsp获得进程号;jinfo -flag survivorRatio 进程号查看某个参数的具体值
-Xms堆空间默认内存大小
-Xmx堆空间的最大内存大小
-Xmn新生代的内存大小
-XX:NewRatio(默认2)老年代:新生代
-XX:SurvivorRatio(默认8)Eden:S0(或S1)
-XX:MaxTenuringThreshold新生代存活的阈值
-XX:PrintGCDetails打印GC细节
-XX:HandlePromotionFailure是否设置空间分配担保
  • -XX:HandlePromotionFailure:在发生Minor GC之前,jvm会去比较老年代中连续的可用空间大小是否大于当前新生代中的所有对象的内存总和
    • 如果大于,则进行Minor GC,且能保证安全
    • 如果小于,则会去检查-XX:HandlePromotionFailure这个参数值。如果参数为true,会继续检查历次晋升到老年代的对象的平均大小是否大于老年代最大空间:如果大于,则会存在较大的风险,需要进行一次Full GC;如果小于,则尝试进行一次Minor GC,但是这次GC任然是由风险的。如果参数为false,直接进行Full GC。

十、堆是分配对象存储的唯一选择吗

1、结论?

是也不是!

2、非特殊情况

一般我们认为对象是在堆上分配内存的。但是也有例外的情况。

3、例外的情况

栈上分配,这个好理解就是将对象在栈上存储

4、什么时候栈上分配?

经过逃逸分析后,对象并没有逃逸出方法外使用,那么就可能在栈上进行分配。

5、什么是逃逸分析?

  • 简单来说就是分析对象的作用域。
  • 当在方法中new出来的对象,只在方法内被使用,则认为没有发生逃逸
  • 当在方法中new出来的对象,被外部方法引用,则认为发生逃逸
  • 特别主要这边观察的是:我们通过new得到的对象实体

6、思考

从内存模型和GC这两方面思考,对象频繁在堆上创建,可能会引起频繁的GC,简单的思考后,是否可以多的使用栈上分配的规则。这个应该是要肯定的。那对我们编码的启发是什么?是多使用局部变量...

7、同步省略

  • 前面说到过堆上的内存是线程共享的,也说过TLAB,jvm为了解决堆空间的多线程同步问题特别弄出TLAB。
  • 在动态编译同步块的时候,系统开销是很大的。JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只被一个线程使用。如果是,则可以进行锁消除。