深入理解java虚拟机

文章目录

  • 2.java内存区域与内存溢出异常
    • 运行时数据区域
      • 程序计数器
      • java虚拟机栈
      • 本地方法栈
      • java堆
      • 方法区
      • 运行时常量池
      • 直接内存
    • 对象
      • 对象的创建
      • 对象的内存布局
        • 对象头-存储
        • 实例数据
        • 对齐填充
      • 对象的访问定位
        • 句柄访问
        • 直接指针访问
    • OOM异常
      • java堆溢出
      • 虚拟机栈和本地方法栈溢出
      • 方法区和运行时常量池溢出
      • 本级直接内存溢出
  • 3.垃圾收集器与内存分配策略
    • 判断对象是否可被回收
      • 引用计数法
      • 可达性分析算法
    • 再谈引用
      • 强引用
      • 软应用
      • 弱引用
      • 虚引用
    • 对象存活与否
    • 回收方法区
    • 垃圾收集算法
      • 1.标记-清除算法
      • 2.复制算法
      • 3.标记整理算法
      • 4.分代收集算法
    • HotSpot算法实现
      • 枚举根节点
      • 安全点
      • 安全区域
    • 内存分配与回收策略
      • 对象优先在Eden分配
      • 大对象直接进入老年代
      • 长期存活的对象将进入老年代
      • 动态对象年龄判断
      • 空间分配担保
  • 4.虚拟机性能监控和故障处理工具
    • 1.jps 虚拟机进程状态工具
    • 2.jstat 虚拟机统计信息监视工具
    • 3.jinfo java配置信息工具
    • 4.jmap java内存映射工具
    • 5.jhat 虚拟机堆转储快照分析工具
    • 6.jstack java堆栈跟踪工具
    • 7.javap反编译命令
  • 5.调优案例分析与实战
    • 类文件结构分析
      • 概述
      • class类文件结构
  • 7.虚拟机类加载机制
    • 类加载的时机
    • 类加载的过程
      • 加载
      • 验证
      • 准备
      • 解析
      • 初始化
    • 类加载器
      • 类加载器
      • 双亲委派模型
  • 8.虚拟机字节码执行引擎
    • 运行时栈帧结构
      • 局部变量表
      • 操作数栈
      • 动态连接
      • 方法返回地址
      • 附加信息
    • 方法调用
      • 解析
      • 分派
        • 静态分派
        • 动态分派
        • 单分派与多分派
        • 动态分配的实现
      • 动态类型语言支持
  • 10.编译器优化
    • Javac编译器
    • Java语法糖
  • 12.Java内存模型与线程
    • volatile型变量

2.java内存区域与内存溢出异常

运行时数据区域

深入理解java虚拟机_第1张图片

程序计数器

当前线程所执行的字节码的行号指示器

线程切换后能够回到正确的执行位置,每条线程都需要一个独立的程序计数器,线程私有

唯一没有规定OOM情况的区域

java虚拟机栈

线程私有,生命周期与线程相同

描述java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,存储局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息

每一个方法从调用直至执行完成的过程,都对应一个栈帧在虚拟机栈中入栈到出栈的过程。

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常,如果拓展无法申请到足够内存则会抛出OOM异常

本地方法栈

Native方法区

java堆

线程共享

唯一目的:存放对象实例

对上垃圾回收分为:新生代、老生代。

方法区

线程共享

存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等

运行时常量池

方法区的一部分

具备动态性,运行时也可能将新的常量放到池中,比如String.intern方法。

直接内存

并不是虚拟机运行时数据区的一部分,也不是jvm规范定义的内存区域

堆外内存,通过对象引用进行操作,避免java堆和Native堆来回复制数据

对象

对象的创建

JVM遇到一条new指令时,先去检查该指令参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否被加载、解析和初始化过。

没有则执行相应的类加载过程

分配内存 1.指针碰撞:java堆内存绝对规整,分配内存仅通过移动指针指示器。 2.空闲表法:内存不规整,虚拟机维护一个表,记录那些内存可用

创建对象的线程安全问题: 1.对分配内存空间的动作进行同步处理,采用CAS配上失败重试的方式保证更新操作的原子性;2.将内存分配的动作按照线程划分在不同的空间中进行,每个线程预先分配内存(本地线程分配缓冲)

JVM对对象进行必要的设置

对象的内存布局

对象头-存储

用于存储对象自身的运行时数据

类型指针:指向类元数据的指针

实例数据

对象真正存储的有效信息

对齐填充

