JVM笔记(一):自动内存管理机制

Java运行时数据区

jvm1

线程私有:

  1. 程序计数器:当前线程所执行的字节码的行号指示器。字节码解释器通过程序计数器的值来取下一条需要执行的字节码指令。
  2. 虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量变存放各种基本数据类型(boolean、byte、char、short、int、float、long、double)和对象引用(引用指针)。
  3. 本地方法栈:与虚拟机栈类似,不过描述的是Native方法执行的内存模型(可能是其他语言的本地方法库)。

线程共享:

  1. Java堆:用以存放对象实例和数组的内存区域。也是垃圾收集的主要区域,故也称GC堆。在物理上,Java堆的内存空间可以是不连续的,只要逻辑上是连续的即可;在实现上,可以是扩展的也可以是固定的。
  2. 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。有的人称为“永久代”。这个区域不需要连续的内存区域,以及可以选择固定大小或者可扩展,还可以选择不实现垃圾回收。方法区的垃圾回收主要针对常量池(存放编译期生成的字面量和符号引用)的回收和对类型的卸载。

虚拟机对象

对象的创建

在虚拟机中,对象的创建过程:

  1. 遇到一个new指令后,检查:
    • 指令的参数是否能在常量池中定位到一个类的符号引用。
    • 这个符号引用代表的类是否已被加载、解析和初始化。没有则执行相应的类加载过程。
  2. 为新生对象分配内存。分配方式有:
    • 指针碰撞:Java堆中的内存是绝对规整的,一边是已分配区域,一边是空闲区域,移动中间的指针进行分配。
    • 空闲列表:Java堆中的内存是不规整的。虚拟机维护一个列表记录空闲的内存块。
  3. 内存分配完后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头),这样使得对象实例字段可以不赋初值就可以直接使用(比如int型字段初始为0)。
  4. 虚拟机为对象进行必要的设置,比如为对象头的内容进行设置。

对象的内存布局

对象的内存布局分为3块区域:对象头、实例数据、对齐填充。

  1. 对象头。分为两部分:
    • 运行时数据:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。
    • 类型指针:对象指向它的类元数据的指针。
  2. 实例数据:真正存储的有效信息,也是在程序代码中定义的各种类型的字段内容(包括从父类继承下来的)。
  3. 对齐填充:只起到占位符的作用,系统要求对象的大小必须是8字节的整数倍。

对象的访问定位

Java程序通过虚拟机栈中的局部变量表保存的对象引用(reference)来操作具体对象。通过reference类型的数据访问对象的方式主要有两种:使用句柄和直接指针。

使用句柄

使用句柄的访问方式时,Java堆会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含对象实例数据与类型数据。这种方式的好处是reference存储的稳定的句柄地址,当对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。

img

直接指针

使用直接指针方式,reference中存储的直接就是对象地址。这种方式的好处是速度更快,只需要一次指针定位。

jvm3

关于引用

Java将引用分为强引用、软引用、弱引用、虚引用。

  1. 强引用:在代码中普遍存在的,类似于Object obj = new Object()这类的引用。
  2. 软引用:用来描述 一些还有用但并非必需的对象。对于软引用的对象,在系统将要发生内存溢出异常之前, 将会把这些对象列进回收范围之中进行第二次回收。
  3. 弱引用:也是用来描述非必需对象的,而且比软引用更弱一些,当垃圾收集器工作时,无论当前内存是否足够,都会 回收掉只被弱引用关联的对象。
  4. 虚引用:虚引用是最弱的一种引用关系。 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

回收对象判断

垃圾收集器在对堆进行回收之前,先要判断哪些对象已死,哪些对象还存活着。判断对象是否存活主要有两种方法:引用计数算法和可达性分析算法。主流的程序语言一般都是采用可达性分析算法来实现,包括Java。

  1. 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;引用失效时,计数器值就减1,计数器为0的对象就是不可使用的。这种方法实现简单、效率高,但很难解决对象之间相互循环引用的问题。
  2. 可达性分析算法:通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(即GC Roots到这个对象不可达) 时, 则证明此对象是不可用的。

jvm4

在可达性分析算法中不可达的对象, 不会立即被回收,要真正宣告一个对象死亡,至少要经历两次标记过程:

  1. 第一次标记:对象在进行可达性分析后发现不可达,将会被第一次标记并且进行一次筛选,即此对象是否有必要执行finalize() 方法:
    • 没有必要执行:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过。
    • 有必要执行:这个对象将放置在F-Queue队列之中,并在稍后由低优先级的Finalizer线程去执行它。
  2. 第二次标记:在执行finalize()后,GC将对F-Queue中的对象进行第二次小规模的标记:
    • 如果对象在finalize()中重新与引用链上的任何一个对象建立关联(如把自己( this 关键字)赋值给某个类变量或者对象的成员变量),那在第二次标记时它将被移除出“ 即将回收” 的集合。
    • 如果对象没有关联上引用链上的对象,那基本上就真的被回收了。

Note:
可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,一般包括:

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

回收方法区

方法区(或者HotSpot虚拟机中的永久代)的垃圾收集通常效率很低,在Java虚拟机规范中可以不要求垃圾收集。方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类。

  1. 废弃常量:以常量池中字面量的回收为例,假如一个字符串”abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做”abc”的,即没有任何String对象引用常量池中的”abc”常量,这个时候,这个” abc” 常量可以被清理 出常量池。常量池中的其他类( 接口)、方法、字段的符号引用也与此类似。
  2. 无用类:虚拟机可以对判定为无用的类进行回收,判定一个类是“无用的类”的条件:
    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记-清除算法

标记-清除(Mark-Sweep)算法分为标记和清除两个阶段:

  1. 标记出所有需要回收的对象,一般用可达性分析算法进行标记。
  2. 对所有被标记的对象进行统一回收。

这种算法的不足主要有两个:

  • 效率问题。标记、清除的效率都不高。
  • 空间问题。标记清除后会产生大量不连续的内存碎片。

复制算法

复制(Copying)算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

复制算法在当前商用的虚拟机中一般用来回收新生代,将新生代的内存区域分为三个部分,一个Eden空间,两个Survivor空间:

  • Eden空间,占80%。
  • Survivor空间:From 和 To,占20%。

内存分配时,只使用Eden和其中一块Survivor。当回收时,将Eden和一块Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。根据研究表明,新生代的对象98%会在第一次GC中被回收,因此将活着的对象复制到一块Survivor中是可行的,但如果Survivor空间不够用,则需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

标记-整理算法

标记-整理( Mark- Compact)算法,主要适用于老年代。标记过程仍然与“ 标记-清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

  • 新生代:复制算法。
  • 老年代:标记—清理 或者 标记—整理
您的支持将鼓励我继续创作!