作者:细妹很快乐 | 来源:互联网 | 2023-09-06 11:04
目录
一、JVM内存模型
1.1、JVM可划分为以下五个区
1.2、Eden空间和Survivor空间
1.3、内存分配及回收策略
二、JVM中的一些基本算法
2.1、对象存活判定算法
2.2、垃圾收集算法
2.3、HotSpot的算法实现
2.4、对象引用
一、JVM内存模型
1.1、JVM可划分为以下五个区
程序计数器
可看作当前线程所执行的字节码的行号计数器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。
Java虚拟机栈
用来描述java方法的执行。每个方法执行时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法接口等信息。方法的调用至完成的过程,对应着栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈
与虚拟机栈功能类似,只是本地方法栈是为Native方法服务。在有的虚拟机中(如:Sun HotSpt虚拟机),Java虚拟机栈与本地方法栈合二为一。
堆(GC堆)
用于存放对象实例,占据JVM所管理内存中的最大一块。可细分为新生代(Yong Gen)、老年带(Tenured Gen),新生代又可细分为Eden空间、From survivor空间、To Survivor空间。Eden空间和survivor空间默认比例为8:1。
方法区(Non-Heap 非堆)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。最初设计时使用的是永久代(Perm Gen)实现方法区,jdk1.8以后永久代被彻底移除。
- 以上五个数据区中,方法区与堆为线程共享的,其余数据区为线程隔离的。
- 方法区含有运行时常量池。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的常量池,这部分内容在类加载后进入方法区的运行时常量池。
1.2、Eden空间和Survivor空间
GC开始前对象只存于Eden空间和From Survivor空间,To Survivor空间是空的。GC时,Eden区中所有存活对象复制到To Survivor,年龄记为一岁,。From Survivor中存活对象计算年龄决定是否移至老年代。经过这次GC后,From和To空间角色交换,即原先的From变为To,To变为From。
1.3、内存分配及回收策略
- 对象优先在Eden空间分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代。(默认15岁,每活过一个GC加一岁)
二、JVM中的一些基本算法
2.1、对象存活判定算法
引用计数算法
给对象中添加一个引用计数器,每当一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器为0的的对象就是不可能再被使用的。
引用计数算法的实现简单,判定效率也高,但难以解决对象间的循环引用问题。如:对象A中含有B的引用,对象B中含有A的引用,这样即使程序中不存在对象A、B的引用了,计数器人不为1,即无法进行垃圾回收。
可达性分析算法
主流商用程序语言的主流判定对象存活的算法实现。这个算法的基本思路就是通过一系列的称"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径车位引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图(深入理解java虚拟机书中原图),对象object5、object6、object7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们被判断为可回收。
在java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
2.2、垃圾收集算法
标记 - 清除算法
标记-清除算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后同一回收所有标记的对象。
它主要不足有两个:
- 效率问题,标记和清除两个过程效率都不高;
- 空间问题,标记清除后会产生大量不连续的内存碎片(可能导致分配较大对象时找不到连续内存而不得不提前触发另一个垃圾收集动作)
复制算法
将可用内存分为大小相等的两块,每次只用其中一块。当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
复制算法使得每次都是对整个半区进行内存回收,内存分配时再也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。代价是将内存缩小为原来的一半,代价过高。
现在的商业虚拟机都采用这种收集算法来回收新生代。因为新生代中的对象98%是"朝生夕死"的,所以不需要1:1分配。只需分为一块较大的Eden空间和两块较小的Survivor空间即可(复制过程参考上文中Eden空间和Survivor空间的描述)。当然,当Survivor空间不够时,需要依赖老年代进行分配担保。
标记 - 整理算法
标记过程与"标记 - 清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象移向一端,然后直接清理掉端边界以外的内存。该算法用于老年代。
2.3、HotSpot的算法实现
HotSpot实现上述对象存活判定算法和垃圾收集算法时需要严格的考量,才能保证虚拟机的高效运行。在此,不展开说明,仅列举一些要点
枚举根节点:在安全点使用一组OopMap的数据结构记录哪些位置是对象的引用。
安全点:在安全点才能停顿进行GC,安全点是以"是否具有让程序长时间执行的特征"为标准选取的。如:方法调用、循环跳转、异常跳转等。
在GC发生时需要让所有线程都跑到最近的安全点上再停顿下来,有两种方案可供选择
- 抢先式中断:在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机采用这种方案。
- 主动式中断:不对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点的地方是重合的。
安全区域:指在一段代码片中,引用关系不会发生变化,在这个区域中任意地方开始CG都是安全的。可看作被拓展的Safepoint,线程执行到Safe Region时,会标识自己进入Safe Region,这样,GC发生时,虚拟机就不用管为自己标识了Safe Region状态的线程了。
2.4、对象引用
在前面对象存活判定中,对象是否存活都与引用有关。那么引用到底是什么?
JDK 1.2之后,Java将引用分为强引用 (Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种。
- 强引用:类似"Object obj = new Object()"这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
-
软引用:用来描述一些还有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
-
弱引用:也是用来描述非必需对象,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前
-
虚引用:也被称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
注:本文实则为《深入理解Java虚拟机》的读书笔记,文中概念大多引自该书。