JVM学习之:运行时数据区和垃圾回收

  1. Java虚拟机运行时数据区:器区堆二栈

    1. 程序计数器:当前线程所执行的字节码指令的行号指示器,字节码解释器工作时就是通   过改变这个计数器的值来选取下一条要执行的字节码指令。

* 占用内存较小

* 线程私有

* 唯一一 个在Java虚拟机规范中没有规定OutOfMemoryError情况的区域

    1. 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代   码等数据。

* 线程共享

* 必要的内存回收:常量池的回收(废旧常量)和类型的卸载(无用的类:没有引用;类加载器已被回收;该类的所有实例已被回收)

    1. 堆:用于存放对象实例,所有对象实例以及数组都要在堆上分配

* 占用内存最大

* 线程共享

* 垃圾收集器管理的主要区域

* 可分为新生代和老年代

* 可以处于物理上不连续的内存空间中,只要逻辑上时连续的

    1. 虚拟机栈:用于存储局部变量表 、操作数栈、动态链接、方法出口等信息。

* 线程私有

    1. 本地方法栈:执行Native方法的虚拟机栈

  1. 虚拟机中对象的创建

1)定位类的符号引用:在常量池中定位对应类的符号引用,并检查类是否已被加载、解析、初始化过。若没有,必须先执行类加载。

2)分配内存:在Java堆中为新对象分配内存,该内存的大小在类加载后就已经确定。若Java堆是规整的,采用指针碰撞方式,否则采用空闲列表方式。

分配内存时候需要考虑线程安全问题,两种解决方案:

* 对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS(一种乐观锁机制,compare and swap)配上失败重试的方式保证更新操作的原子性;

* TLAB(Thread Local Allocation Buffer,本地线程分配缓冲),每个线程在Java堆中预先分配一小块内存,当线程需要分配内存时,先在TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

  1. 初始化为零值:内存分配完后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。

  2. 对对象进行必要的设置,设置类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。

以上4步对应Java中的new指令。

  1. 对象的内存布局

    1. 对象头

  1. Mark Word:存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,在32位和64位虚拟机中分别位32bit和64bit。

  2. 类型指针:对象指向它的类元数据的指针,用来确定该对象时哪个类的实例。

    1. 实例数据

程序代码中定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录。存储顺序会收到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。

    1. 对齐填充

仅仅起着占位符的作用,长度为8bit的整数倍。

  1. 对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象。

    1. 句柄

优势:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据的指针,reference本身不需要改变。

    1. 直接指针

优势:访问速度快。

  1. 判断对象是否存活

    1. 引用计数算法(Java虚拟机没有使用该算法)

给对象添加一个引用计数器,有引用时加1,引用失效时减1,任何时刻计数器为0时对象就不能再被使用。

优:实现简单,判定效率高,大部分情况都可以用;

缺:很难解决对象间相互循环引用的问题。

    1. 可达性分析算法(主流方法)

GC Roots到这个对象不可达时,证明此对象不可用。

Java中可作为GC Roots的对象:

  1. 虚拟机栈中引用的对象

  2. 方法区中类静态属性引用的对象

  3. 方法区中常量引用的对象

  4. 本地方法栈中JNI引用的对象

  1. 生存还是死亡

可达性分析——>finalize()方法

一个对象的死亡需要经历至少两次标记过程:

  1. 第一次标记:对象在可达性分析后发现此对象不可用,进行第一次标记;

  2. 筛选:若当前对象未覆盖finalize方法或finalize方法已被调用过,则没必要执行finalize方法,判定该对象死亡;否则有必要执行finalize方法;

  3. 执行finalize方法:将对象放在F-Queue队列中,若对象要拯救自己,则要在finalize方法中重新建立引用;任何对象的finalize方法都只会被调用一次,若某个对象在上一次回收时通过finalize方法逃脱了死亡的命运,那么在这次回收时,一定会被回收;

  4. 第二次标记:将自我拯救的对象移出“即将回收”的集合;没能自救的对象基本已经判定死亡。

注意:应该尽量避免使用finalize方法,finalize方法能做的所有工作,使用try-finally或者其他方式都可以做的更好、更及时。

  1. 方法区的回收

    1. 废弃常量

常量池中的常量没有在其他地方被引用,就会废弃。

    1. 无用的类

  1. 该类的所有实例都已被回收

  2. 该类的ClassLoader已被回收

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

  1. GC算法

    1. 标记—清除算法

首先标记出所有需要回收的对象,然后统一回收。最基础的收集算法。

不足:效率低;会产生大量内存碎片,可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次GC操作。

    1. 复制算法

