深入理解java虚拟机


[TOC]


深入理解java虚拟机读书笔记

基于此书第二版,会加上自己的一些理解,如有错误,望指出。后续会一点点更新

第1章 走近java

主要介绍了java的技术体系,java发展史,java虚拟机发展史和java未来

第2章 java内存区域与内存溢出一场

运行时数据区域

这里写图片描述

程序计数器

记录当前线程执行字节码的行号

  • 每个线程拥有一个
  • jvm规范中唯一一个没有定义OutOfMemoryError情况的区域

java虚拟机栈

  • java代码调用才有的,俗称栈帧
  • 线程私有
  • 存储局部变量表 操作数栈 动态链接 方法出口
  • 局部变量表所需要的内存空间在编译期间完成分配。
  • 两种异常:
    如果线程请求的栈的深度大于虚拟机所允许的深度抛,出“StackOverflowError”。
    如果虚拟机栈可以动态扩展(大多数虚拟机都支持),扩展时无法申请
    到足够的内存,抛出“OutOfMemoryError”。

本地方法栈

同java虚拟机栈,只不过是针对为虚拟机执行Native方法服务。

java堆

是所有线程共享的一块内存,也是虚拟机所需要管理的内存(GC)最大的一块, 用来存储对象实例和数组,,不连续,可扩展,会抛出OutOfMemoryError,指令-xmx xms。
粗略分区:

  • 新生代
  • 老年代

细分区:

  • Eden
  • From Survivor
  • To Survivor

方法区

  • 线程共享
  • 存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码
  • 可以选择不进行垃圾回收
  • 不连续,可扩展,会抛出OutOfMemoryError。

运行时常量池

属于方法区的一部分,用于存放编译期生成的各种字面量和符号引用

  • 具备动态性:并不是预置Class文件中的常量池的内容才能进入方法运行时常量;运行时也可以将新的常量放入池中。如String的intern()方法。
  • OutOfMemeoryError

直接内存

不是虚拟机运行时数据区的一部分
NIO利用 原生Navtive函数直接分配堆外内存,避免java堆和Native堆来回进行数据复制,也会产生OutOfMemoryError

HotSpot虚拟机对象探秘

对象的创建

(第七章详细介绍)

对象的内存布局

对象在内存中存储的布局

  • 对象头
  • 实例数据
  • 对齐填充

对象头

  • mark word
    存储对象自身的运行时数据,如:HashCode、GC分代年龄、锁状态、线程持有的锁、偏向线程id、偏向时间戳等。

  • 类型指针:对象指向它的类元素的指针。
    通过这个指针找到这个对象是哪个类的实例
    不是所有jvm在对象数据上保留类型指针,查找对象不一定经过对象本身(如果是java数组,对象头部还会记录数组长度的数据)。

实例数据
对象真正存储的有效信息。也是在程序中所定义的各种类型的字段内容。
这部分定义会受到jvm分配策略参数和字段在java源码中定义的顺序的影响。

对齐填充
仅是占位符的作用。

对象的访问定位

通过站上的reference数据来操作堆上的具体对象
不同的虚拟机有不同的访问凡是主流的的访问方式有句柄和指针两种

  1. 通过句柄访问对象
  • 堆中划出句柄池保留对象指针,java栈存储句柄池地址
  • 二次索引,速度慢,但是对象移动无需改变reference,只需改变句柄池
    通过句柄访问对象
  1. 通过直接指针访问对象(HotSpot)
    直接指针访问对象
  • reference 保存的是对象地址,对象中包含对象类型数据的指针
  • 速度快,少了一次指针定位

实战:OutOfMemoryError异常

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

程序计数器,虚拟机栈,本地方法栈这三个区域随线程而生,随线程而灭

对象已死吗

如何判断对象要被回收了

引用计数法

存在循环引用的问题

可达性分析算法

通过一系列称为“GC Roots”的对象为起点,从这些起点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链,则称为不可达,即可以被回收。

