jvm自动内存管理

2021/6/3 7:24:14

本文主要是介绍jvm自动内存管理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 一、java内存区域
    • 1.运行时数据区域
      • 1.1.1 程序计数器
      • 1.1.2 Java 虚拟机栈
      • 1.1.3 本地方法栈
      • 1.1.4 Java 堆
      • 1.1.5 方法区
      • 1.1.6 运行时常量池
      • 1.1.7 直接内存
  • 二、垃圾收集器与内存分配策略
    • 1.对象已死?
      • 2.1.1 引用计数法
      • 2.1.2 可达性分析
      • 2.1.3 软引用 弱引用
    • 2.垃圾收集算法
      • 2.2.1 标记清除 标记复制 标记整理
      • 2.2.2 标记复制为什么比标记整理快?
      • 2.2.3 GC ROOT 枚举、安全点、安全区域
      • 2.2.4 并发可达性分析-三色标记法
      • 2.2.5 分代收集理论
      • 2.2. 记忆集及写屏障
  • 3.垃圾回收器
    • 1. 经典垃圾回收器
    • 2. CMS
    • 3. G1垃圾回收器


一、java内存区域

1.运行时数据区域

1.1.1 程序计数器

内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。

1.1.2 Java 虚拟机栈

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

1.1.3 本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

1.1.4 Java 堆

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

1.1.5 方法区

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

1.1.6 运行时常量池

属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。

1.1.7 直接内存

也叫堆外内存
优点:java堆内部的数据进行远程发送,需要先把堆内部的数据拷贝到直接内存里面,也就是拷贝到堆外内存,然后在发送。如果把对象分配到直接内存里面,发送的时候就可以省掉复制的哪一步操作
缺点:就是没有jvm帮助管理内存,需要我们自己来管理堆外内存,防止内存溢出。
使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引
ByteBuffer buffer = ByteBuffer.allocateDirect(400);
buffer.putInt(j);buffer.getInt();buffer.clear();

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

1.对象已死?

2.1.1 引用计数法

在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一。
缺点:不能解决循环引用问题,如果两个对象都不在引用,但他们相互引用,这时引用计数器值不为0,也不能回收他们

2.1.2 可达性分析

从一些称为GC ROOT的根节点开始,根据引用关系(主动引用)向下搜索,没有经过引用链的对象为不再使用的对象,可被回收。
注意:引用时有向的。在理解可达性分析的图中,如果图像显示 A–>B,代表对象A引用对象B
可作为GC ROOT的对象: 全局性的引用(例如常量或类静态属性)与执行上下文(例如 栈帧中的本地变量表)

2.1.3 软引用 弱引用

在内存空间紧张的时候,为不抛出oom异常,可以回收这些对象
软引用(SoftReference类) 内存不足即回收,。典型应用为加载外部的可再生资源。如加载外部文件,如果内存充足,则不需要再次加载,如果内存紧张,软引用被回收,则下次使用时候再次加载。
弱引用(WeakReference)下一次垃圾回收即被清理。也可用做缓存,由于垃圾回收线程优先级比较低,在下一次垃圾回收之前还是可以使用该对象的(相比无引用 =null 来说,保存着弱引用,还可以用WeakReference.get()获取该对象)

2.垃圾收集算法

2.2.1 标记清除 标记复制 标记整理

标记清除:快速,但产生很多内存碎片
标记复制:标记快速,但要牺牲内存为代价 典型场景 eden : from :to =8 :1 :1
标记整理:慢,没有内存牺牲,没内存碎片

2.2.2 标记复制为什么比标记整理快?

先了解这两种算法的典型实现:
标记整理的典型实现是Lisp2 算法:
1.标记阶段:遍历所有的存活对象,与活动对象数成正比
2.设置forwarding指针阶段:扫描整个堆。
3.更改子对象指针阶段:扫描整个堆。
4.移动阶段:扫描整个堆。内存间的复制

标记复制算法:
复制时,需从GC ROOTS开始遍历对象图,对每一个存活的对象进行复制;复制后对象地址改变,还需要更新GC ROOTS引用的地址

标记复制比标记整理快的原因:
1.吞吐量高,不需要遍历全堆,只需要处理活动对象
2.分配速度快,和free-list分配法相比,顺序分配不需要搜索free-list,只需要移动free pointer即可(使用bitmap标记的堆所用时间和空间开销会小于排序树)

2.2.3 GC ROOT 枚举、安全点、安全区域

根节点枚举必须在一个一致性快照中进行,用户现场需暂停(STW)。
虚拟机用oop Map 数据结构提前把这些根节点保存下来,不用去遍历堆查找这些对象。

导致oop Map变化的指令很多,不会为每个指令都生成oop Map。只有在特定指令才生成oop map.这些位置称为安全点。虚拟机不能随意停止进行垃圾回收,必须在安全点位置停顿进行垃圾回收。

