JVM 2.JVM垃圾回收
0 思考
如果我们抛开JVM对垃圾回收的实现,如果让我们自己来进行垃圾回收,我们会怎么做呢?
- 首先我们要知道在哪里进行垃圾回收,也就是哪里会产生垃圾 堆
- 我们要判别哪些是垃圾 没被引用的对象是垃圾
- 我们如何把“垃圾对象”所占用空间释放出来 类似操作系统,把这块内存标记为空闲内存
- 如何又好有快的完成回收 既不产生碎片,又不损耗性能 各种算法
以上都是我自己想的答案,接下来我们去看下JVM是怎么解决这些问题的
1. 哪些对象是垃圾?
我们有许多方法识别哪些对象是垃圾
1.1 引用计数法
给对象添加一个引用计数器,每当有一个地方引用它,计数器加1,引用失效时,计数器间1,计数器为0,就代表它暂时是个垃圾。但JVM没用使用这种方法 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CiBgnX5Z-1640184180408)(C:\Users\55488\AppData\Roaming\Typora\typora-user-images\image-20211221223309857.png)]
如果采用引用计数法,就不能正常回收。
1.2 可达性分析算法
基本思想:通过一些列被称为==“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为应用链==,当一个对象无法被GC Roots 到达时,就是垃圾。
在Java中,可做为GC Roots的对象主要在全局性应用和执行上下文中,有:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即Native方法)引用的对象
1.2.0 HotSpot JVM中的可达性分析算法
上面的可达性分析算法有个一个大问题,方法区太大了,找到所有的对象和GCRoots对象需要的时间太久了。更致命的是,我们在进行GC是系统还能正常运行吗? 显然不能,这些对象的状态都不能发生改变了,也就是所谓的一致性快照。
如果让我们自己解决这个问题,我们会怎么解决呢?
让我来的话,我会在一个Map<引用,地址>把每个引用和他对于的地址存下来,每有新的对象,就更新这个Map。
这正确吗? 只正确了一半
JVM中确实是用了一个 叫做OopMap的Map结构来存储哪个对象引用指向了哪个位置的对象。但他是每有引用变动,就更新吗? 不是的
因为这样需要进行大量的操作,消耗大量资源。
JVM 中定义了==安全点(Safepoint)==来解决这个问题,程序会在安全点进行对OopMap的操作更新。然后只有在安全点才能进行GC。
注意安全点是指令级别的操作了,是JVM 直接写入 字节码文件
有了安全点我们就高枕无忧了吗? 不是的
安全点机制要求如果需要进行GC,那么所有的线程都会在最近的安全点停下,等待GC,这是一个动态的过程。那么如果有的线程没办法到安全点呢?比如阻塞的线程。其实这些阻塞的线程已经满足垃圾回收的条件了,那就是他的状态确实不会变了。所以我们称他是在安全区的。安全区可以看作扩展的安全点。
1.3 什么是引用
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,则称这块内存代表这一个引用,也就是这个reference类型的数据代表一个引用。
JDK1.2后对引用进行了扩充
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l0QThVtk-1640184180410)(C:\Users\55488\AppData\Roaming\Typora\typora-user-images\image-20211221224518836.png)]
虚引用相当于一个报警器,当这个对象被回收时,会发出报警
1.4 finalize
现在我们知道了哪些对象是可以进行回收的,那么 JVM 一定会在垃圾回收时回收这些对象吗?
不一定!
如果一个对象实现了且没有执行过finalize()方法,那么他会被放入F-Queue中去执行他的finalize方法,如果执行完,他还是GC Roots不可达,那么就会被回收掉。
怎么判断有没有执行过呢?
这个对象被放入F-Queue前,会给他一个标记。下次再回收到他,看到这个标记,就直接回收了。
F-Queue队列将交给一个 低优先级的Finalizer线程去执行。也就是如何里面出问题 了,也要保证不影响别的地方
这个功能建议不用
1.5 方法区中的垃圾
除了堆里面,方法区中也有垃圾。
这里以1.8为例,之前的大家看下图也就知道了。
1.8中除去堆的部分,方法区就剩下元空间了。
2.垃圾收集
JVM为了实现我们上面提到的问题中的 3 和 4 。
在进行垃圾回收前又进行了垃圾的收集,非常合理
那么如何进行垃圾收集呢?
2.1 标记-清除算法
属于收集算法的基本款,在前面讲到的应用技术法的基础上进行。但显然无法满足我们 问题4 中要用高效还要没有内存碎片的要求。
2.2 复制算法
把内存分为大小相等的两块,每次只用一块,当不够的时候,把还存活的对象复制到另外一块上,放整齐。然后对这一块进行全部回收。代价:牺牲一半的内存,这会导致垃圾回收的频率加大,再加上要复制,效率会有所降低,不过好在每次活下来的对象很少很少,所以总体来讲,效率不错,效果也不错。
因为我们现在主要使用的HotSpot JVM 就用的这种方法来回收新生代,并且更加优化,我们在对HotSpot 中的复制算法做详细解释。
2.3 HotSpot JVM 中的 复制算法
我们知道新时代的垃圾回收后,存活下来的对象会很少,明显不需要一半的内存时存放。所以我们做了点优化,把存放幸存对象的区域缩小了,而且分成了两个。至于为什么分成两个,请看。
- Eden 区放新进入新生代的对象
- survivor 1 放 从之前的垃圾回收中存活下来的对象
- survivor 2 为空 相当于 原始的复制算法的内一半空的区域,用于存放存活下来的对象。
那么对新生代进行垃圾回收时,会将Eden区和survivor 1 区存活下来的对象会进入 survivor 2 区放整齐。那么为了保证survivor2 保存空,是不是垃圾回收后我们还要将survivor2 区的对象放入 survivor 1区呢? 不是这样的 这样太慢了,JVM 直接把survivor 2 改名为survivor 1 ,把原来的surviror 1 改为survivor 2 。这样更高效
然后在一次垃圾回收后,在交换survivor区前,如果survivor 2 也满了,那么就把里面的对象放入老年代。
2.4 标记-整理算法
在新生代我们之所以可以使用复制算法,是因为新生代存活率很低,即使要复制,复制的也不多。在HotSpot 中 甚至我们还可以只浪费10%的空间。
但如果我们对象存活率很高呢?那么需要留给复制的空间就要很多,极端情况下 100%存活率,那么我们就需要浪费50%的空间,好要花大量时间复制。亏爆了。
那么哪里对象存活率高呢?老年代,他来了。
标记整理算法就是在标记清除算法的基础上,加了整理这个过程,先把存活的对象都整齐摆好,然后在回收,这样就没有碎片了。
2.5 分代收集算法
不算一个单独的算法。就是把堆分为新生代,老年代,新生代用复制,老年代用标记整理。
3.垃圾收集器
垃圾收集器是对上面垃圾回收算法的具体实现和应用。
每种垃圾回收器都有自己的优缺点,适合不同的场景,通常可以配合使用。真正的垃圾回收流程也会因为垃圾收集器的选择而产生不同。这里不做详细解释。
4.内存分配与回收策略
我们上面讨论了对象内存的回收,下面我们开始聊聊对象内存的分配。
4.1 对象内存分配在哪?
这个问题好像很简单,新对象的内存分配肯定在新生代的Eden区呀,但真实情况下一定是这样吗?
**首先,对象确实优先在Eden分配。**当Eden区空间不够的时候,就触发一次Minor GC,然后再试图放入Eden。这里,从Eden区被转入Survivor 1 区的对象,如果能放入,则放入,否则会直接进老年代。
大对象直接进入老年代。这么做是为了避免Eden区及两个Survivor区之间发生大量的内存复制。
长期存活的对象将进入老年代。这个很好理解,虚拟机给每个对象了一个Age计数器,在survivor中每活过一次垃圾回收,就加一,到达一定数量(默认15,通过-XX:MaxTenuringThreshold调整)会被放入老年代。
Survivor中相同年龄的对象所占用内存大小和超过survivor区的一半超过该年龄的对象会之间进入老年代
4.2 空间分配担保
大白话,就是一次minor GC 后存活下来的对象的内存大于survivor 2 区中的内存,那么就会把超出的部分之间放入老年代。那么理所当然的,在放入老年代前,要看下够不够放,如果够就可以放,如果不够还要看是否允许失败,不允许的化,就进行一次Full GC,允许的话,就去根据以往的经验推测下能否成功,推测可以就尝试Minor GC,不可以就Full GC。
这部分很简单,就不长篇大论了,大家去看下书就明白了。
这种机制的存在,是为了防止一次突发的Minor GC后大量对象存活,导致一次Full GC。 也就是减少Full GC频率。 以放,如果不够还要看是否允许失败,不允许的化,就进行一次Full GC,允许的话,就去根据以往的经验推测下能否成功,推测可以就尝试Minor GC,不可以就Full GC。
这部分很简单,就不长篇大论了,大家去看下书就明白了。
这种机制的存在,是为了防止一次突发的Minor GC后大量对象存活,导致一次Full GC。 也就是减少Full GC频率。
|