可作为GC Roots

  • 虚拟机栈中引用的对象
  • 方法去中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native 方法)引用的对象。

再谈引用

  • 强引用:new 出来的,只要强引用还在,就不会被gc回收
  • 软引用:用来描述一些有用但是非必需的对象,会在内存溢出之前,将这些对象列为回收范围,进行第二次回收。
  • 弱引用:描述非必需对象,强度比软引用弱,用来描述非必须对象,只能生存到下一次GC之前,无论内存是否足够 ,都会进行回收。
  • 虚引用: 也称为幽灵引用或者幻影引用。最弱的引用,完全不会对其生存时间构成影响。无法通过虚引用来取得一个对象实例。
    目的:在这个对象被回收的时候收到一个通知。

生存还是死亡

首先经过可达性分析,如果没有哦GC Roots相连接的引用链,会进行第一次标记并进行筛选,筛选的条件是对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或finalize方法已经被虚拟机调用过,虚拟机视这两种情况为“没有必要执行“

如果对象被判定为有必要执行finalize方法,将此对象放到F-Queue对象中。稍后,有虚拟机自建一个低优先级的Finalize线程去执行它。
任何对象的finalize方法只能被系统自动调用一次。

可以在finalize中阻止被GC回收,所以finalize尽量不要用它进行外部资源清理,否则会导致gc缓慢,不确定性增加

回收方法区

方法区在hotSpot虚拟机中也叫永久代,
永久代要回收两部分内容:废弃的常量、无用的类。

废弃的常量:无对象引用。
无用类,同事满足以下3个条件

  • 该类的所有实例都已经被回收,java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任务地方被引用,无法在任何地方通过反射访问到该类的方法。

在使用反射,动态代理,CGLIB等ByteCode框架,动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类的卸载功能,保证永久代不溢出。

垃圾收集算法

标记-清除算法

标记要回收的对象,统一进行回收。

  • 效率低
  • 产生不连续内存

复制算法

为了解决效率问题。通常用在新生代算法
将可用内存划分为大小两块,只是用其中一块。当这一块用完时,就将还存活的对象赋值到另一块上面,然后再把已使用过的内存空间一次清理掉。(每次都对半个内存区进行回收。)

  • 内存利用率低,只能使用一半
  • 在对象存活率高的情况下,效率会更低
  • 解决了内存不连续问题

标记-整理算法

针对老年代,标记后,让存活的对象都向同一端移动。然后直接清理掉端边界以外的内存。

分代收集算法

  • 新生代:使用复制算法。
  • 老年代:使用“标记-清理”或者“标记-整理”。

HotSpot的算法实现

枚举根节点

  • 会出现停顿现象。
    使用称为OopMap的数据结构来记录那些地方存放了对象的引用。用于快速完成GC Roots枚举
  • 安全点
    HotSpot在特定的位置记录安全点。GC只在到达安全点的时候才暂停。
    指令序列复用会导致“长时间执行”,如:方法调用,循环跳转,异常跳转等。
    GC发生时让所有线程都跑到最近的“安全点”再停下来。两钟方式:抢先式中断和主动中断。
  • 安全区域
    指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,可以将SafeRegion当做是SafePoint的扩展。

垃圾收集器

分为新生代和老年代收集器,下图中有连线的可以配合使用
HotSpot收集器

Serial收集器

Serial/Serial Old收集器运行示意图

  • 新生代
  • 适合单个处理器,适合client模式
  • 单线程
  • stop the world 停止其他所有线程

ParNew收集器

ParNew-Serial Old收集器运行示意图

  • 新生代
  • 多线程,适合多核处理器
  • Server模式比较好
  • 除了Serial之外,只有它可以与CMS收集器配合使用。

Parallel Scavenge收集器

