深入理解Java虚拟机

Posted dajunjun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解Java虚拟机相关的知识,希望对你有一定的参考价值。

引用计数法

高效率,但无法解决循环引用的问题,Python语言在使用

可达性分析

主流商用程序语言在使用,比如C#,Java,以及Lisp。通过一系列被称为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连,对象不可达的,则证明此对象是不可用的,会被判定为可回收的对象。

作为GC Roots的对象有以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈JNI(也就是Native方法)引用的对象

引用

  1. Strong Reference: 最常用的方式
  2. Soft Reference: 会在发生内存溢出之前,会把这些对象列入回收范围,进行二次回收。
  3. Weak Reference: 被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
  4. PhantomReference: 最弱的一种引用关系,是否有虚引用存在,不对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。唯一作用是在这个对象被收集器回收时收到一个系统通知。

回收过程

被标记为不可达对象,处于一种缓刑阶段,正式回收前,至少要经历两次标记过程。 第一次被标记为不可达,会进行一次筛选,条件是对象是否有必要执行finalize方法(没有执行过finalize方法并且覆盖过这个方法),对于要执行finalize方法的对象,会放置到一个低优先级的Finalizer线程去执行。注意这个是异步的,不会等待执行结束,稍后GC会对F-Queue中对象进行第二次效果莫的标记。如果这次标记还没有逃脱不可达状态,基本上会真的被回收。如果对象在finalize方法里摆脱了不可达状态,则避免了被回收。注意,finalize只会调用一次

回收方法区

方法区(永久代)的回收性价比低,不属于Java虚拟机规范。主要回收两类:废弃常量和无用的类。比如一个字符串常量没有任何对象在使用。 判定一个类是无用的类,有以下几个条件:

  1. 该类所有的实例都已经被回收。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾回收算法

标记-清除算法(Mark-sweep)

对内存块进行标记,然后根据状态进行回收。缺点:效率不高,产生大量不连续的内存碎片。

改进型:标记完后不是直接对可回收内存进行清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。这种叫标记整理算法

复制算法

将内存分为两块,每次只使用一块,当一块内存使用完了,就将存活对象复制到另一块上面,然后把已经使用的内存空间一次清理掉。不存在内存碎片,分配内存时只使用堆顶指针按照顺序分配就可以。高效率且运行简单。缺点是把可用的内存缩小为原来的一半。

改进型:98%的对象都是可以回收的,把内存分为三部分8:1:1(Eden:Survivor:Survivor),每次使用一个Eden和Survivor,每次GC时,将还存活的对象一次性复制到另一快Survivor上,然后清理Eden和Survivor。如果10%d的空间不够用时,会使用老年代内存进行分配担保,这些对象会直接进入老年代。

分代手机算法

根据对象存活周期不同,把内存划分为几块。一般是把Java堆分为老年代和新生代。新生代每次回收只有很少对象存活,就采用复制算法,老年代对象存活率高,没有额外空间进行分配担保,就采用标记-清理或者标记-整理算法。

GC遍历的算法

枚举根节点

可作为GC Roots的节点主要在全局性的引用(常量或者类静态属性)与执行上下文(栈帧也即是执行方法中的本地变量表)。

可达性分析对执行时间的敏感还体现在GC停顿上,因为分析必须在一个能确保一致性的快照中进行,整个分析期间整个执行系统看起来就像被冻结在某个时间点上。这就是GC进行时必须停顿所有的执行线程(Stop The World)的原因。

主流的虚拟机采用了准确式GC,并不需要一个不漏的检查完所有的执行上下文和全局的引用位置。HotSpot采用了一组称为OopMap的数据结构,类加载完成后HotSpot已经计算出来对象内偏移量上是什么类型数据,JIT编译过程中,也会在特定位置记录栈和寄存器中哪些位置是引用。

安全点

OopMap不会对每条指令都生成,只是在特定的位置记录了这些信息,这些位置就叫安全点,程序执行时也不是在所有的地方都能停下来GC,只有到达安全点才可以。安全点的选取以“是否具有让程序长时间执行的特征”为标准来进行选定的。长时间执行的最明显特征就是指令序列复用,比如方法调用,循环跳转,异常跳转等,具有这些功能的指令才会产生safePoint。

GC需要让所有线程都跑到最近的安全点在停顿下来,有两种方案可供选择:抢先式中断和主动式中断。

抢先式:首先把所有线程全部中断,如果发现有线程不在安全点,则恢复线程,让它执行到安全点。目前基本不再采用这种方式。

主动式:不对线程进行操作,仅设置一个标志位,各线程执行的时候主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的。

安全区域

