秋招准备之——Java虚拟机

秋招复习笔记系列目录(不断更新中):

前段时间看了周志明老师的《深入理解Java虚拟机(第三版)》,加上自己在看的过程中查找的一些资料和理解,做了一些笔记,今天趁着复习,在这里分享一下。希望能帮助到同在复习Java虚拟机的同学,希望大家秋招Offer多多!

一、Java内存区域与内存溢出异常

1.运行时数据区域

在这里插入图片描述

  • 1.程序计数器: 当前线程所执行字节码的行号指示器,线程私有
  • 2.虚拟机栈: 描述方法执行的线程内存模型,每个方法执行的时候,会同步创建一个帧栈存储局部变量等信息。与方法中的局部变量紧密相关,局部变量会存放在局部变量表中,在编译期间,局部变量表所需的空间就确定了。局部变量中的存储空间以局部变量槽表示,long和double占两个变量槽,其他占一个。
    • 出现异常的情况:
      • 1.线程请求的栈深度大于虚拟机允许的深度会StackOverflow
      • 2.如果虚拟机栈可以动态扩容,当扩展无法申请到足够内存时会OutOfMemory
  • 3.本地方法栈: 和虚拟机栈类似,但本地方法栈为native方法服务
  • 4.堆: 所有线程共享,在虚拟机启动时创建,用于存放对象实例(所有的对象实例都在堆上分配),堆是垃圾收集器管理的区域。堆中可以划分出多个线程私有的分配缓冲区(TLAB,在分配内存时,每个线程都有自己的本地线程缓存池,分配时先从缓存池中分配,防止同步操作带来的性能问题),以提升对象分配效率。当堆中无法完成实例分配且堆无法扩展时,会抛出OOM异常。
  • 5.方法区: 所有线程共享,存储已被虚拟机加载的类型信息,常量,静态变量等,无法完成内存分配时,会抛OOM异常
    • 5.1 运行时常量池: 是方法区的一部分,存放字面量及符号引用(比如字符串常量池)
  • 6.直接内存: 堆外内存,受到本机总内存大小以及处理器寻址空间的限制。

2.HotSpot对象探秘

2.1 对象创建的大致步骤:

在这里插入图片描述

2.2 对象的内存布局

在HotSpot虚拟机中,对象在堆内存中的存储布局分为三个部分:

  • 1.对象头
    • Mark Word: 存储对象自身的运行时数据,如HashCode、GC分代年龄以及锁有关的信息
    • 类型指针: 对象指向它类型元数据的指针,通过这个对象确定类型是哪个类的实例
    • ③数组长度: 如果对象是数组,还需要一块内存记录数组长度。
  • 2.对象信息: 存储对象具体的信息,相同宽度的字段总是被分配到一起存放
  • 3.对齐填充: 仅仅起占位符作用(HotSpot中任何对象的大小必须8字节的整数倍,不足的需要填充)

2.3 对象的访问定位

访问过程:通过栈上的reference数据来操作堆上的具体对象,如下图所示:
通过句柄访问对象
在这里插入图片描述
通过直接指针访问对象:
在这里插入图片描述
对象类型数据和实例区别:

  • 对象类型数据(方法区):对象的类型、父类、实现的接口、方法等
  • 对象实例数据(堆 ):对象中各个实例字段的数据

二、垃圾收集器与内存分配策略

2.1 对象生命的确定

1.引用计数器算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

2.可达性分析算法

这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

  • 可以作为GCRoot的对象有哪些: 虚拟机栈中本地方法变量表中的引用对象、方法区中的类静态属性引用的对象,方法区中的常量引用的对象,本地方法栈中JNI的引用对象。
  • **为什么要选择这些对象作为GCRoot:**可达性分析的基本思路是,以当前活着的对象为root,遍历出他们(引用)关联的所有对象,没有遍历到对象即为非存活对象,这部分对象可以GC掉。当前帧栈中的引用型变量、静态变量引用的对象、本地方法栈JNI对象,是当前存活的对象,所有他们应该作为GCRoots。方法区中的常量引用对象,在当前可能存活,因此,也可能是GC roots的一部分。还有其他一些对象也可能是GC Roots的一部分,比如被classloader加载的class对象,monitor的对象,被JVM持有的对象等等,这些都需要视当前情况而定。

3.引用类型

  • 强引用: 是指在程序代码之中普遍存在的引用赋值,即类似Object obj=new Object()这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

  • 软引用: 用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用: 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用: 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

2.2 垃圾收集算法

