Java虚拟机 - 内存管理 (Java内存结构,内存分配,垃圾回收)

Java虚拟机- 内存管理

Java虚拟机相关知识点一览图
java虚拟机一览图
本篇文章仅涉及内存管理部分

1. 运行时数据区域划分

运行时数据划分为五个部分:程序计数器,虚拟机栈,本地方法栈,堆,方法区

1.1程序计数器
  • 线程不共享,线程消亡即释放
  • 记录当前指向字节码的的地址
  • 唯一不会发生OutOfMemoryError(OOM异常)的区域
1.2虚拟机栈与本地方法栈
  • 线程不共享,现象消亡即释放
  • 存储局部变量表(基本数据类型,对象引用,returnAddress类型(指向一条字节码指令地址,应该就是方法出口)),操作数栈,动态链接,方法出口
  • 本地方法栈与虚拟机栈差不多,只不过是存储native本地方法相关的数据
  • 如果扩展无法申请到足够的内存是,就会抛出OOM异常

*动态链接:*根据环境变化指向不同的引用(可以想想多态和动态绑定)

1.4堆
  • 线程共享空间,需要垃圾回收(堆一般是新生代)
  • 存储对象的实例(不是所有的对象以及数组实例都存储在这里)
  • 当堆中没有可分配的空间可供分配是就是抛出OOM异常
  • (-Xmx和 -Xms)可以控制堆得大小
1.5方法区
  • 线程共享空间,需要垃圾回收(方法区一般是老年代)
  • 存储类信息(类版本,域,方法,接口),常量(常量池:编译期生成的各种字面常量和符号引用),静态变量,及时编译器JIT编译后的代码数据
  • 当没有更多空间可供分配是就会抛出OOM异常,常量池无法扩展也会有OOM异常

符号引用:例如java.util.List就是符号引用。运行时会翻译成具体的地址指针

2. 内存分配

2.1 创建(new)一个对象的过程

new一个对象的具体过程:

  1. 虚拟机在运行过程中遇到new指令,先判断指令的参数是否能在常量池(方法区)中定位到符号引用,并且检查改符号引用的类是否被加载,解析,和初始化。如果没有,执行类加载过程
  2. 分配内存。对象的大小在类加载过程时就已经更具对象头的信息确认。为新生对象在中分配内存空间(无法分配会有OOM异常)。如果堆中的内存是规整的(经历过复制算法),分配内存就是讲指针往后挪一部分,这种方式称为指针碰撞;如果内存的空间不是规整的(回收用的可能是标记清理算法),就需要维护个表来记录对象分配在那块内存,改方法称为空闲列表
  3. 初始化零值,(这是为什么在我们没有赋默认值时我们的boolean是false,int等是0,String是null的原因。同时也体现了安全性。即使什么都没有做,虚拟机不会返回一个莫名其妙的东西(原先那块内存的数据))
  4. 依据对象头进行设置:例如是否启用偏向锁,是哪个对象的实例,GC分代年龄信息等
  5. 真正初始化。其实就是执行构造函数

额外:
上面提到了,对内存是共享的,而且内存分配可能很多线程同时在做,那么虚拟机是怎么处理的?有两种方案 1.采用CAS加上失败重试保证原子性;2 把内存分配的动作按照线程,划分到不同的空间中,这样就不会互相干扰了(这样应该会有内存碎片的问题),每个线程预先分配一块内存,称为本地线程分配缓冲TLAB,这种需要在虚拟机配置文件指定(-XX:+/-UseTLAB)

CAS:CompareAndSwap
原子性:该操作不可再分割

2.2 对象在内存中的分布

Java对象在虚拟机中分为三部分(都是堆中)
对象头
1. 存储对象自身运行时的数据:hash码,GC分代年龄,锁状态标志位,线程持有的锁,偏向线程ID (线程安全,偏向锁)待补充链接地址
2. 类型指针:指向类元数据的指针 句柄访问和直接指针访问。衔接上文,可以更具类元数据确定类对象的大小。
3. 如果是数组还有记录数组长度的一块数据(这部分数据不是数组没有)
实例数据:包括自己和父类继承的各个字段的具体内容
填充数据:仅起占位符作用,使对象头大小为8字节(64位)的倍数

