深入理解Java虚拟机 学习笔记 JVM

来源:
1.《深入理解Java虚拟机》
写在前面:Java8用的是Hotspot虚拟机

目录

  • 1.基础知识
  • 2.运行时数据区域
    • (1)OOM异常
    • (2)对象创建
    • (3)对象内存布局
    • (4)对象的访问定位
  • 3.垃圾收集(Garbage Collection)
    • (1)如何判断对象死亡?
      • a.引用计数法
      • b.可达分析法
      • 四种引用
      • 两次标记过程,不可达对象就是可回收对象吗?
      • 方法区垃圾回收
    • (2) 垃圾收集算法
      • 分代收集理论
      • a.标记-清除算法
      • b.标记-复制算法
      • c.标记-整理算法
    • (3)垃圾收集器
      • 1.Serial收集器(标记-复制)
      • 2.ParNew收集器(标记-复制)
      • 3.Parallel Scavenge收集器(标记-整理)
      • 4.Serial Old收集器(标记-整理)
      • 5.Parallel Old收集器(标记-整理)
      • 6.CMS收集器(标记-清除)
      • 7.Garbage First收集器(G1)
      • 衡量垃圾收集器的标准
  • 4.类加载机制
    • (1)类的生命周期
    • (2)类加载过程
    • (3)类加载器
    • (4)双亲委派模型
  • 5.虚拟机性能监控、故障处理工具
    • 基础故障处理工具
    • 监控
  • Jvm调优的目的

1.基础知识

JDK:Java Development Kit, java开发工具包
JRE:Java Runtime Environment,java运行环境
JVM:Java Virtual Machine,java虚拟机
深入理解Java虚拟机 学习笔记 JVM_第1张图片
Helloworld.java–>javac–>Helloworld.class–>java–>JVM–windows机器码–>Windows
JVM:从软件层面屏蔽不同操作系统在底层硬件指令上的区别。
Hotspot JVM 后台运行的系统线程:
(1)虚拟机线程:JVM位于安全点的操作,stw垃圾回收,线程栈、线程暂停、线程偏向锁解除。
(2)周期性任务线程:定时器事件(中断)
(3)GC线程
(4)编译器线程
(5)信号分发线程:接收发送到 JVM 的信号并调用适当的 JVM 方法处理

2.运行时数据区域

深入理解Java虚拟机 学习笔记 JVM_第2张图片
(1)程序计数器:当前线程所执行的字节码的行号指示器,唯一一个在虚拟机中没有任何规定OOM情况的区域。
(2)虚拟机栈:
1.放线程的局部变量。
2.一个方法对应一块栈帧内存区域。
3.栈帧组成:局部变量表、操作数栈(比如,a+b)、动态链接、方法出口
4.栈帧里如果有对象类型,放的是对象类型的引用地址
(3)本地方法栈:跨语言调用
(4)方法区/永久代:JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,运行时常量池 (用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中)。JDK 8,完全废弃了永久代的概念,在本地内存中实现的元空间(Meta-space)来代替永久代。
(5)堆:运行时数据区,new出来的对象和数组都在堆内存中,垃圾收集器进行垃圾收集的最重要的内存区域。存放对象实例。大部分虚拟机的堆都是可扩展大小的。

(1)OOM异常

a. 虚拟机栈/本地方法栈存在两类异常状况:
(1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
(2)如果当栈无法申请到足够的内存会抛出OutOfMemoryError异常。
b. Java堆异常:
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
c. 方法区异常:运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
d. 直接内存出现异常:NIO使用Native函数库直接分配堆外内存,设置堆参数时忽略直接内存,导致动态扩展时OOM异常。

(2)对象创建

深入理解Java虚拟机 学习笔记 JVM_第3张图片

(3)对象内存布局

对象头、实例数据、对齐填充
对象头:包含存储对象自身的运行时数据(Mark Word,哈希码,GC分代年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳)和类型指针(对象指向它的类型元数据的指针,用来确定是哪个类的实例)

(4)对象的访问定位

对象的访问方式:句柄和直接指针
句柄: reference中存储的就是对象的句柄地址。Java堆中可能会划分出一块内存作为句柄池,句柄池存放实例数据指针和对象类型数据指针。实例数据指针指向对象示例数据(Java堆中),对象类型数据指针指向对象类型数据(方法区中)。
直接指针: reference中存储的直接就是对象地址。对象类型数据指针指向对象类型数据。
句柄优点:稳定,在对象被移动(垃圾收集时)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
指针优点:速度快,节省实例数据指针定位的时间。
HotSpot主要使用的是直接指针。

3.垃圾收集(Garbage Collection)

(1)如何判断对象死亡?

a.引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;计数器为零的对象可被回收。
优点: 效率高,简单。
缺点: 占用额外内存空间来计数,不能解决对象之间互相循环引用。

b.可达分析法

通过“GC Roots”的根对象作为起始节点集,根据引用关系向下搜索所走过的路径(“引用链”ReferenceChain),如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。
可作为GC Roots的对象:
1.虚拟机栈中引用的对象:局部变量、参数等。
2.方法区中类静态属性引用的对象:Java类的引用类型静态变量。
3.方法区中常量引用的对象:字符串常量池中的引用。
4.本地方法栈中引用的对象。
5.虚拟机栈内部引用:基本数据类型的Class对象、常驻异常对象、系统类加载器。
6.同步锁持有的对象。
7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

四种引用

强引用、软引用(有用但非必须的对象,SoftReference类)、弱引用(非必须的对象,WeakReference类,gc时会回收该对象)、虚引用(PhantomReference类实现,不能单独使用,和引用队列联合使用,虚引用的主要作用是跟踪对象被垃圾回收的状态)

两次标记过程,不可达对象就是可回收对象吗?

(1)第一次标记。如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记。
(2)进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
(3)若有必要执行finalize(),对象放到F-Queue的队列中,第二次标记,是否被引用,若被引用,移除即将回收的集合。由虚拟机自动建立的Finalizer线程执行finalize()方法。
例子:对象的自我拯救,一次自我拯救成功后,第二次就不会执行finalize()方法了。疑问:第二次不执行finalize()如何回收该对象?

方法区垃圾回收

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。

(2) 垃圾收集算法

分代收集理论

分代假说:
1)弱分代假说:朝生夕灭
2)强分代假说:熬过越多垃圾收集越难以消亡
3)跨代引用假说:对比同代引用占极少数