对象的访问定位

通过栈上的reference数据来操作堆上的具体对象

句柄访问

[图片上传失败…(image-4bc7b8-1570263461374)]

reference中存储的是稳定的句柄指针,在对象被移动时只会改变句柄中的实例数据的指针,而reference本身不发生改变

直接指针访问

深入理解java虚拟机_第2张图片

速度更快,节省了一次指针定位的开销

OOM异常

java堆溢出

java堆用于存储对象实例,只要不断创建对象并且保证GC Root到对象之间有可达路径来避免垃圾回收机制清除,就有可能发生溢出

解决这个区域的异常,先通过内存映射工具,对dump出来的堆转储快照进行分析,对象是否有必要存在

虚拟机栈和本地方法栈溢出

线程请求栈深度大于虚拟机所允许的深度:StackOverflowError 递归

拓展栈时无法申请到足够的内存空间:OOM 创建过多线程

方法区和运行时常量池溢出

String.intern:如果字符串常量池包含一个等于此String对象的字符串,则返回池中的String对象引用,否则将此String对象包含的字符串添加到常量池,再返回String对象引用

通过该方法循环往常量池添加对象,可能造成运行时常量池溢出。

方法区是存放CLass相关信息的,通过Cglib产生大量类去填充方法区,导致溢出

本级直接内存溢出

直接通过Unsafe实例进行内存分配,unsafe.allocateMemory不断方法分配内存

明显特征 在Heap dump文件中没有明确异常,OOM之后 dump文件小,程序中又直接间接的使用了NIO。

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

判断对象是否可被回收

引用计数法

给对象添加一个引用计数器,每当一个地方引用他的时候,计数器值就+1,当引用失效,计数器值就-1,计数器值为0就是不可能在被使用

实现简单,判定效率高

缺陷:互相引用

深入理解java虚拟机_第3张图片

可达性分析算法

通过一些列称为“Gc Root”的对象作为起始点,从这些节点往下搜索,搜索所走过的路径叫做引用链,当一个对象到Gc Root没有任何引用链,则表示这个对象不可达

GC Root对象可能为:1.虚拟机栈中的引用对象 2.方法区中类静态属性引用的对象 3.方法区中常量引用的对象 4.本地方法栈中引用的对象

深入理解java虚拟机_第4张图片

再谈引用

强引用

直接引用,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象

软应用

描述一些有用但是非必须的对,在发生内存溢出之前,将这些对象列入回收范围进行第二次回收

弱引用

非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前

虚引用

唯一目的:能后在对象被收集齐回收时收到一个系统通知

对象存活与否

要真正宣告一个对象死亡,至少要经历两次标记过程。如果可达性分析后没有与GC Root相关联的引用链,则第一次被标记筛选,筛选出有必要执行finalize()方法的对象.

该对象被放置在F-Queue队列中,由优先级低的Finalizer线程执行,该方法执行不承诺等待运行结束,如果对象在finalize()方法中执行缓慢,或者发生死循环,将对导致F-Queue队列的永久等待。

finalize()方法是对象被回收的最后一次机会,如果在finalize中成功拯救就会被移除队列。对象只有一次自救就会,因为finalize最多被系统执行一次

回收方法区

主要回收内容:废弃常量 + 无用的类

无用常量:当前系统没有一个对象对应常量值。如果发生回收,必要的话会被清除

无用的类:1.类的实例已被回收 、 2.加载该类的ClassLoader已被回收 、3.对应的CLass没有被任何地方引用。(在大量使用反射、动态代理等频繁定义CLassLoader的场景都需要虚拟机具备卸载类功能)

通过-XX:+TraceClassLoading参数查看类加载信息

垃圾收集算法

1.标记-清除算法

算法分“标记”、“清除”阶段。首先标记处需要回收的对象,然后统一回收被标记的对象

两个不足,1.效率问题,标记和清除两个过程效率都不高。2.标记和清除后产生大量不连续的内存碎片会导致分配大对象时,找不到连续内存而不得不提前触发另一次垃圾收集

2.复制算法

将内存容量划分为相等的两块,每次只使用其中一块,其中一块的内存使用完,则将还存活的对象复制到另一块,然后对已使用过的那一块进行清除

实现简单,运行高效。 内存缩小代价大。

现代内存分配为较大的Eden区和两块较小的survivor区(进行复制算法的实现),当survior不够用的话,对象直接通过分配担保机制进入老年代.

3.标记整理算法

老年代的整理算法,标记之后,进行清除内存并且整理存活对象,保证连续内存空间