2.3 对象在内存中的访问

上面介绍的虚拟机栈里面局部变量表存储的对象引就是通过下面两种访问方式

  • 句柄访问 (好处是如果对象实例数据地址改变(垃圾回收会改变),仅改变句柄池地址,不需要改变栈中的Reference)
    句柄访问
  • 直接指针访问(更快,但是如果对象实例数据地址改变,就需要改变栈和堆中的数据)
    直接指针访问

3.垃圾回收

我们的内存并不是无限的,必须将不用的东西进行回收,垃圾回收是Java一大特色(并不是Java独有,Java也不是第一个推出垃圾回收的)。不需要像C++ 那样写析构函数。那么JVM是如何判断该对象可被回收?同时,虚拟机是如何回收的?有哪些垃圾回收器?各自的特色是什么?

3.1 判断是否可回收算法

判断是否是垃圾,或者说是否可回收有两种算法:

  • 引用计数器:给对象添加一个引用计数器(对象头就挺好),每当被引用,计数值+1,取消引用-1 当为0是代表没有引用,可以被回收。这种算法实现简单,效率也比较高;但是如果出现循环引用(两个对象互相仅有对方的引用),会导致对象不会被回收。所以JVM不是采用的这种方式
  • 可达性分析:通过一系列的 GC Root对象作为起点,从这些节点往下搜寻,搜寻走的路径为引用链,如果一个对象没有一条引用链到GC Root,则认为该对象不可达。判定为可回收。

GC ROOT的选取:如果选的GC root本来就是该被回收的,岂不是会把不该回收的对象给回收了。GC Root一般从长寿的对象中选:

  • 虚拟机栈中引用的对象(生命与线程同在)
  • 方法区中类静态属性应用的对象(老年代中的对象)
  • 方法区中常量引用的对象(老年代的对象)
  • 本地方法中Native方法中的对象(与线程同在)

引用的概念

  • 强引用Cat cat = new Cat()这种,只要存在这种引用,该对象就不会被回收
  • 软引用:softReference,有用但是非必须的对象,将要发生溢出是就会被回收
  • 弱引用:WeakReference,只要发生垃圾回收就会被回收
  • 虚引用:PhantomReference仅用来知会什么时候发生了垃圾回收

finalize()方法
这个东西日常和 final,finally一起出面试题
回收一个对象,至少要经历两次标记。但第一次标记后,会进行一次筛选,如果该方法没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机会认为没有必要执行finalize()方法,将对象放入F-Queue队列中(暂时逃过一劫),之后会在该队列中再一次标记,然后就被回收了。finalize()是系统的,没事不要手动调用
回收方法区的常量:当该常量没有被引用就被回收
回收方法区中的类:该类的所有实例已被回收,加载该类的ClassLoader已被回收,且该类的java.lang.Class没有被引用。如果用反射估计是回收不了了

3.2 垃圾回收算法
3.2.1 标记-清除算法
  • 思想:标记需要清除的对象,然后统一回收
  • 缺点:效率不高,而且会有内存碎片
3.2.2 复制算法
  • 思想:将内存分为大小相等两块,平常使用其中一块,如果满了,将存活的对象放到另一块
  • 优点:效率高,同时不会有内存碎片
  • 缺点:空间利用率不高,利用率最大才50%,而且如果存活的对象比较多,复制效率也不高
3.2.3 标记-整理算法
  • 思想:标记要清除的对象,然后将存货的对象移动到一端
  • 有点:解决了内存碎片的问题
  • 缺点:效率比标记清除还低,如果对象多,那不得累死