a.标记-清除算法

过程:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。(也可以反过来)
缺点:
1.执行效率不稳定,若包含大量需回收的对象,大量的标记和清除动作。
2.内存空间碎片化严重,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

b.标记-复制算法

过程:半区复制算法,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象 复制到另外一块上面,然后再把已使用过
的内存空间一次清理掉。
缺点:
1.大部分是存活的对象,会产生大量内存间复制的开销。
2.可用内存缩小为原来的一半,空间浪费。
优点:
大部分是可回收的对象,复制少数对象,且不用考虑有空间碎片的复杂情况。
现代的标记复制算法: 8:1:1分配内存区,Appel式回收。
为什么这样分? 因为新生代“朝生夕灭”的特点,新生代中的对象有98%熬不过第一轮收集。
(1)Eden(8/10)->from(1/10)->to(1/10)->老年代(2/3)
年轻代:Eden(8/10)->from(1/10)->to(1/10)
Survivor区:from->to
(2)new出来的先放在Eden区,Eden放满了,触发Minor gc(复制->清空->互换),对新生代区进行一次垃圾回收。
(3)一个对象经历过Minor gc后没有被销毁,年龄+1,当年龄到了15,放到老年代,或大的对象放到老年代,老年代进行分配担保。
(4)老年代空间不足时,触发MajorGC。(实际上除了CMS收集器,其他都不存在只针对老年代的收集)

c.标记-整理算法

过程:首先标记出所有需要回收的对象,让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
对于老年代:
移动对象: 大量移动操作,吞吐量相比不移动对象的吞吐量大。Paraller Scavenge收集器使用标记-整理算法。
不移动对象: 内存分配时更复杂,这部分耗时增加。停顿时间短。CMS使用标记-清除算法

(3)垃圾收集器

新生代收集器:Serial,Parnew,Paraller Scavenge
老年代收集器:CMS,Serial Old,Paraller Old
G1:混合收集,目标是收集整个新生代以及部分老年代的垃圾收集。
新生代使用标记-复制算法, 因为新生代的特点是每次垃圾回收时都有大量垃圾需要被回收。

老年代使用标记-整理算法/标记-清除算法, 因为老年代的特点是每次垃圾回收时只有少量对象需要被回收,对象存活率高、没有额外空间对它进行分配担保。

1.Serial收集器(标记-复制)

单线程工作的新生代收集器,在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点: 简单,额外内存消耗小,对于单核处理器或处理器核心数较少的环境来说,没有线程交互的开销,此时效率很高。

2.ParNew收集器(标记-复制)

Serial收集器的多线程并行版本。可以被使用的处理器核心数量多时,可以使用这种方法。

3.Parallel Scavenge收集器(标记-整理)

目标是达到可控的吞吐量 (运行用户代码时间/(运行用户代码时间+运行垃圾收集时间))。
参数:MaxGCPauseMillis(最大垃圾收集停顿时间)、GCTimeRatio(吞吐量的倒数)、GC Ergonomics(自适应调节策略)

4.Serial Old收集器(标记-整理)

Serial收集器的老年代版本。
Server模式下用途:

  1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
  2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。

5.Parallel Old收集器(标记-整理)

Parallel Scavenge收集器的老年代版本。
注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

6.CMS收集器(标记-清除)