4.分代收集算法

新生代/老生代 使用不同的垃圾回收算法

HotSpot算法实现

枚举根节点

可达性分析必须确保在一致性的快照中进行,通过OopMap的数据结构来记录对象指针引用。完成GC Root枚举。

安全点

只在特定位置(安全点)记录OopMap信息

安全点选择标准:是否具有让程序长时间执行的特征

如果在GC发生时让所有线程在最近的安全点上停顿

抢先式中断:GC发生,线程全部中断,发现中断的地方不在安全点的线程,恢复线程让他跑到安全点

主动式中断:Gc发生,给每个线程一个标记,每个线程主动轮询标记,自动挂起。

安全区域

线程处于sleep和blocked阶段,无法响应中断请求

程序会进入安全区域。

内存分配与回收策略

对象优先在Eden分配

对象在新生代Eden区分配,当Eden没有足够的空间时会进行一次minor GC(新生代GC,回收速度比较快)

大对象直接进入老年代

大对象:需要连续内存空间的java对象,长字符串以及数组。

长期存活的对象将进入老年代

对象年龄计数器,保证达到对应年龄的对象进入老年代。

动态对象年龄判断

当Survivor中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或者等于的会直接进入老年代

空间分配担保

在发生Minor Gc的时候,会检查老年代最大连续可用空间是否大于新生代,如果成立,minor Gc确定是成立的,如果不成立,查看是否设置担保失败,允许的话则检查历代晋升老年代的对象平均大小,如果大于,进行一次minor gc。

如果小于或者不设置担保失败,就进行一次Full GC.

4.虚拟机性能监控和故障处理工具

1.jps 虚拟机进程状态工具

jps [ options ] [ hostid ]

options 参数说明:
深入理解java虚拟机_第5张图片

参考地址:https://www.jianshu.com/p/d39b2e208e72

2.jstat 虚拟机统计信息监视工具

https://blog.csdn.net/maosijunzi/article/details/46049117

3.jinfo java配置信息工具

https://www.jianshu.com/p/8d8aef212b25

4.jmap java内存映射工具

https://www.jianshu.com/p/a4ad53179df3

5.jhat 虚拟机堆转储快照分析工具

https://www.jianshu.com/p/1347cc8d79ee

6.jstack java堆栈跟踪工具

https://www.jianshu.com/p/025cb069cb69

7.javap反编译命令

https://www.jianshu.com/p/6a8997560b05

jdk可视化工具

JConsole:java监视和管理控制平台

JDK/bin目录下

https://www.cnblogs.com/hy007x/p/6984794.html

VisualVm:多合一故障处理工具

5.调优案例分析与实战

1.高性能硬件上程序部署策略

2.集群间同步导致的内存溢出

3.堆外内存导致的溢出错误

4.外部命令导致系统缓慢

5.服务器JVM进程奔溃

6.不恰当的数据结构导致内存占用多大

7.由windows虚拟内存导致的长时间停顿

6.虚拟机执行子系统

类文件结构分析

概述

实现语言无关性的基础仍然是虚拟机和字节码存储格式,java虚拟机不和包含java在内的任何语言绑定,他只与Class文件这种特定的二进制文件格式相关联

class类文件结构

Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数 + 表

无符号数 属于基本的数据类型。描述 数字、索引引用、数量值、字符串值

表 由多个无符号数或者其他表作为数据结构构成的复合数据类型,习惯性以“_info”结尾。整个class文件本质就是一张表

魔数:Class文件开头的4个字节,确定该文件是否为一个能被虚拟机接受的Class文件。

class文件版本号,5-6:次版本号,7-8:主版本号

常量池,常量数量不固定,入口处放置一个u2类型数据,代表常量池的计数值。

两大类常量 —字面量:文本字符串、生命为final的常量值

—符号引用:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符

常量池的每一个常量都是一个表(涉及14个表),每个表的第一位都是标识该常量属于哪种常量类型

访问标记,两个字节表示,用来标识类或者接口层次的访问信息。

类索引、父索引、接口索引集合:类的全限定名、父类的全限定名、实现的接口集合

字段表集合,描述接口或者类中声明的变量(包含类级变量和实例级变量,不包含局部变量)

方法表集合,(访问标记、名称索引、描述符索引、属性表集合)

重载:除了要与原方法具有相同的简单名称之外,还必须要求与原方法不同的特征签名(各个参数在常量池中的字段符号引用的集合,不包含返回值)

属性表集合,Class文件、字段表、方发表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

