jvm-堆详解

1.堆概述

在这里插入图片描述
方法区和堆是线程共享的,是每个进程唯一的,一个java程序对应一个进程,一个进程对应jvm实例,一个jvm实例拥有一个单例的运行时数据区,堆是java内存管理的核心区域

  • 堆的大小可以被调节,通过-Xms和-Xmx参数调节
  • 堆在jvm启动时即创建,空间大小也就随之确定
  • 所有线程共享堆,可以设置线程私有缓冲区TLAB(文章稍后介绍)
  • 堆在物理上不连续,在逻辑上连续,方法区是堆的逻辑上的一部分

2.堆内存细分

在这里插入图片描述
java堆是垃圾收集器管理的内存区域,故也叫GC堆,从内存回收的角度看,现代垃圾收集器大部分是基于分代收集理论设计的,所以java堆中会经常出现如图所示的内存划分(新生代包括伊甸区和幸存区),根据不同jdk版本(以jdk8划分),堆内存划分也不同:
在这里插入图片描述

2.1设置堆内存大小

  • -Xms:堆起始内存,相当于-XX:InitialHeapSpace
  • -Xmx:堆最大内存,相当于-XX:MaxHeapSpace

一般情况下将这两个参数的值设为相同,当堆区的内存超出设置的最大内存时,将会抛出OutOfMemoryError异常,jvm在计算堆内存的方式与我们不同,比如我们计算堆内存是新生代+老年代,而jvm则是不同,如图所示,堆内存为600m,jvm计算出来只有575m在这里插入图片描述

2.2新生代和老年代

在上面我们提到了,新生代分为Eden,Survivor(S0,S1/From Survivor,To Survivor)区,为什么这样划分呢?原因是java中的对象生命周期导致:

  • 对于生命周期较短的对象,创建和消亡都非常迅速
  • 某些对象生命周期很长,甚至可以与jvm生命周期一致

新生代与老年代在堆中内存占比: -XX:NewRation=2,表示新生代占1,老年代占2
关于新生代中Eden和Survivor区的内存划分::-XX:Survivor=8,默认情况下Eden和sos1的划分是8:1:1
以上两个参数一般不需要调整,这里需要说明的是:

  • 几乎所有的对象都是在Eden区被new出来的,且绝大部分的对象的销毁都在新生代中进行
  • 在养老区,当内存不足时,会触发Major GC,对老年代的对象进行垃圾回收,如果还是内存不足,则会产生OOM异常

3.对象分配过程

  • 1.对象优先在Eden中分配
  • 2.当Eden空间填满时,程序还要继续创建对象,这是会触发YGC/Minor GC对新生代(Eden和Survivor)进行垃圾回收,将Eden中没有引用的对象销毁,再创建新的对象到Eden中
  • 3.在Minor GC回收后的Eden中,没有被销毁的对象移动到Survivor0
  • 4.若Eden区满了再次触发垃圾回收,并且垃圾回收过后,此前在Survivor0中仍然存在的对象会被移到Survivor1中,经历多次移动,达到jvm规定的阈值(15次),就会被移动到养老区了,至于阈值,可以通过:-XX:MaxTenuringThreshold=< N >进行设置

在这里插入图片描述
注意:Eden满了会触发Minor GC,而Survivor区满了不会触发,且Minor GC会对新生代进行垃圾回收,且垃圾回收在新生代频繁进行,很少在养老区搜集,几乎不再方法区搜集

3.1对象分配特殊情况

在创建对象的时候,会遇到一些特殊情况,比如在创建一个非常大的对象的时候,因为是在Eden中创建,当对象的大小大于Eden区的大小,会触发垃圾回收,在回收后仍然无法存放对象,一般会去老年区尝试存放,老年区比Eden内存空间大,如果老年区也无法存放就会抛出OOM异常,下面这张图清晰的展示了遇到特殊情况时JVM的处理过程
在这里插入图片描述

4.几种垃圾收集比较

在jvm进行垃圾回收时,会根据内存区域的不同进行垃圾回收,以HotSpot VM为例,它的GC按照回收区域划分为部分收集(Partial gc)和整堆收集(Full GC)

  • 部分收集
    • 新生代收集:Minor GC/Young GC,新生代的垃圾收集
    • 老年代收集:Major GC/Old GC,老年代的垃圾收集,只有CMS GC会有单独收集老年代的行为
    • 混合收集(Mixed GC):收集整个新生代和部分老年代的垃圾收集,目前只有G1 GC有这种行为
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集

下面分别介绍这几种垃圾收集机制,这里只是简单介绍这些机制的用处,并不会深入解析

