JVM 的分代模型

2 篇文章 0 订阅

伸手摘星,即使一无所获,亦不致满手污泥

请关注公众号:星河之码

我们知道在Java虚拟机中,当对象被创建出来后,是放在堆内存中的,那么在堆内存里面,对象又是怎么存储的呢?是所有对象都放在一起吗?

我们先做一个假设,假设堆内存没有做任何区分,所有对象的从创建到销毁都是在放在一起。这回引起什么问题呢?

思考一个问题,Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆,当对象不在被使用了,GC在要去清理的时候,它怎么知道自己要清理那些对象呢?所有GC每次执行的时候都将堆内存所有对象全部扫描吗?这很显然是一种方式,但是却要消耗很多的资源,会直接影响到程序的性能问题,显然JVM中不可能会放任这种方式,因此提出了一个【分代收集理论】来解决这个问题。

一、分代收集理论

分代收集理论的思想:根据对象的生命周期将内存划分,然后进行分区管理

目前商业虚拟机的垃圾收集器,大多都遵循【分代收集理论】进行设计, 它建立在两个分代假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过越多次数垃圾回收过程的对象就越难消亡。

这两种设计原则共同奠定了大多数垃圾收集器的一致的设计原则:应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储

把那些难以回收的对象放在一起,这个区域的垃圾回收频率就可以降低,减少垃圾回收的开销

把那些朝生夕灭的对象放在单独的区域,可以用较高的频率去回收

在Java堆划分出不同的区域之后,垃圾回收器就可以每次只回收自己负责的那一部分区域,因此也就有了我们常说的几种GC:

  • 部分收集(Partial GC) : 指收集的不是整个Java堆的垃圾收集, 其中又分为:

    • 新生代收集(Minor GC/Young GC)

      处理新生代(Eden \ S0,S1)的垃圾回收

    • 老年代收集(Major GC/Old GC)

      只处理老年代的垃圾回收

      目前只有CMS收集器会有单独收集老年代的行为。

    • 混合收集(Mixed GC)

      整个新生代以及部分老年代的垃圾收集回收

      目前只有G1收集器会有这种行为。

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

以上的几种垃圾回收器分别针对不同的区域进行垃圾回收,各种GC的原理后面在看,既然Java堆被划分为多个区域,接下来就看看堆内存的集体划分。


基于此分代收集理论,JVM将堆内存进行了进一步的划分,分为【新生代】【老年代】,每个区域由不同的GC清理。

注意:这个图中还有一个永久代,实际JDK1.8以后就取消了永久代,并将方法区移动到元空间中,但是我们要知道有这个东西。

一、新生代

在Java程序中,大多数对象的存活时间都是很短的,就比如以下这段代码

public void funA(){
    User user= new User();
    user.queryUserById(id);
}

public void queryUserById(String id){
    System.out.println("queryUserById");
}

当线程执行了funA后,会创建一个User对象实例存放在Java堆内存中,此时funA栈帧中局部变量user会指向该对象实例的地址

当funA 方法执行完成后,funA栈帧会出栈,局部变量也就不存在了,此时User对象实例就没有被引用了,如下

此时在JVM后台运行的垃圾回收线程一旦发现了没有被引用的对象实例,它就会将其回收掉,释放堆内存。


以上这个过程,我们的User对象被创建出来就放在堆中,使用完了就立刻销毁,这种对象一般就会在堆内存的第一个区域年轻代中。

新生代(Young Generation)

创建之后会优先放在被存放在堆内存的年轻代中,当年轻代放不下的时候才会放在老年代,由于新生代的对象创建销毁频繁,所以Young GC也是执行的非常频繁的。

有关GC的在后续讲解


从上面的图中可以看出来,新生代内又分三个区:一个Eden区,两个Survivor区(幸存者区)

  • 大部分对象在Eden区中生成,当Eden区满时,还存活的对象将被复制到两个Survivor区(其中的一个)中

  • 而当某个Survivor区满时,此区存活且不满足【晋升老年代】条件的对象将被复制到另外一个Survivor区

  • 这些对象每经历一次Minor GC,它们的年龄会加1,当年龄达到【晋升老年代阈值】后,会被被放到老年代,这个过程就是对象的晋升过程

晋升老年代阈值直接影响着对象在新生代中的停留时间,默认为15

二、老年代