3.2.4 分代算法
  • 思想:结合了复制算法和分代算法,将堆和方法区需要垃圾回收支持的区域划分为老年代和新生代。有研究表明:大部分对象都是“朝闻道夕死可矣”。将新生代的内存区域划分大些,老年代的内存区域划分小些,新生代使用复制算法,因为新生代存活对象少,复制算法效率比较高,而且可以划分存活空间的大小,提高空间利用率。老年代用标记-整理算法,有效减小空间碎片,提高空间利用率
  • 模型大概是 Eden:survivor from:survivor to: old = 32:4:4:20
  • 其中Eden,两survivor是新生代,old是老年代
  • 复制算法是 把Eden和 from里面存活的对象放到 to 中
  • from 与 to每执行一次复制算法,交换一次位置
  • 如果对象经过多次复制算法,依然存活,大概15岁,则移入老年代
  • 老年代同时起担保人的作用,如果单个对象太大。to放不下会直接放到老年代中。或者说存活对象太多,也会放入老年代中

什么时候触发垃圾回收?

  • 对于Eden来说,如果无法创建新的对象,就会触发垃圾回收(Mintor GC),比较频繁,影响不大
  • 如果老年代满了,会触发Major GC。这个会Stop The World(STP),我们调优很多时候就是为了避免这个(为什么用CMS的理由)。例如不要用太大的对象。
3.3 垃圾收集器

新生代收集器:Serial,ParNew,Parallel Scavenge
老年代收集器:Serial Old,CMS,Parallel Old
新老都可用:G1

3.3.1 Serial收集器
  • 新生代垃圾收集器
  • 复制算法
  • 最基础的垃圾回收,单线程
  • 会STP,但是对于新生代来说,暂停时间比较短
  • 能够与老年代的 CMS,Serial Old配合使用
3.3.2 ParNew收集器
  • 新生代垃圾收集器
  • 复制算法
  • 在Serial基础上改进的,多线程
  • 多CPU,多线程机器上效果比较好
  • 能够与老年代的 CMS与Serial Old配合使用
3.3.3 Parallel Scavenge收集器
  • 新生代垃圾收集器
  • 复制算法
  • 多线程
  • 与ParNew比,更专注于吞吐量
  • 可以自适应调整GC策略
  • 与老年代的Serial Old与Parallel Old配合使用
3.3.4 Serial Old收集器
  • 老年代垃圾收集器
  • 最基础的垃圾收集器,单线程
  • 标记-整理算法,STP时间较长
  • 能够需3总新生代都配合使用
  • 还替CMS兜底(因为CMS用的是标-清算法,有内存碎片)
3.3.5 CMS收集器
  • 老年代收集器
  • 标记清除算法(内存碎片),追求最短STP
  • 过程:初始标记(STP),并发标记,重新标记(STP),并发清除
  • 与Serial和ParNewpei和使用
  • 缺陷:CPU资源敏感浮动垃圾,内存碎片,需要Serial Old 兜底
3.3.6 Parallel Old收集器
  • 老年代收集器
  • 标记整理算法,多线程 追求吞吐量
  • 仅配合Parallel Scavenge配合使用
3.3.7 G1收集器
  • 新生代老年代通用收集器,最前沿的技术
  • 并发与并行:(结合了ParNew的优点)
  • 分代收集
  • 空间整合:不会有内存碎片
  • 可预测的停顿
  • 回收过程:初始标记,并发标记,最终标记,筛选回收与CMS很像

G1干掉了新生代和老年的的严格区分,将内存划分为多个区域(Region),一个区域一个区域的回收,化整为零。优点分段锁的味道

4 小结

  • 对象的创建过程
  • 运行时内存区域:5个区域
  • 4类垃圾回收算法
  • 可达性分析 GC Root
  • 各种垃圾收集器的比较,重点在CMS与G1收集器

应该是目前总结的最好的一篇文章

									    乘风破浪会有时,直挂云帆济沧海
										博主:五更依旧朝花落
发布了35 篇原创文章 · 获赞 19 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/m0_37628958/article/details/105246501
今日推荐