7.虚拟机类加载机制

类加载的时机

深入理解java虚拟机_第6张图片

五种情况必须立即对类进行初始化:

遇到new、getstatic、putstatic、invokestatic指令时

反射调用时

初始化类,发现父类未初始化

需要执行的主类

动态语言支持.

上述五种情况称为对一个类进行主动引用

所有的引用类方式都不会触发初始化,称为被动引用

1.通过子类引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化

2.通过数组定义引用类不会触发此类的初始化

3.常量在编译阶段会存入类的常量池,引用某个类的常量,不会触发该类的初始化.

类加载的过程

加载

通过一个类的全限定名获取定义此类的二进制字节流

将这个字节流所代表的静态存储结构转化成方法区的运行时数据结构

在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口

验证

文件格式验证

只有通过这个阶段验证,字节流才会进入内存的方法区进行存储,后面的验证阶段都是基于方法区操作

元数据验证

对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息

字节码验证

通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的.

符号引用验证

在解析阶段进行,可以看做是对类自身以外的信息进行匹配性校验.

准备

正式为类变量分配内存并设置类变量初始化值的阶段,变量所使用的的内存都将在方法区中被分配,仅包含类变量的默认值分配,不包含实例变量

解析

将常量池内的符号引用替换为直接引用的过程,具体过程看文章,略

初始化

真正开始执行类中定义的java代码。

静态语句块只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,在前面的静态语句块中可以赋值但是不能被访问。

父类的静态语句块要优于子类执行

类加载器

类加载器

java把“通过一个类的全限定名获取定义该类的二进制字节流”的动作放在Java虚拟机外部实现,以便于让应用程序自己决定如何去获取所需要的类

只有两个类1.由同一个类加载器加载,2.源于同一个Class文件,3.被同一个虚拟机加载,他们才是相等的。

双亲委派模型

分类:1.启动类加载器,2.抽象于ClassLoader的类加载器

启动类加载器

拓展类加载器

应用类加载器

双亲委派模型:
深入理解java虚拟机_第7张图片

如果一个类加载器收到类加载请求,首先把这个请求委派给父类加载器去完成,所有的加载请求都会传送到最顶层的启动类加载器,只有当父加载器反馈无法完成加载请求时,子类才会尝试自己去加载

目的:java类随着类加载器一起具备了一种带有优先级的层次关系,举例Object类,因为最终都是委派给启动类加载器加载,所以在各个类加载器环境中都是同一个类,而不会出现多个不同的Object类

8.虚拟机字节码执行引擎

运行时栈帧结构

栈帧适用于支持虚拟机进行方法调用和方法执行的数据结构,他是虚拟机运行时数据区中的虚拟机栈的栈元素

对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧。

局部变量表

一组变量值存储空间,用于存放方法参数和方法内定义的局部变量

操作数栈

动态连接

方法返回地址

附加信息

方法调用

解析

解析调用一定是静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,而不会延迟到运行期再去完成

分派

静态分派

Human h = new Man()

Human 是变量的静态类型,Man是变量的实际类型。静态类型的变化仅仅在使用时发生,变量本身的静态类型是在编译器可知的,实际类型在运行期才能确认

静态分派的典型应用就是方法重载,虚拟机重载是通过静态类型而不是实际类型作为判断依据的。

动态分派

重写

把常量池中的类方法符号引用解析到不同的直接引用上,这个过程就是重写的本质。

单分派与多分派

Son继承father

Father father = new Father();

Father son = new Son();

father.say();

son.say();

上面一个执行Father的say方法,下面一个执行Son重写的say方法。

分析:编译期是静态分配的过程,通过静态类确认两个方法执行都指向Father类。
运行期是动态分派,影响执行的是实际类型。

java语言属于静态多分派、动态单分派的语言。

动态分配的实现

为避免频繁的搜索,在方法区建立了虚方法表,通过方法表索引代替元数据查找以提高效率

子类若重写方法,子类方法表的地址指向重写方法,否则指向父类的方法地址。

动态类型语言支持

10.编译器优化

Javac编译器

解析与填充符号表的过程

插入式注解处理器的注解处理过程

分析与字节码生成过程

Java语法糖

泛型与类型擦除

伪泛型

自动装箱、拆箱与遍历循环

条件编译

12.Java内存模型与线程

volatile型变量

具备特性:保证此变量对所有线程的可见性,当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即的得知的。但是不保证同步
禁止指令重排序优化。内存屏障

你可能感兴趣的:(技术书分享)