4.1Minor GC

  • 当年轻代空间(Eden)空间不足时会触发Minor GC,Survivor满不会触发Minor GC
  • 因为Java对象大多都具备朝生夕死的特征,所以Minor GC非常频繁,所以垃圾回收主要发生在新生代
  • Minor GC会引发STW,暂停其他用户线程,当垃圾回收结束,用户线程才恢复运行

4.2Major GC

即老年代GC触发机制、

  • 当对象从老年代消失,我们就会说Major GC或Full GC
  • 出现Major GC,经常会伴随至少一次的Minor GC(当老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,就会触发Major GC)
  • Major GC的速度一般比Minor GC慢10倍以上,STW时间更长
  • Major GC之后内存还不足,就会OOM

4.3Full GC

当出现以下几种情况会触发Full GC

  • 调用System.gc()
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden,S0(From区)向S1(To区)复制时,对象大小大于To区可用内存,则把对象转入老年代,且老年代可用空间大小小于该对象大小

5.为什么要分代

前面我们说过,不同对象的生命周期不同,且70%-99%的对象都是临时对象,如果不分代,就相当于将所有对象放在一块管理,这样对GC性能影响太大,而分代的唯一理由就是优化GC性能,新生代存储的对象大多都是朝生夕死,这个区域也是GC最频繁的地区,类似数据库连接池对象则放在老年代,这样的分代管理就大大优化GC性能

6.本地线程缓冲TLAB

6.1为什么要有TLAB(Thread Local Allocation Buffer)

我们都知道堆是线程共享的,并且对象在虚拟机中的创建是非常频繁的,在并发情况下是不安全的,比如正在给A对象分配内存,指针还没来得及修改,对象B又使用了原来的指针,解决这个问题有两种方式,一种是对分配空间的动作进行同步,这种方式影响分配速度,另一种就是每个线程在堆中预先分配一小块内存,称为本地线程分配缓冲

6.2什么是TLAB

从内存模型的角度来看,在堆中的Eden区域,jvm为每个线程分配了一小块空间,如图所示
在这里插入图片描述
通过TLAB,可以避免一些线程安全问题,因为每个线程的对象创建销毁都是在本地线程的TLAB空间中进行,属于线程私有,不会造成并发问题,并且能够提升内存分配的吞吐量,因此这种内存分配方式被称为快速分配策略,JVM将TLAB作为内存分配的首选,我们可以在程序中通过-XX:UseTLAB设置是否开启TLAB空间

默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,通过选项-XX:TLABWasteTargetPertcent重新设置所占Eden空间百分比,一旦对象在TLAB空间分配内存失败,JVM就会尝试通过加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存

在这里插入图片描述

7.堆是否是对象存储的唯一选择

到目前为止我们所说的对象都是在堆上创建的,在《深入理解Java虚拟机》中有这样一段描述:

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

言外之意,通过其他技术类似逃逸分析技术的发展,对象可以不只分配在对上,也可以在栈上分配了
在这里插入图片描述
那么什么是逃逸分析

7.1逃逸分析

举个例子

class Demo{
    void fun(){
        Demo d = new Demo();
    }
}

在fun方法中过的对象仅仅作用在这个方法内,即没有逃逸到方法外,对于这种对象,可以将其分配到栈上,随着方法的结束进行弹栈,栈空间被移除,对象也就被销毁,不存在GC问题,所以经过逃逸分析,我们可以对代码进行多种优化方式

7.2栈上分配

和在逃逸分析中举得例子一样, 对象尽可能的写成局部变量的方式,可以避免垃圾回收,提高程序性能,但值得注意的是:

  • 逃逸技术并不是很成熟,且HotSpot虚拟机并没有采用逃逸分析技术,所以到目前为止,所有的对象还是分配到java堆上的

7.3同步省略

线程的同步往往会带来程序性能的下降,举个例子:
在这里插入图片描述
未发生逃逸的对象可以省略同步代码块,进而提高性能

7.4标量分配

首先要知道什么是标量(Scalar),值无法再分解的数据,对应着java的原始数据,与之相对的就叫做聚合量,对象就是聚合量,看下面的代码:

public class heapTest {
    public static void main(String[] args){
        alloc();
    }
    static void alloc(){
        Demo d = new Demo();
        System.out.println(d.x + " " + d.y);
    }
}
class Demo{
    int x;
    int y;

}

在alloc函数中,对象未发生逃逸,进而可以将代码拆分为:

static void alloc(){
        int x;
        int y;
        System.out.println(d.x + " " + d.y);
    }

在经过拆分后,大大减少了堆内存的占用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章