1.分代收集理论

  • (1) 两个分代假说

    • 弱分代假说:绝大多数对象都是朝生夕灭
    • 强分代假说:熬过越多次垃圾收集过程的对象就难以消亡
    • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。所以新生代GC时,若存在老年代引用时,直接判定对象,若干年龄后进去老年代,存活避免新生代与老年代有GC引用链,导致新生代GC的时候,需要进行老年代GC
  • (2) 垃圾收集器的设计原则: 应该将Java堆划分出不同的区域,然后将对象根据其年龄(熬过垃圾收集过程的次数)分配到不同的区域中存储。

  • (3) Java虚拟机中的设计: 一般把Java堆分成新生代和老生代两个区域。新生代的未被收集的对象会逐渐跨向老生代。

    • 新生代: 主要是用来存放新生的对象。一般占据堆空间的1/3,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代分为Eden区、ServivorFrom、ServivorTo三个区。

      • Eden区:Java新对象的出生地(如果新创建的对象占用内存很大则直接分配给老年代)。当Eden区内存不够的时候就会触发一次MinorGc,对新生代区进行一次垃圾回收。
      • ServivorTo:保留了一次MinorGc过程中的幸存者。
      • ServivorFrom: 上一次GC的幸存者,作为这一次GC的被扫描者。当JVM无法为新建对象分配内存空间的时候(Eden区满的时候),JVM触发MinorGc。因此新生代空间占用越低,MinorGc越频繁。 MinorGC采用复制算法。
    • 老年代: 老年代的对象比较稳定,所以MajorGC不会频繁执行。

      Minor GC(新生代GC):简单理解就是发生在年轻代的GC。
      Minor GC的触发条件为:当产生一个新对象,新对象优先在Eden区分配。如果Eden区放不下这个对象,虚拟机会使用复制算法发生一次Minor GC,清除掉无用对象,同时将存活对象移动到Survivor的其中一个区(fromspace区或者tospace区)。虚拟机会给每个对象定义一个对象年龄(Age)计数器,对象在Survivor区中每“熬过”一次GC,年龄就会+1。待到年龄到达一定岁数(默认是15岁),虚拟机就会将对象移动到年老代。如果新生对象在Eden区无法分配空间时,此时发生Minor GC。发生MinorGC,对象会从Eden区进入Survivor区,如果Survivor区放不下从Eden区过来的对象时,此时会使用分配担保机制将对象直接移动到年老代。
      Major GC(老年代GC)的触发条件:清理老年代,
      Full GC(堆清理):整个堆的清理,包括老年代和新生代
      

2.标记清除算法

  • 定义: 算法分为标记清除两个阶段,首先标记出要回收的对象(或者标记处不需要回收的对象),然后统一回收。
  • 缺点: ①效率不稳定(对象太多时效率下降);②内存空间碎片化
    在这里插入图片描述

3.标记复制算法

  • 定义: 将内存按容量划分成大小相等的两块,每次只适用其中的一块,当一块用完了,就将还存活的对象复制到另一半,将已使用的一半全部清除掉。能避免内存空间碎片化的问题。 此方法一般用于新生代的垃圾回收中
  • 缺点: 可用内存缩小为原来的一半,会产生对象复制的开销
    在这里插入图片描述

4.标记整理算法

  • 定义: 标记过程和标记清除算法一样,但后续步骤不是清除,而是所有存活的对象都向内存空间另一端移动,然后直接清理掉边界以外的内存
  • 缺点: 大量老生代存活的对象的移动,耗费时间,且需要全程暂停用户程序。基于此,一种解决方式是,先使用标记清除算法,当内存碎片化严重到不可忍受时,再使用标记整理算法整理一次
    在这里插入图片描述

2.3 HotSpot的算法实现细节

1.根节点枚举

枚举GCRoots,利用OopMaps的数据结构,来达到根节点快速枚举,OopMaps其实就是一个映射表,通过映射表知道在对象内的什么偏移量上是什么类型的数据。

2.安全点

只在安全点的位置建立OopMaps,强制到达安全点以后才暂停,进行垃圾收集,通常在方法调用、循环跳转、异常跳转等地方设置安全点。

3.安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。当代码执行到安全区域时,首先标识自己已经进入了安全区域,那样如果在这段时间里JVM发起GC,就不用管标示自己在安全区域的那些线程了,在线程离开安全区域时,会检查系统是否正在执行GC,如果是,就等到GC完成后再离开安全区域。

在这里插入图片描述

4.记忆集和卡表

为了解决跨代引用问题,在新生代引入的记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。这样在垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引用指针的全部细节。可以采用不同的记录粒度,以节省记忆集的存储维护成本。如:

  • 字长精度:每个记录精确到一个机器字长(处理器的寻址位数,如常见的 32 位或 64 位),该字包含跨代指针
  • 对象精度:每个记录精确到一个对象,该对象中有字段包含跨代指针
  • 卡精度:每个记录精确到一块内存区域,该区域中有对象包含跨代指针

卡精度使用"卡表"的方式实现记忆集,卡表使用一个字节数组实现,每个元素对应着其标志的内存区域中一块特定大小的内存块,称为卡页。卡页大小为2的整数次方,HotSpot中是29 ,即512字节。

一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0。GC时,只要筛选卡表中变脏的元素加入GCRoots。

5.写屏障

写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面&#x

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MeteorChenBo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值