Parallel Scavenge+Parallel Old收集器

  • 新生代
  • 并行多线程
  • 使用复制算法的收集器
  • 关注吞吐量。
    目的:达到一个可控的吞吐量。(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。
    CMS收集器,停顿时间短,停顿时间越短适合需要与用户交互的程序,适合交互较多的任务。Parallel Scavenge高吞吐量可以高效的利用CPU时间,主要适合后台运算不需要太多交互的任务。

Serial Old收集器

是Serial老年代版本。

  • 单线程收集器
  • 使用“标记-整理”算法。
  • 可以给Client模式的虚拟机使用。
    如果是Server模式:1、和JDK1.5以前的版本中的Parallel Scavenge收集器搭配之用;2、作为CMS收集器的后备预案。

Parallel Old收集器

Parallel Old是Parallel Scavenge老年代版本。

  • 多线程
  • “标记-整理”算法。
    在吞吐量优先的场合,优先使用Parallel Scavenge+Parallel Old收集器。

CMS收集器

CMS收集器
Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器。
相应时间优先,用户体验优先,采用CMS收集器。
采用“标记-清除”算法实现。
收集步骤:

  • 初始标记:只是标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记:可以与用户线程一起工作。并发标记阶段就是进行GC Roots Tracing 的过程
  • 重新标记:重新标记是为了修正并发标记期间因用户程序继续运作而导致标记变动的那边分对象标记。
  • 并发清除:可以与用户线程一起工作。
    缺点
  • 对cpu资源非常敏感。CMS默认开启的回收线程数=(cpu+3)/4。随着cpu的增加而下降。为了避免这个缺陷,虚拟机提供了一种称为“增量式并发收集器”(在并发标记、清理的时候让GC线程和用户线程交替运行)效果一般,不提倡使用。
  • 无法处理浮动垃圾。
  • 标记-清除”导致内存碎片的产生。从而导致Full GC的产生。为了解决这个问题,CMS提供一个-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的。

G1 收集器

G1收集器
面向服务器端的收集器。
优点:

  • 并行与并发:充分利用多CPU,多核来缩短Stop-The-World的停顿时间。
  • 分代收集:能够采用不同的方式去处理新创建的对象和已经存货一段时间、熬过多次GC的就对象以获取更好的收集效果。
  • 空间整合:整体采用“标记-整理”算法,从局部(两个Region之间)上来看是居于“复制”算法实现的。意味着G1在运行期间不会差生内存碎片。
  • 可预测的停顿:降低停顿时间,追求低停顿,建立可预测的停顿时间模型。

收集步骤:

  • 初始标记:标记与GC Roots直接关联的对象。
  • 并发标记:从GC Roots开始对堆中的对象进行可达性分析,找出存货的对象,耗时场,可与用户程序并发执行。
  • 最终标记:
  • 筛选回收:

内存分配和回收策略

对象优先在Eden分配

对象优先在Eden区分配,Eden没有足够的空间时将发起一次Minor GC。
Minor GC:新生代GC,指发生在新生代的垃圾收集动作,速度快。
老年代GC(Major GC/Full GC):老年代发生的牢记收集动作,Major GC一般比Minor GC慢10倍以上。

大对象直接进入老年代

比如:很长的字符串以及数组。

长期生存的对象将进入老年代;
为每个对象定义了一个年龄计数器,如果第一次Minor GC后仍然存活,并且被survivor容纳的话,将被移动到Survivor中,并且对象年龄设为1;对象在Survivor去中熬过一次Monior GC,年龄增加1,档年龄增加到一定程度(默认15),将会移动到老年代。这个发着可以通过参数-XX:MaxTenuringThreshold设置。

动态对象年龄判断

为了更好地适应不同程度的内存状况,虚拟机并不是永远的要求对象必须到达了MaxTenuringThreshold才能晋升为老年代,如果在Survivor空间中相同年龄所有对象大小之和大于Survivor空间的一般,年龄大于或等于改年龄的对象将被移动到老年代。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,则Minor GC是安全的;如果不成立,会查看HandlePromotionFailure设置值是否允许担保失败。如果允许那么继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次MiNor GC;如果小于则改为进行一次Full GC。

第4章 虚拟机性能监控与故障处理工具

JDK自带工具,JConsole ,VirtualVM