第三章 垃圾收集器与内存分配策略

概述

​ 程序计数器、虚拟机栈、本地方法栈3个区随线程而生,随线程而灭。因此大体上可认为这几个区域的内存分配和回收都具备确定性。在方法/线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

引用计数算法

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

​ 但是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

可达性分析算法

​ 主流商业程序语言的主流实现中,都是称通过可达性分析来判定对象是否存活的。

​ 基本思想:通过一系列的被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当某个对象的引用链不存在时,说明该对象不可用。

哪些对象可以作为GC Roots?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即Native方法)引用的对象

关于引用

JDK1.2之后,Java对引用进行了扩充,引用被分为了强引用、软引用、弱引用、虚引用。

  • 强引用: 在程序代码中普遍存在的,如New出来的引用,垃圾收集器不会回收。
  • 软引用: 描述有用但并非必需的对象。在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用: 描述非必需对象。但强度弱于软引用。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
  • 虚引用: 也称幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

生存还是死亡

​ 即使对象被分析为不可达,也并非一定会被回收。一个对象要真正被回收,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

​ 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法只会被系统自动调用一次,是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

回收方法区

​ 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

什么才是无用的类

  • 该类所有的实例都已经被回收

  • 加载该类的ClassLoader已经被回收

  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

    和对象的区别在于,无用的类仅仅是可以被回收,而不是像对象那样不使用了就必然回收。

垃圾收集算法

标记-清除算法

​ 算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

​ 不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

​ 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

​ 不足:将内存缩小为了原来的一半,代价过高。

PS:现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

标记-整理算法

​ 针对老年代,标记-整理算法是更好的选择。标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

​ 当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

深入理解java虚拟机学习笔记(二)的更多相关文章

  1. 深入理解java虚拟机学习笔记(二)垃圾回收策略

    上篇文章介绍了JVM内存模型的相关知识,其实还有些内容可以更深入的介绍下,比如运行时常量池的动态插入,直接内存等,后期抽空再完善下上篇博客,今天来介绍下JVM中的一些垃圾回收策略.        一. ...

  2. 深入理解java虚拟机学习笔记(一)JVM内存模型

    上周末搬家后,家里的宽带一直没弄好,跟电信客服反映了N遍了终于约了个师傅明天早上来迁移宽带,可以结束一个多星期没网的痛苦日子了.这段时间也是各种忙,都一个星期没更新博客了,再不写之前那种状态和激情都要 ...

  3. 深入理解Java虚拟机学习笔记(二)-----垃圾收集器与内存分配策略

    写在前面 本节常见面试题: 如何判断对象是否死亡(两种方法). 简单的介绍一下强引用.软引用.弱引用.虚引用(虚引用与软引用和弱引用的区别.使用软引用能带来的好处). 如何判断一个常量是废弃常量 如何 ...

  4. 深入理解Java虚拟机学习笔记(一)-----Java内存区域

    一 概述 对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题 ...

  5. 深入理解Java虚拟机 - 学习笔记 1

    Java内存区域 程序计数器 (Program Counter Register) 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器.在虚拟机的概念模型里,字节码解释器工作时就是通过 ...

  6. 深入理解Java虚拟机--阅读笔记二

    垃圾收集器与内存分配策略 一.判断对象是否已死 1.垃圾收集器在对堆进行回收前,要先判断对象是否已死.而判断的算法有引用计数算法和可达性分析算法: 2.引用计数算法是给对象添加引用计数器,有地方引用就 ...

  7. 深入理解java虚拟机学习笔记(一)

    第二章 Java内存区域与内存溢出异常 运行时数据区域 程序计数器(Program Counter Register) 程序计数器:当前线程所执行的字节码行号指示器.各条线程之间计数器互不影响,独立存 ...

  8. 深入理解Java虚拟机学习笔记(三)-----类文件结构/虚拟机类加载机制

    第6章 类文件结构 1. 无关性 各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(即扩展名为 .class 的文件) 是构成平台无关性的基石. 字节码(即扩展名为 .class 的文 ...

  9. 类加载机制(深入理解JAVA虚拟机学习笔记)

    1.类加载机制的定义 将class文件加载到内存,然后对class文件中的数据进行校验.解析和初始化,转换成可以被虚拟机直接使用的JAVA类型,这就是虚拟机的类加载机制.(在JAVA中,类的加载.连接 ...

随机推荐

  1. Unity透明视频播放 所需的Shader脚本

    Shader "Custom/ShaderMovie" { Properties { _MainTex("Color (RGB)", 2D) = "b ...

  2. oracle入门(一)

    ### 一.体系结构 1. 数据库 : 只有一个数据库 2. 实例 : 后台运行的一个进程 3. 表空间: 逻辑存储单位 4. 数据文件: 物理存储单位 5. 用户:面向用户管理,由用户来管理表空间, ...

  3. 【POJ 3368】Frequent values(RMQ)

    Description You are given a sequence of n integers a1 , a2 , ... , an in non-decreasing order. In ad ...

  4. ubuntu18.04错误配置变量环境导致无法进入系统

    1.问题描述 错误配置环境变量(直接在/etc/profile文件末尾添加了export xxx),关机后一直在登录界面循环无法进入系统. ###环境变量的添加是在原有变量之后以冒号(:)分隔加入,并 ...

  5. md5加密+盐方式一

    这种方法是采用随机生成盐值加入password中组合成的新密码,下面是md5+盐的一个工具类,直接导入使用即可! 工具类 package com.oracle.utils; import java.s ...

  6. react组件间传值详解

    一.父子组件间传值     <1>父传子         父组件:

  7. 【Spark】源码分析之SparkContext

    一.概述 SaprkContext非常重要,是Spark提交任务到集群的入口 SparkContext中没有main方法,在SparkContext主构造器中,主要做一下四件事情: 1. 调用crea ...

  8. 关于解决 https 网站无法加载 http 脚本

    前几天刚配置好https网站 然后今天浏览发现自己网站的地图插件不见了 然后看了一下报错显示 然后百度搜索一番找到了解决办法 <meta http-equiv="Content-Sec ...

  9. spark ---词频统计(二)

    利用python来操作spark的词频统计,现将过程分享如下: 1.新建项目:(这里是在已有的项目中创建的,可单独创建wordcount项目) ①新建txt文件: wordcount.txt (文件内 ...

  10. Go语言入门(一)特性、安装、环境搭建、第一个程序等

    Go语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性.谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发Go,是因为过去10多年间软件开发的难度令人沮 ...