一段代码里,引用关系不会发生变化,这个区域中的任意位置停下来进行垃圾回收都是安全的,这段代码称为安全区域。

2.2.4 并发可达性分析-三色标记法

可达性分析算法理论上要求在一致性快照中进行,则全部用户线程必须停止。
为了垃圾回收线程跟工作线程并发工作,引入了三色标记法
白色:表示对象尚未被垃圾收集器访问过。在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。
灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

如果把白色的标错为黑色的,最多该垃圾这次不被回收,可容忍。
如果把黑色的错标为白色的,把有用对象当作垃圾回收了,致命,此时需满足条件:
1.插入了一条或多条从黑色到白色的新引用
2.删除了全部从灰色到该白色的对象的直接或间接引用 (可以原本就没有吗?那就不需要删除了。 我认为原本不会没有,如果没有,那么条件1不能实现)
破坏任一条件即可,有增量跟新和原始快照
增量跟新:破坏条件1。当黑色插入一条新的指向白色的引用,在扫描之后,以这些黑色对象为跟节点,重新扫描
原始快照:破坏条件2。当灰色删除一条白色的引用,在扫描之后,将这些记录过的引用关系中的灰色对象为根,重新扫描一次 (注意是扫描的原来的引用关系)

2.2.5 分代收集理论

在新生代的对象,大部分是短时间内会被回收的。在老年代中的对象,由于熬过了多次垃圾回收,是很少被回收的。
所以,jvm区分了新手代跟老年代。一般情况,只需要回收新生代的垃圾,因为这里的垃圾多,回收所带来的回报大。
另外,由于新生代所用的回收算法是标记清除,老年代所用的算法一般是标记整理,所以回收新生代比回收老年代效率高。

2.2. 记忆集及写屏障

由于存在跨代引用的,典型的是老年代引用了新生代的对象,在收集新生代的垃圾时,如果只遍历新生代的gc root会存在问题。
引入记忆集的方式解决这种问题,即如果老年代引用了新生代的对象,把这个引用标记出来,放到记忆集中。
在实现过程,一般采用老年代分区的方式,即把老年代划分成大小相同的内存区(如128KB),如果老年代的某一个区引用了新生代,则把这个区标记为dirty。在回收新生代时,把这些标记为dirty的区域的也加入gc root扫描。(注意加入扫描的gc root是这些dirty区中具体引用了新手代对象的老年代对象,而不是这些drity区域中的gc root)

写屏障,记忆集卡的具体实现,跟并发没什么关系。
即在给老年代对象赋值的时候,也要同步跟新记忆集卡里的内容。
可以理解为面向切面的aop形式,jvm为每个赋值语句增加一个跟新新记忆集卡的操作

3.垃圾回收器

1. 经典垃圾回收器

有serial , serial old ,parNew, parallel scavenge, paralle old 这几款。
在这里插入图片描述
如图为serial 的工作图,其中,serial 为单线程垃圾回收器,采用标记复制算法。
内存利用率可达90%, 其中 eden:from survial :to survial = 8: 1 : 1 。
serial 为单线程垃圾回收器,采用标记整理算法。
parallel跟serial工作原理差不多,只不过parallel 的gc 线程采用的是多线程的方式。

Parallel Scavenge自适应调节策略,吞吐量优先。ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。Serial Old作为CMS收集器的后备方案。

2. CMS

在这里插入图片描述
CMS由四个部分组成:
1.初始标记,标记gc root ,要STW.
2.并发标记,查找引用链,gc线程跟用户线程并发工作,使用了三色标记法
3.重新标记,由于在并发标记中,会有不可回收对象被误当作垃圾,这是要重新标记这部分,cms采用的是增量法。
4.并发清除,会产生内存碎片。

CMS减低了停顿时间,但总的吞吐量是降低的。
在并发标记及并发清除阶段,仍会有垃圾产生,这部分垃圾在下次垃圾回收才被清理,称为浮动垃圾。
由于用户线程跟gc线程并行工作,CMS不能像其他垃圾回收器那样,等老年代满了再回收,要预留一定的内存空间。
若预留太多,则内存回收频率会增大。若预留太少,则可能在并发过程,会导致用户线程得不到足够的内存,触发serial old垃圾回收。

3. G1垃圾回收器

garbage first垃圾回收器,相比于CMS,最大的不同就是把内存区域分成大小相同的region。
如图,G1不再单纯的区分新生代跟老年代。仍然属于分代收集器,但每个区域可以是eden,survivor,old,humongous
在这里插入图片描述
humongous专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,G1大多数把humongous当作老年代处理。
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,优先处理回收价值收益最大的那些Region,这也就是“GarbageFirst”名字的由来。
从局部看,G1是复制算法,从一个region复制到另一个region。从全局看,G1仍是整理算法。



这篇关于jvm自动内存管理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程