CMS,Concurrent Mark Sweep。是一种以获取最短回收停顿时间为目标的收集器。
B/S系统追求响应速度, 可以使用CMS。
过程:
(1)初始标记:需要STW,速度很快,只是标记一下GC Roots能直接关联到的对象。
(2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程。
(3)重新标记:需要STW,修正并发标记期间标记变动的对象。
(4)并发清除:与用户线程并发,清理删除掉标记阶段判断的已经死亡的对象。
优点: 低停顿。
缺点:
(1)对处理器资源敏感。并发阶段不会导致用户线程停顿,但占用了处理器的计算能力导致用户线程变慢,总吞吐量下降。CMS默认启动的回收线程数是(处理器核心数量+3)/4,当处理器核心数量不足4个,对用户线程影响很大。
(2)由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“StopThe World”的Full GC的产生。
浮动垃圾:并发标记和并发清理阶段用户线程产生的垃圾对象。
CMS需要预留空间供并发收集时用户程序运作使用,若空间不足,并发失败,启动Serial Old预备方案,停顿时间就更长了。
(3)内存空间碎片产生,找不到可以给大对象分配的连续空间,提前触发Full GC。为解决该问题,几次(默认0)不整理空间后Full GC之后,下一次碎片整理。

7.Garbage First收集器(G1)

目的: 建立可预测的停顿时间模型,在延迟可控的情况下获得尽可能高的吞吐量。
追求能够应付应用的内存分配速率。
如何建立可预测的停顿时间模型:分区,将Rigion作为单次回收最小单元,避免全堆垃圾收集。具有优先级的区域回收方式保证收集高效。
主要面向服务端应用 的垃圾收集器。服务端模式下的默认垃圾收集器。新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。
Mixed GC ,面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,哪块内存存放垃圾数量多,进行回收。
Humongous区域, 专门存储大对象。只要大小超过了一个Region容量一半的对象即可判定为大对象。
G1收集器跟踪Rigion的垃圾价值,在后台维护优先级列表,优先处理回收价值最大的区。

过程:
(1)初始标记:只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
(2)并发标记:扫描堆的对象图,可与用户线程并发执行 。还要重新处理SATB(原始快照)记录下的在并发时有引用变动的对象。
(3)最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
(4)筛选回收:暂停用户线程,更新区数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收
集,然后把决定回收的那一部分Region的存活对象复制 到空的Region中,再清理掉整个旧Region的全部空间。
特点: 整体标记-整理,局部(两个Region之间)标记-复制。

与CMS对比:
优点:
(1)可以由用户指定期望的停顿时间,可预测的停顿时间模型。
(2)G1运作期间不会产生内存空间碎片,这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。
缺点:
为了垃圾收集产生的内存占用,程序运行时额外负载比CMS高。G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间。

衡量垃圾收集器的标准

内存占用、吞吐量、延迟。

4.类加载机制

(1)类的生命周期

加载、验证、准备、解析、初始化、使用、卸载。
几种情况必须立即对类进行初始化:new关键字、调用类型的静态方法、反射、先父类初始化…
几种情况不会执行类初始化:
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

(2)类加载过程

加载、验证、准备、解析、初始化。

  1. 加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  2. 验证:确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,不会危害虚拟机。
  3. 准备:准备阶段是正式为类中定义的变量 (即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,如果不是常量,设为零值,如果是常量,设为指定值。
  4. 解析:Java虚拟机将常量池内的符号引用替换为直接引用 的过程。
  5. 初始化:初始化阶段就是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。

符号引用:符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用:直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

(3)类加载器

作用: 通过一个类的全限定名来获取描述该类的二进制字节流。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性 ,每一个类加载器,都拥有一个独立的类名称空间。
启动类加载器:加载存放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
扩展类加载器:负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
应用程序类加载器:负责加载用户路径(classpath)上的类库。

(4)双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。通常使用组合(Composition)关系来复用父加载器的代码。
过程: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
优点: 带有优先级的层次关系。类在程序的各种类加载器环境中都能够保证是一个类, 解决了各个类加载器协作时基础类型的一致性问题。

5.虚拟机性能监控、故障处理工具

基础故障处理工具

  • jps:虚拟机进程状况工具 jps-v输出虚拟机进程启动时的JVM参数
  • jstat:虚拟机统计信息监视工具,jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程[1]虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。 jstat -gc vmid 间隔时间 查询几次 监视java堆状况;jstat -gcutil 输出主要关注已使用空间占总空间的百分比
  • jinfo:Java配置信息工具,实时查看和调整虚拟机各项参数。
  • jmap:Java内存映像工具,用于生成堆转储快照(一般称为heapdump或dump文件),它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。 jmap -heap 显示Java堆信息,使用哪种回收期,参数配置等
  • jhat:虚拟机堆转储快照分析工具,JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。用户在浏览器中输入http://localhost:7000/可以看到分析结果。
  • jstack:Java堆栈跟踪工具用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)

监控

JMS

Jvm调优的目的

减少STW次数,减少full gc。
方法1:扩大Eden区。
方法2: 使用工具,
生成jvm日志的方法:可以通过以下几个参数要求虚拟机生成GC日志:-XX:+PrintGCTimeStamps(打印GC停顿时间)、-XX:+PrintGCDetails(打印GC详细信息)、-verbose:gc(打印GC信息,输出内容已被前一个参数包括,可以不写)、-Xloggc:gc.log

你可能感兴趣的:(深入理解Java虚拟机 学习笔记 JVM)