实习日记08/16 day28 理解JVM--理解内存分配策略

概览

在这里插入图片描述

理解对象创建

对象的创建

Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常只是依靠new 关键字而已,而在虚拟机层面则复杂的多:

  1. 当虚拟机遇上new指令,先去监察指令参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类量是否已经被加载、解析或者初始化过。如没有则需先执行相应的类加载过程。
  2. 类加载过后,是虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。此时分为两种划分方式:一种名为指针碰撞,比较简单只是向空闲区域移动一个对象的距离,当然前提是内存划分规整,还有一种是空闲分配方式是空闲列表法,选择哪种方式取决于Java堆是否规整。
  3. 接下来虚拟机对对象进行必要的设置,例如对象是那个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

对象的内存布局

在Hotpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充

  1. 对象头:存储对象的自身运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等。这部分也称为Mark word。另一部分则是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象属于哪个实例
  2. 实例数据:对象真正存储的有效信息,也是在程序中所定义的各个类型的字段内容。
  3. 对齐填充:不是必然存在,只是站位符的作用。

对象的访问定位

Java程序通过栈上的reference数据来操作堆上的具体对象。

  1. 使用句柄访问:Java堆中将会划分出一块内存区域作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
  2. 使用直接指针访问:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息。reference中存储的直接就是对象地址。

对象的内存分配策略

Java技术体系中所提倡的自动内存管理最终可归结为自动化的解决了两个问题:给对象分配内存和回收分配给对象的内存。回收内存是垃圾收集算法和垃圾收集器所做的事,而对象的分配则采用了较为复杂的策略。

对象的内存分配,往大方向将,是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中,分配的规则并不是百分之百固定,其细节取决于当前使用的是哪一种垃圾收集器组合。

对象优先在Eden区分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次MinorGC。

大对象直接进入老年代

所谓的大对象是指,需要大量连续空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对于虚拟机的内存分派来说是一个大挑战。

长期存活的对象将进入老年代

既然虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须能识别到那些对象应放在新生代,那些对象应放在老年代。所以每个对象都有一个年龄计数器,来判断这个小家伙到底够资格进入老年代吗?

动态对象的年龄判定

为了能更好的适应不同程序的内存情况,虚拟机并不是永远的要求对象的年龄必须达到MaxTenuringTreshold才能晋升到老年代,如果Survior空间中相同年龄的所有对象代销的总和大于Survivor空间的一半,年龄大于或者等于年龄的对象就可以直接进入老年代。

空间分配担保

在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总和,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许则会继续检查老年代最大可用空间连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将会进行一次MinorGC,尽管这次MinorGC是有风险的,如果小于又或者HandlePromotionFailure设置为不允许冒险,那这时也要改为进行一次Full GC。

冒险:新生代使用的复制算法,当大量对象都存活,就需要老年代进行分配担保,把Survivor无法容纳的对象移入老年代中,但是老年代也不是无底洞,所以取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多的空间。

对象的成神之路:

在很多的Java虚拟机中,内存区域被划分为两个大块:新生代和老年代,而在一些Java虚拟机中新生代又细分为Eden区和两个Survivor空间,这种布局让对象的历程变得更加的“坎坷”,想晋升到老年代变得更加困难,在新生代有了更多的被回收的机会。

这种设计模式是为了防止在新生代垃圾收集时,有的对象还处于活跃期。这些对象可能是刚诞生的新生儿,可能有相当长的生命周期,但也有很大一部分用之即弃,有些超短寿命的对象或许占有很大的内存空间,他们刚被创建,因此不能被回收释放;但是他们的生命周期又非常的短,不应该被放在老年代,由此出发重新设计新生代,层层淘汰那些不符合的对象,保证老年代不会被轻易填满。毕竟对于新生代和老年代采用的算法与收集器都是不同的,清理老年代的代建大得多。

新生代埃及回收的时候,如果JVM发现对象还十分的活跃,会首先尝试将其移动到Survivor空间,而把不是直接移动到老年代,首次新生代的垃圾收集时,对象从Eden移动到Survivor0区域,紧接着下一次的垃圾收集中,活跃对象会从Survivor0和Eden空间移动到Survivor1,所以Survivor中0和1不是递进关系,因为下一次的垃圾回收是从Survivor1和Eden区域移动到Survivor0,每次垃圾回收这两个Survivor之间会发生频繁的对象互换。就像水很热,但又特别渴就会拜托妈妈把水从一个杯子移动到另一个杯子中去,活跃对象在这两个Survivor区域也是如此。

显而易见,这种情况不会一直持续,否则对象只是在新生区打转罢了,在以下两种情况下:对象会被移动到老年代。第一:Survivor空间的大小实在是太小了,新生代垃圾收集时,如果目标Survivor被填满,Eden空间剩下的对象会直接进入到老年代。第二:对象在Survivor空间经历的GC周期是有上线的,超过这个上限也会被移动到老年代,这个上限值被称为晋升阈值。这个数值是可以调节的,在JVM优化中要选择合适的阈值来进行优化。

总结一下:

  1. 设计Survivor空间的初衷是让对象(尤其是已经分配的对象)在新生代停留更多的GC周期。
  2. 如果Survivor空间过小,独享会直接晋升到老年代,从而出发更多的老年代GC
  3. 有些情况下,如需避免对象晋升到老年代,调整阈值或者Survivor的空间的大小来避免对象晋升到老年代。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值