[读书笔记]深入理解Java虚拟机——内存自动管理

Java内存区域与内存溢出异常

内存模型

  • 程序计数器

    Program Counter Register 线程私有,较小的内存空间,当前线程所执行的字节码行号指示器。

  • Java虚拟机栈

    Java Virtual Machine Stack 线程私有,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

    局部变量表:

    存储基本数据类型对象引用(指向对象起始地址的指针or代表对象的句柄or与对象相关的位置)、returnAddress类型(一个字节码指令的地址)。

    存储空间以局部变量槽(Slot)分割,64位的long和double用2个槽,其他用一个。

    所需的内存空间均在编译期间分配

    线程请求的栈深度超过虚拟机允许,抛出StackOverflowError异常;如果栈可以动态扩展,则会在无法申请到足够内存时抛出OutOfMemoryError异常。

  • 本地方法栈

    Native Method Stacks 与JVM Stack相似,只不过时为本地方法服务。

  • Java堆

    Java Heap 是虚拟机所管理的内存中最大的,被所有线程共享,作用是存放对象实例。“几乎”所有对象实例都在此存放。

    Java堆是垃圾收集器管理的内存区域,因此被称为GC(Garbage Collected Heap)。

    堆物理可以不连续,但逻辑上应该被视为是连续的。

    当前主流堆都是可扩展的,当堆没有内存可以分配且不能扩展时,抛出OutOfMemoryError异常。

  • 方法区

    Method Area 被所有线程共享,存储已被加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

    垃圾回收比较少见,主要针对常量池的回收和类型的卸载。

    运行时常量池:Runtime Constant Pool,存放编译期生成的各种字面量和符号引用、由符号引用翻译出来的直接引用。

对象的内存布局

在HotSpot虚拟机中,对象存储在堆中可以划分为:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

  • 对象头:包括

    • 存储对象自身的运行时数据Mark Word,例如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;考虑到虚拟机的空间效率Mark Word被设计为一个有动态定义的数据结构。

    • 类型指针,对象指向它类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个对象的实例。

    • 如果对象是一个数组,还需要记录数组的长度。

  • 实例数据:在代码中定义的各种类型的字段内容。

  • 对齐填充:HotSpot要求对象起始地址是8字节的整数倍。

对象的访问定位

Java程序通过栈上的reference数据来操作堆上的具体对象。方法有句柄和直接指针两种:

  • 句柄:在Java堆中划出一块内存存储句柄,reference指向句柄。好处是在对象被移动时(垃圾回收时普遍发送)只需要修改句柄中指针,而不修改reference本身。

  • 直接指针:reference直接指向实例数据,此时对象内部需要存储访问类型数据的相关信息。好处是节约了一次指针定位的开销。

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

程序计数器、虚拟机栈、本地方法栈在方法结束或线程结束时,内存自然回收;

JAVA堆和方法区,在运行期间动态分配和回收内存。