既然上面说了对象被创建后,就会优先放在年轻代中,当年龄到达晋升阈值后才会晋升,那么什么样的对象会被晋升到老年代呢?下面我们改造一下上面的案例

private static Order order= new Order();
public void funA(){
    User user= new User()
    user.queryUserById(id);
    order.queryOrderById(id);
}

public void queryUserById(String id){
    System.out.println("queryUserById");
}

public void queryOrderById(String id){
    System.out.println("queryOrderById");
}

上面的伪代码中,有一个静态变量order执行了order实例,user变量是存在方法区的(类的静态变量,类信息等是存在方法区的),画一下这个伪代码的图


注意!!!

不要被这张图误导,order对象一开始也是存在于新生代的,但是由于它是静态变量,一般而言静态变量,只要类没有销毁都会存在于方法区中,所以它会在新生代存在一段时间,最终会晋升到老年代中

那么方法区里面的类啥时候会被销毁呢,这个静态变量是不是会一直存在呢?答案是否定的,方法区里面的类满足以下几种情况下会被销毁。

  • 该类所有的实例对象都已经被回收
  • 加载这个类的ClassLoader对象被回收了
  • 该类的Class对象没有任何引用

满足以上几种情况,方法区的类会被回收,那么它的静态变量自然也就会被回收了,老年代的对象实例也就会被清理了


回来继续看以上伪代码,当我们的funA执行完毕后,funA栈帧出栈,局部变量user被清理,此时User的实例对象就失去了引用,但是此时静态变量order还在引用着Order实例对象。如下


由以上案例可以得出一个结论:

年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁

三、为啥要分新生代与老年代

通过上述的描述,我们已经大致清楚了,Java的堆内存的新生代与老年代分别是存储了什么,那JVM 为啥要这么分呢?这么分有什么好处呢?

其实很好理解,堆虽然是JVM中最大的一块内存区域,但是再大也是有上限的,因此jvm提供了垃圾回收算法来清理那些已经失去引用的对象,而垃圾回收需要判断对象是否已经失去了引用,就需要检查每个对象,这无疑是一笔很大的资源开销。

将堆内存进一步划分,将那些创建之后很快就会被销毁的对象存放在新生代区域,将那些需要长期存在的对象存放在老年代区域,针对新生代/老年代分别使用不同对策垃圾回收策略,这样就不需要每次进行GC的时候都扫描所有的对象了,减小资源开销


既然新生代,老年代都是堆内存,存放不同的对象,那它们是怎么划分区域的呢?接下来看看置新生代和老年代堆结构占比

四、新生代和老年代堆内存占比

堆的内存大小是可以人为设置的,主要受物理内存大小的限制

  • -Xmx:表示堆区的起始内存
  • -Xms:表示堆区的最大内存

堆被分为新生代和老年代堆,新生代又被分为了三个区。这些内区域是怎么划分的呢?先来看一张图,看图说话

通过这张图,可以很清晰的看到新生代和老年代在堆中的占比

  • 新生代和老年代占比

    新生代在堆中的大小由参数【-XX:NewRatio】控制,默认-XX:NewRatio = 2,表示新生代占1 , 老年代占2,新生代占整个堆的1/3

  • 新生代三个区的内存占比

    新生代的三个区占比默认情况下:Eden空间和另外两个Survivor空间占比分别为8:1:1,受参数【-XX:SurvivorRatio】控制,默认-XX:SurvivorRatio = 8,表示Eden::S0:S1= 8:1:1

    几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁.

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 的新生代空间

五、对象分配过程

明白了堆内存的区域划分和各个区域的内存大小,只是明白了JVM的内存如何分配,在哪里分配而已,但是我们的对象的创建于销毁还与内存回收算法有关系,因此对对象的创建后具体是怎么分配的也要有考虑,中间是否产生了内存碎片等问题。

所以要对对象分配过程的流程有一个了解,看一下下面这张流程图,