当程序不执行,比如Sleep或者blocked时,没法响应JVM中断请求。安全区域(safe Region)是指一段代码片段,在任何地方引用关系都不会发生变化,在任何地方开始GC都是安全的。线程执行到safe Region时,会标识自己进入safe region,当JVM发起GC时,不用管标识自己为safe Region的线程。当线程要离开safe Region时,会检查系统是否完成了根节点枚举(或者整个GC过程),如果完成了线程会继续执行,否则会等待直到收到可以安全离开safe Region的信号为止。

垃圾收集器

Serial收集器大专栏  深入理解Java虚拟机4>

单线程的收集器,在垃圾收集时会暂停所有的工作线程。简单而高效,目前仍然是Client模式下新生代默认的收集器。

ParNew收集器

Serial收集器的多线程版本。运行在server模式下的虚拟机首选的新生代收集器。在CPU核数增加时有一定优势,默认开启的线程数与CPU的数量相同。使用-XX:+UseConcMarkSweepGC,选项后新生代默认会使用ParNew收集器。-XX:ParallelGCThread可以用来显示垃圾收集的线程数。

Parallel并行,多条垃圾收集线程并行工作,但是用户线程仍然等待。 Concurrent并发,用户线程和垃圾收集线程同时执行(有可能是交替执行),用户程序在继续运行,垃圾收集程序运行在另一个CPU上。

Parallel Scavenge收集器

使用复制算法,多线程并行,新生代的收集器。目标是达到一个可控制的吞吐量。 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

设置最大垃圾收集停顿时间-XX:MaxGCPauseMillis

设置吞吐量大小 -XX:GCTimeRatio,大于0小于100

Serial Old收集器

Serial收集器的老年代版本。单线程

Parallel Old收集器

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

CMS收集器

CMS(Concurrent Mark Sweep)以获取最短回收停顿时间为目标的收集器。基于标记清除算法。 四个步骤: 初始标记、并发标记、重新标记、并发清除

初始标记和重新标记需要Stop The World,并发标记就是进行GC RootsTracing的过程。重新标记为了修正并发标记期间因为用户继续运作导致标记产生变动的那一部分对象的标记记录。

优点:并发收集、低停顿。默认启动线程数=(CPU数量+3)/4。缺点:对CPU资源敏感,当CPU数量少时导致用户程序执行速度降低。无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次FullGC的产生。这些垃圾只能在下一次GC时再清理掉。另外会产生碎片,导致无法足够连续空间来分配当前对象

G1收集器

并行与并发,分代收集,不需要其他收集器配合就能独立管理整个GC堆。空间整合,采用了标记-整理算法,从局部上看(两个Region之间)是基于复制算法实现的,G1不会产生内存空间碎片。可预测的停顿。

步骤: 初始标记、并发标记、最终标记、筛选回收。

内存分配与回收策略

内存分配就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下回直接分配在老年代上。主要和垃圾收集器以及虚拟机参数设置。

对象优先在Eden分配

对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代GC,频道但回收速度快)。 老年代GC(Major GC/Full GC),发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC,比如Minor GC慢10倍以上。

大对象直接进入老年代

大对象,比如连续内存的字符串或者数组,(要尽量避免一群朝生夕灭的短命大对象),-XX:PretenureSizeThreshold参数,可以令大于这个设置值的对象直接在老年代分配。

长期存活对象进入老年代

为了分代收集来管理内存,内存回收时必须能识别哪些对象应放在新生代和老年代。虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并且经过第一次Minor GC仍然存活,并且能被Survivor容纳,那么将被移动到到Survivor,年龄设为1。在Survivor中没经过一次Minor GC,年龄就加1.加到默认15(可以通过-XX:MaxTenuringThreshold设置),就可以进入老年代。

动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold,如果Survivor中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立Minor GC是安全的,否则虚拟机会查看HandlerPromotionFailure设置值看看是否允许担保失败。

只要new的对象,一定是在堆上分配的内存。局部变量是指方法内部声明的局部变量和方法的参数。编译时统一存放为方法字节码,存放在局部变量表,放在栈空间上。java对象的实例域和java方法的局部变量完全没有关系!唯一相同的就是值的分配。如果实例域(局部变量)的类型为基本类型,那么存储的值就是字面量值;如果实例域(局部变量)的类型为引用类型,那么存储的就是该引用所“指向java对象在堆中地址值”的副本。java方法调用中,局部变量的传递方式也是如此。

以上是关于深入理解Java虚拟机的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Java虚拟机-常用vm参数分析

深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析

《深入理解Java虚拟机》读后笔记-HotSpot虚拟机对象探秘

深入理解Java虚拟机类加载机制

Java虚拟机系列(25篇文章)一起啃

深入理解java虚拟机