商业虚拟机采用这种算法来回收新生代。先将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间,然后清理掉Eden和用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例时8:1;,所有会有10%的内存会被浪费。如果另外一块Survivor空间不够时,这些对象将直接通过分配担保机制进入老年代。

    1. 标记—整理算法

在标记—清除算法的基础上,将存活的对象向一端移动,然后清理掉端边界以外的内存。

    1. 分代收集算法

将java堆分为新生代和老年代,新生代采用复制算法,老年代采用标记—清除/整理算法。

新生代:每次垃圾回收时只有少量存活;

老年代:对象存活率高。

  1. HotSpot如何发起内存回收

GC停顿:在进行可达性分析时,不可以出现在分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法保证。这点是导致GC进行时必须停顿所有Java执行线程的其中一个重要原因。

“stop the world”:GC进行时暂停所有Java执行线程。

OopMap:帮助虚拟机快速准确地完成GC Roots枚举。GC在扫描时通过OopMap就可以得知哪些地方存放着对象引用。

安全点:HotSpot没有为每条指令生成OopMap,只有在安全点记录了这些信息,从而降低GC的空间成本。程序执行时只有在安全点才能停顿下来开始GC。长时间执行的指令才会产生Safepoint。GC发生需要中断线程时,虚拟机采用主动式中断的方式,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动区轮询这个标志,发现中断标志为真时就自己中断挂起。安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长。

一般会在如下几个位置选择安全点:

1、循环的末尾

2、方法临返回前

3、调用方法之后

4、抛异常的位置

为什么选定这些位置作为安全点:

主要的目的就是避免程序长时间无法进入 Safe Point。比如 JVM 在做 GC 之前要等所有的应用线程进入安全点,如果有一个线程一直没有进入安全点,就会导致 GC 时 JVM 停顿时间延长。

安全区域:安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任何地方GC都是安全的。

在线程执行到Safe Region中的代码时首先标识自己已进入Safe Region,这样在进行GC时,就不用管位于Safe Region中的线程了。当线程要离开Safe Region时,需检查系统是否已完成根节点枚举,若完成则线程继续执行,反之则必须等待直到收到可以安全离开Safe Region时为止。

安全区域是为了解决线程处于Sleep或Blocked状态时无法响应JVM的中断请求提出的。乍看可能会感觉到疑惑:JVM请求线程中断是为了防止线程在GC时改变了对象之间的引用关系。而此时线程都直接Sleep(Blocked)了,肯定不会改变对象的引用关系啊。细想一下是这样的,为的就是防止在GC过程中,线程被唤醒(不阻塞),获得CPU时间继续执行,那么这时线程就很有可能改变对象之间的引用关系,所以当线程被唤醒需要离开safe region时,需要检查系统是否已经完成GC Roots枚举。

  1. 垃圾收集器

    1. Serial

  1. 最基本、发展历史最悠久

  2. 采用复制算法

  3. 单线程,对于单CPU环境来说,更加简单高效

  4. 运行在Client模式下的默认新生代收集器

    1. ParNew

  1. Serial收集器的多线程版本

  2. 许多运行在Server模式下的首选新生代收集器

  3. 除Serial收集器外,只有它能与CMS收集器配合工作

    1. Parallel Scavenge

  1. 多线程、新生代收集器、使用复制算法

  2. 吞吐量优先(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),其他收集器更多关注的时垃圾收集时用户线程的停顿时间

  3. 高吞吐量适合在后台运算,停顿时间短适合需要与用户交互的程序

    1. Serial Old

  1. Serial收集器的老年代版本,使用标记—整理算法

    1. Parallel Old

  1. Parallel Scavenge收集器的老年代版本,使用多线程和标记—整理算法

    1. CMS(Concurrent Mark Sweep)

  1. 以获取最短回收停顿时间为目标

  2. 采用标记—清除算法,会产生空间碎片

  3. 对CPU资源非常敏感,会降低吞吐量,吞吐量随着CPU数量的增加而升高

  4. 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生(浮动垃圾:并发清理阶段出现的垃圾,由于这部分垃圾出现在标记过程之后,所以收集器无法清理,只能留到下一次GC清理。)

    1. G1

  1. 面向服务端应用

  2. 并行与并发,能充分利用多CPU、多核环境

  3. 分代收集:保留分代概念,且不需要其他收集器的配合就可以独立管理GC堆

  4. 空间整合:整体基于标记—整理算法,局部基于“复制”算法

  5. 可预测的停顿:G1将整个Java堆分成多个Region,根据每个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值)来确定回收优先级

  6. G1使用Remembered Set来避免全堆扫描


版权声明:本文为qq_26552071原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。