通过上面这种图,我们可以大致梳理一下对象的内存分配流程

  • 当我们创建一个对象的时候,这个对象会优先放在Eden区

  • 当Eden区放不下了,此时又需要创建新的对象,jvm的垃圾回收器就会对Eden区进行垃圾回收(Minor GC)

    • 将Eden区没有被引用的对象进行下销毁,有引用的的对象移动到S0区,并将其年龄加1
    • 释放内存,给新的对象分配内存

  • 后面再次再次触发垃圾回收,也会做两件事

    • 清理Eden 和 两个Survivor区中没有被引用的对象

    • 将S0区还有引用的对象移动到S1区,年龄加1,将S1区还有引用的对象移动到S0区,年龄加1

      这个过程其实就是在互换S0与S1区的对象,以达到其增加年龄,并整理内存碎片的目的

  • 当S1或者S0区的对象年龄达到阈值后,就会被移动到老年代中

    阈值可以通过参数 -XX:MaxTenuringThreshold设置,比如 -XX:MaxTenuringThreshold = 10

  • 当对象移动到老年代区时,老年代内存不足,也会触发GC(Minor GC)进行老年代的清理

    如果老年代清理之后,新的对象还是放不进去,就会OOM异常

六、元空间

在之前的文章《Jvm 的内存模型》中说到方法区的时候,我们说方法区在JDK1.8中名字了,称之为元空间(metaspace),并将其移出了jvm的内存区域的,了解了堆内存的分代模型后,我们再回头来看看元空间到底是个啥?


前面说分代模型的时候,提到在Jdk1.8之前出了新生代,老年代还有一个永久代的,那时候HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。

从 JDK 1.8 开始,HotSpots取消了永久代,并把方法区移至元空间,元空间它位于本地内存中,而不是虚拟机内存中

永久代从某种意义上讲并不是去掉了,只是被元空间(Metaspace)取代了而已

接下来就来看看元空间具体是什么

6.1 元空间和永久代有什么不同

  • 存储位置不同

    1.7之前永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存

  • 存储内容不同

    在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。

    现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。

下图是一张1.8与1.7的内存结构图

6.2 为什么要废弃永久代,引入元空间

之前既然已经有了永久代,那为啥要将其废弃呢,废弃了它引入元空间又有什么好处呢?

  • 在1.7的永久代中它需要存放【类的元数据】、【静态变量】和【常量】等

    • 我们需要给它分配大小,而它的大小受诸多因素影响,比如类的总数,常量池的大小和方法数量等,

    • 当我们通过-XX:MaxPermSize,指定太小可能造成永久代内存溢出。

  • 移除永久代后,就不需要配置永久代,这样HotSpot VM就可以进一步与 JRockit VM融合,

    JRockit没有永久代

  • 既然新生代,永久代都有自己的GC,那么永久代必然也有自己的GC,永久代在一定程度上让GC复杂度提升了,并且回收效率偏低

6.3 废除永久代的好处

  • 废除永久代,引入元空间后,类的元数据存储在本地内存中,元空间的最大可分配空间就是系统可用内存空间,大大降低OOM的出现。
  • 将元数据从永久代剥离出来到Metaspace,提升对元数据的管理同时提升GC效率。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM(Java虚拟机)的分代模型是一种内存管理策略,将堆内存划分为不同的代(Generation),包括年轻代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,JDK8之后被元空间(Metaspace)取代)。下面是JVM分代模型的执行流程: 1. 初始阶段:当Java应用程序启动时,JVM会为其分配一块初始的堆内存空间。此时,年轻代和老年代都是空的。 2. 对象创建:当Java程序创建对象时,对象会被分配在年轻代的Eden区域。如果Eden区域没有足够的空间来存放新创建的对象,就会触发一次垃圾回收(Minor GC)。 3. Minor GC:在Minor GC中,垃圾回收器会扫描年轻代的Eden区域和Survivor区域,将不再被引用的对象进行回收。存活的对象会被移动到Survivor区域中的一个空闲区域。 4. 对象晋升:当一个对象经过多次Minor GC后仍然存活,它会被晋升到老年代。晋升条件可以根据不同的垃圾回收器而有所不同。 5. Major GC:当老年代空间不足时,会触发一次Major GC(也称为Full GC)。在Major GC中,垃圾回收器会扫描整个堆内存,对不再被引用的对象进行回收。 6. 永久代/元空间:永久代(JDK8之前)或元空间(JDK8及以后)用于存放类的元数据和常量池等信息。当类的元数据不再被使用时,会触发一次永久代/元空间的垃圾回收。 7. 内存分配担保:在进行垃圾回收时,如果老年代的空间不足以存放新创建的对象,JVM会进行一次内存分配担保。即使触发了垃圾回收,也能保证新创建的对象能够顺利分配到老年代。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值