JVM---jvm对象

1.对象的创建

对象的创建大概可以分为5步,流程如下图所示

1.类加载检查

虚拟机遇到一条new指令时,首先在常量池中定位到这个类的符号引用,然后检查这个符号引用代表的类是否已经被加载过、解析和初始化过,如果没有,那必须先执行相应的类加载过程。

2.分配内存

就是在类加载完成之后,为对象分配实际的内存,对象的大小会在类加载的时候知道,分配内存就是把一块确定大小的内存从Java堆里分出来。有“指针碰撞”和“空闲列表”两种方式。

  • 内存分配并发问题

    在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机有两种方式来保证线程安全:

    CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。

    TLAB:为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。

3.初始化零值

内存分配完成后,虚拟机要将分配的内存都初始化为零值(不包括对象头),就是为了保证对象的实例字段能够在不赋初始值时可以直接使用。

4.设置对象头

初始化零值后,虚拟机需要对对象头进行设置,比方说这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启动偏向锁等,对象头会有不同的设置方式。

5.执行init方法

设置完对象头之后,对于虚拟机来讲,对象就已经创建完成了,但对Java程序来说才刚刚开始,方法还没有执行,所有的字段都还为零。所以一般new指令完成之后都会执行方法,按照程序所写的意思进行初始化,这样一个真正可用的对象才算完全产生。

在这里插入图片描述

2.对象在内存中的结构

这里写图片描述

参考:https://www.cnblogs.com/duanxz/p/4967042.html

3. 堆栈方法区的交互

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

对象的访问定位目前有两种方式,一种是通过句柄访问,一种是直接指针

句柄:如果通过句柄访问的话,那么java堆中将会划分出一块内存来作为句柄池,引用(reference)中存储的就是对象的句柄池,句柄池中包含对象实例数据和对象类型数据的指针。

**直接指针:**如果使用直接指针访问,reference中存储的直接就是对象实例数据,对象实例数据中有指向对象类型的指针。

**两种方式对比:**使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一项十分可观的执行成本。

这里写图片描述

4.内存分配

为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

4.1 给对象分配内存方式
4.1.1 指针碰撞(Serial、ParNew等带Compact过程的收集器)

假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。

4.1.2 空闲列表(CMS这种基于Mark-Sweep算法的收集器)

如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

4.2 内存分配流程

总体流程
这里写图片描述

对象分配流程
这里写图片描述
  如果开启栈上分配,JVM会先进行栈上分配,如果没有开启栈上分配或则不符合条件的则会进行TLAB分配,如果TLAB分配不成功,再尝试在eden区分配,如果对象满足了直接进入老年代的条件,那就直接分配在老年代。

4.3 对象分配在堆栈

在学习Java的过程中,一般认为new出来的对象都是被分配在堆上的,其实这个结论不完全正确,因为是大部分new出来的对象被分配在堆上,而不是全部。通过对Java对象分配的过程分析,可以知道有另外两个地方也是可以存放对象的。这两个地方分别栈 (涉及逃逸分析相关知识)和TLAB(Thread Local Allocation Buffer)。

4.3.1 栈上分配

在JVM中,堆是线程共享的,因此堆上的对象对于各个线程都是共享和可见的,只要持有对象的引用,就可以访问堆中存储的对象数据。虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但对于垃圾收集器来说,无论筛选可回收对象,还是回收和整理内存都需要耗费时间。

如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上,这样,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以减小垃圾收集器的负载。

JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。

1) 栈上分配如何开启

栈上分配前提:

  • 开启逃逸分析 (-XX:+DoEscapeAnalysis);逃逸分析的作用就是分析对象的作用域是否会逃逸出方法之外,再server虚拟机模式下才可以开启(jdk1.6默认开启)
  • 开启标量替换 (-XX:+EliminateAllocations);标量替换的作用是允许将对象根据属性打散后分配再栈上,默认该配置为开启

查看逃逸结果:可以通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果。

逃逸分析的基本行为就是分析对象的动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用。

方法逃逸:例如作为调用参数传递到其他方法中。
线程逃逸:有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量。
可以参考:https://blog.csdn.net/yangzl2008/article/details/43202969

4.3.2 TLAB 线程本地分配缓存

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
  由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。
  TLAB本身占用Eden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
  由于TLAB空间一般不会很大,因此大对象无法在TLAB上进行分配,总是会直接分配在堆上。TLAB空间由于比较小,因此很容易装满。比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,若小于该值,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。如果想要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,并使用-XX:TLABSize手工指定一个TLAB的大小。

-XX:+PrintTLAB可以跟踪TLAB的使用情况。一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为。

5.对象的访问定位

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

对象的访问定位目前有两种方式,一种是通过句柄访问,一种是直接指针

句柄:如果通过句柄访问的话,那么java堆中将会划分出一块内存来作为句柄池,引用(reference)中存储的就是对象的句柄池,句柄池中包含对象实例数据和对象类型数据的指针。

**直接指针:**如果使用直接指针访问,reference中存储的直接就是对象实例数据,对象实例数据中有指向对象类型的指针。

**两种方式对比:**使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一项十分可观的执行成本。

6.判断对象已死

有两种方法

引用计数法,就是如果一个对象被引用,就计数加一,反之则计数减一,若计数为零,则说明这个对象不被引用。有一个缺点就是,它无法解决循环引用的那种情况。所以没有Java虚拟机使用此算法。

可达性分析算法,就是说首先标记一个GC root,然后找出和GC ROOT直接关联的对象,再接着追溯,找出所有与之关联的,称为引用链。其他没有关联的就是不可达的,这些对象就是不可用的,稍后会回收掉。

可作为GC Roots的对象:虚拟机栈中引用的对象、方法区静态引用的对象、方法区常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。

7.对象分配策略

1.优先分配在新生代

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

2.大对象直接进入老年代

1)大对象来回复制不方便,2)因为是大对象所以会频繁触发GC。

为了避免为大对象分配内存时,由于分配担保机制带来的复制而降低效率。

3.长期存活的对象直接进入老年代

对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。

4.动态对象年龄判定

为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到默认的15,才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold中要求的年龄。(也是为了减少对象复制移动)

回收策略

空间分配担保

在进行新生代gc之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,

  • 大于的话,本次Minor GC可以确保是安全的。

  • 如果不大于,需要查看虚拟机设置的参数是否允许担保失败。

    • 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的。
      • Minor GC后,如果老年代放不下,就进行一次老年代的GC,再存不进来,就报outofnumer异常。
    • 如果不允许,则直接进行一次full gc。

一段话描述

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;Minor GC后,如果老年代放不下,就进行一次老年代的GC,再存不进来,就报outofnumer异常。如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值