判断方法

  • 引用计数法

    Reference Counting 额外占用内存空间进行计数,(但是主流Java虚拟机没有用引用计数法);

    弊端:循环引用导致计数永远不为零。

  • 可达性算法

    Reachability Analysis (Java,C#使用);

    使用GC Roots作为起始节点集,根据引用关系向下搜索,不可达的就被判断为可回收对象;

    GC Roots可以是虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量应用的对象、本地方法栈中引用的对象、虚拟机内部的引用(Class对象、异常对象、类加载器等)、被synchronized关键字持有的对象、虚拟机内贸部注册的回调缓存等。

引用(强软弱虚)

强引用:类似于传统引用的定义,只要强引用关系存在,就不会被回收;

软引用:还有一定作用,但不是必须的对象。在内存Out Of Memory之前会被回收,如果回收之后还没有足够内存,才抛出溢出异常;

弱引用:非必须对象,存活到下一次垃圾收集时,无论内存是否足够,都会回收掉弱引用对象;

虚引用:虚引用不会对对象生存时间有影响,也无法通过虚引用获得对象实例,只能用于在对象被回收时收到一个系统通知。

两次标记

在对象可达性分析后,且没有与GC Roots相连接的引用链,就会被第一次标记,然后进行筛选:

 if 对象没有覆盖finalize()方法 or finalize()方法已经被虚拟机调用过一次
  then 判断为 没有必要执行 finalize()方法
  else 有必要执行finalize()方法,对象会被放在一个F-Queue队列中,并被虚拟机建立的、低调度优先级的Finalizer线程执行它的finalize()方法(仅触发运行,不等待结束),然后收集器会对F-Queue中对象进行第二次标记

这时,如果要拯救自己,则可以在finalize()函数中将自己和引用链上的一个对象关联即可

两次标记后,对象才会被真正地回收。

方法区回收

主要回收:废弃的常量和不再使用的类型

不再使用的类(只是被允许回收,不是一定会回收):

  • 堆中不存在该类和任何派生子类的实例

  • 加载该类的类加载器已经被回收(通常很难达成)

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

垃圾收集算法

下列均为”追踪式垃圾收集“(Tracing GC)

分代收集理论

大多数商业虚拟机使用,建立在两个假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的

  • 强分代假说:熬过越多次垃圾收集的对象越难以消亡

  • 跨代引用假说:跨代引用相对于同代引用仅占少数

由前两个假说得出的设计原则:收集器将堆划分为多个区域,按垃圾收集后存活次数来分配对象存储区域。垃圾收集器每次回收其中一个或某些部分的区域。

根据第三条假说:我们不会为了少量跨代引用去扫描整个老年代,而是在新生代维护一个数据结构”记忆集”Remembered Set,标志出老年代哪一块内存由跨代引用,Minor GC时只有跨代引用的内存对象才会被加入扫描。

  • Minor GC:新生代收集

  • Major GC:老年代收集(通常没有单独收集老年代,只有CMDS收集器会)

  • Mixed GC:收集整个新生代和部分老年代(只有G1收集器会)

  • Full GC:收集整个堆和方法区

标记-清除算法

最早出现也是最基础的算法。标记需要回收的对象,然后统一回收;或标记存活的对象, 然后统一回收未标记的对象。

缺点有:1.效率不稳定;2.产生内存空间碎片

标记-复制算法

理论:将内存分为等大小的两块,一块用完了就将存活对象复制到另一块上,在将用过的一块全部清理。

实现简单,运行高效,但空间浪费。

现在商用虚拟机大多采用这种方法回收新生代。

Appel式回收:按照弱分代假说,HotSpot虚拟机将空间分为Eden:Survivor:Survivor = 8:1:1,每次垃圾收集时,将Eden和一块Survivor中存活的对象复制到另一块Survivor上。如果一块Survivor不够容纳所有存活对象,可以去其他内存区域(大多是老年代)进行分配担保,将对象放入老年代。

标记-整理算法

针对老年代的特征,让所有存活对象向一段移动,并清理边界以外的内存

弊端:存活对象移动需要暂停用户应用程序(标记-清除也需要暂停,但时间相对较短)

回收策略

  • 对象优先在新生代Eden分配,当Eden没有足够空间时,虚拟机发起一次Minor GC.

  • 大对象直接进入老年代,避免在Eden和Survivor之间来回复制。

  • 长期存活对象进入老年代,默认为存活15次。

  • 动态对象年龄判定,如果Survivor中相同年龄的对象大小总和大于Survivor空间的一半,大于这个年龄的所有对象都可以直接进入老年代。

  • 空间分配担保,在Minor GC之前

     if 老年代最大连续可用空间大于新生代所有对象总空间
      then Minor GC安全
     else if 参数设置允许担保失败 and 老年代最大连续可用空间大于历次晋升到老年代对象的平均大小
      then 尝试 Minor GC
     else 空间小于 or 参数不允许担保失败
      then Full GC

     

posted @ 2022-03-02 12:45  kubab  阅读(32)  评论(0编辑  收藏  举报