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,因只计算了Eden和其中一块Survivor空间在这里插入图片描述

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异常

至于为什么要在新生代中设置这一内存占比,跟后面的标记-复制算法有很大关系,现在主流的java虚拟机都采用了这种方式去回收新生代,因为大多数对象的生命周期很短,采用复制算法可以将Eden和其中一个Survivor中少量的存活对象复制到另一块Survivor空间,这样使得垃圾回收效率大大增加,这也从侧面说明了在计算新生代内存空间时只计算了一块Survivor空间,另一块Survivor空间是‘浪费掉’的,也就是整个新生代10%的空间是没有被计算的。IBM的一次针对新生代对象的研究,发现有98%的对象熬不过第一轮收集,这只是在一次普通场景下做的测试,无法百分百保证每次回收都只有不多于10%对象存活,也就是在某些特殊情况下存活的对象大于10%,那么另一块Survivor不足以容纳一次Minor GC后存活的对象,就会将对象存放到其他区域,进行分配担保,综上所述,这种8:1:1的内存分配策略被称为Appel式回收,HotSpot虚拟机的Serial,PaeNew等年轻代收集器都采用了这种策略来设计新生代内存布局

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.几种垃圾收集比较

对于GC不太了解的可以看一下关于GC垃圾回收及其算法的一点思考,里面有对GC详细的阐述
在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);
    }

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

8.STW(Stop The World)

在GC时间发生过程中,会产生应用程序的卡顿,停顿时产生整个应用线程都会被暂停,没有任何响应,优点类似卡死的感觉,这个停顿成为STW,被STW中断的用户线程会在完成GC后恢复,STW事件和采用哪款GC无关,所有的GC都有这个事件,垃圾收集器不能完全避免STW,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间,STW是JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,所以在开发中不要用System.GC,会导致STW发生

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值