《Java虚拟机精讲》_笔记


JIT编译器、垃圾回收、运行时数据区、类加载机制、对象结构。
重点关注代码编译、解释执行、GC、运行时数据区。

1,Java体系结构

1.1 认识Java

Java SE、Java EE、Java ME。
java为编译解释型语言。
java程序只能运行在Java虚拟机中,与物理机隔离;可禁止许多不安全操作。

1.2 重要概念

java体系:java编程语言、字节码、Java API、Java虚拟机
所有jvm编译出的字节码文件相同;在不同平台执行时,解释为平台相关机器码。
HotSpot虚拟机:解释器、编译器。解释器启动时间短,内存占用低;编译器执行效率高。
编译器:Client Complier(C1)、ServerComplier(C2)
Java7默认开启分层编译策略(Tiered Compliation);C1进行简单优化,C2进行耗时长的优化。也可指定完全解释策略、完全编译策略。

1.3 安装与配置

下载jdk并安装
window配置环境变量,JAVA_HOME、PATH。
Linux执行vi /etc/profile,配置JAVA_HOME、PATH;而后source /etc/profile,刷新。
执行javac,检验是否正常

2,字节码的编译原理

前端编译器,将java代码转换为字节码。不涉及优化。

2.1 javac编译器简介

eclipse java编译器ECJ,为增量式编译器。
javac编译步骤:词法分析、语法分析、语义分析、生成字节码。

2.2 词法解析

读取源码,将关键词、标示符转为Token序列;按指定规则校验序列。
按行读取转换,分号分隔。

2.3 语法解析

将Token序列整合为抽象语法树。
JCCompilationUnit作为根节点,子节点:package语法节点、import语法树、class语法树。

2.4 语义解析

检查、完善语法树:

  • 无构造器类,添加默认无参构造器。
  • 检查变量是否初始化、变量类型是否与值匹配。
  • 将String类型的常量进行合并处理。
  • 检查代码中的所有操作是否可达。
  • 异常检查;检查性异常必须catch,或throws。
  • 解除java语法糖。

2.5 生成字节码

JVM的架构模型基于栈,所有操作都经过入栈、出栈完成。

2.6 实战

class文件,首行前4字节,0xCAFEBABE;magic,校验文件是否为合法的字节码文件。
5、6字节为编译的次版本号;7、8为编译的主版本号。
字节码文件无分隔符,16位、32位、64位数据构造成2个、4个、8个字节单位来表示。
javap,jdk自带反编译命令。javap -c Lkk,Lkk为类名。
GCJ,可将java源码直接编译为本地机器指令;从而脱离jvm运行。无法跨平台。

3,字节码文件

3.1 字节码文件的内部组成结构

将描述类结构格式的内容定义为项(item)。各项按严格的顺序进行连续存放。
项包含类型、名称、项的数量。类型:表、基本类型(u1、u2、u4、u8)。
表是由多个无符号数、表作为数据项的符合数据类型;后缀为"_info"。字节码文件实质上是一张表。

ClassFile {
u4			magic;
u2			minor_version;
u2			major_version;
u2			constant_pool_count;
cp_info		constanc_pool[constant_pool_count-1];
u2			access_flags;
u2			this_class;
u2			super_class;
u2			interfaces_count;
u2			interfaces[inferfaces_count];
u2			fields_count;
field_info		fields[fields_count];
u2			method_counts;
method_info	methods[method_count];
u2			attributes_count;
attribute_info	attributes[attributes_count]}

magic;魔数,固定0xCAFEBABE;校验文件是否为合法的class文件。
minor_version、major_version;编译的次版本号、主版本号
constanc_pool_count、constant_pool;常量池计数器、常量池

  • 常量池计数器;从1开始。其他计数器从0开始。
  • 常量池;存放字面量literal、符号引用symbol reference。索引为0时不引用常量池 。

access_flags;访问标志
this_class、super_class;类索引、超类索引。指向常量池中CONSTANT_Class_info的常量项,获取全限定名。

  • super_class,只能一个;0时默认为java.lang.Object

interfaces_count、interfaces;接口计数器、接口表。

  • interfaces,为索引数组。

fields_count、fields;字段计数器、字段表。

  • fields;类变量、实例变量,field_info的数组。只包含本类声明的变量。

methods_count、methods;方法计数器、方法表

  • methods;静态方法、实例方法,method_info数组。只包含本类声明的方法。

attributes_count、attributes;属性计数器、属性表。

3.2 符号引用

常量池中主要存放字面量、符号引用;通过索引访问,索引0不引用常量池值。
符号引用构成:全限定名、简单名称、描述符。
字节码文件中,全限定名的点号".“被替换为”/"。
简单名称:存储类、接口中定义的变量、方法。如String#toString(),存储为toString。
描述符:字段描述符(字段类型)、方法描述符(返回类型,参数列表)。

  • B,byte;S,short;I,int;J,long
  • D,double;F,float
  • C,char;Z,boolean
  • L,对象;[,数组
  • V,方法返回类型为Void。

()Ljava/lang/Object;方法无参。

3.3 常量池

常量项格式:cp_info { u1 tag; u1 info[]; }
字面量:Utf8、Integer、Long、Float、Double
符号引用,指向Utf8字面量。
注意:CONSTANT_String_info也指向CONSTANT_Utf8_info

3.4 字段表

field_info {
u2	access_flags;	访问修饰符
u2	name_index;	名称
u2	descriptor_index;	描述符,类型
u2	attributes_count;	属性个数
attribute_info attributes[attribute_count];	属性集合
}

要求同名的字段,描述符不能相同。

3.5 方法表

method_info {
u2	access_flags;
u2	name_index;
u2	descriptor_index;
u2	attributes_count;
attribute_info attributes[attributes_count];
}

方法签名相同的方法,返回值不能相同。

3.6 属性表

attribute_info {
u2	attribute_name_index;
u4	attribute_length;
u1	info[attribute_length];
}

6,内存分配与垃圾回收

6.1 JVM的运行时数据结构

方法区、堆、栈(java栈、本地方法栈)、程序计数器。
方法区、堆;线程共享。
栈、程序计数器;线程私有。

6.2 线程共享内存区

堆区:YoungGen(Eden、s1、s2)、OldGen。

  • -Xms,堆区起始大小;-Xmx,堆区最大大小。

方法区:运行时常量池、类信息(字段、方法)。也称永久代。

  • HotSpot中,方法区逻辑上独立;实际还是包含在Java堆区内。
  • -XX:MaxPermSize;设置方法区内存。
  • 默认回收常量池、类型卸载;可指定不回收方法区。

运行时常量池:字节码文件中常量池表的运行时表现形式。

  • 内容:字面量、符号引用。
  • 类加载器加载类后,会创建与之对应的运行时常量池。

6.3 线程私有内存区

java栈、本地方法栈、程序计数器
PC寄存器(Program Counter Register),也称程序计数器。

  • 执行java方法时,存储正在执行的指令地址;
  • 执行native方法时为空(undefined)。

java栈,也称java虚拟机栈(Java Virtual Machine stack)。存储栈帧。

  • 栈帧存储局部变量表、操作数栈、方法出口等信息。
  • java堆存储对象实例;局部变量表存储基本类型、对象引用、返回地址等。
  • java栈可设为固定大小,也可设为动态扩展。

本地方法栈,用于支持本地方法(native 方法)的执行。

6.5 自动内存管理

内存泄漏(无效引用一直占用内存)、内存溢出(内存不足新建对象)。
内存分配原理
JVM引用类型:类类型(class type)、数组类型(array type)、接口类型(interface type);引用类型的值分别由类实例、数组实例、接口派生类实例负责动态创建。
new指令创建对象时,先在常量池中定位类的符号引用,检查是否已加载;确定对象内存大小,分配内存。
内存分配方式,并发分配堆空间时,加锁确保安全:

  • 指针碰撞;内存规整有序分布时,通过指针偏移分配内存。
  • 空闲列表;
  • 快速分配;TLAB,本地线程分配缓冲区,包含在Eden,优先。
  • 大对象直接在老年代中分配空间

Eden中无法分配内存时,触发GC;回收空间。
创建对象过程:

  • 初始化;类加载,空间分配,零值初始化;
  • 实例化;初始化对象头、实例数据;
  • 对象引用入栈,更新PC寄存器。

逃逸分析与栈上分配
使用堆外存储技术,降低GC频率,提高效率。
利用逃逸分析技术(作用域),将未逃逸对象分配在栈内存空间;方法结束时释放。
对象内存布局与OOP-Klass
对象内存布局:

  • 对象头;Mark Word(对象运行时数据)、元数据指针(指向类)。
  • 实例数据;二进制位数相同的字段会划分到一起。
  • padding填充;对齐填充。

OOP(Ordinary Object Pointer);普通对象指针,表示对象头。对象引用指向对象头。
Klass;元数据指针,包含在OOP内。
GC的作用
GC(Garbage Collector);垃圾收集器,JVM中自动内存管理机制的实现。
HotSpot中,GC负责动态内存分配、垃圾回收。
评估GC性能,Tjvm,JVM运行时间,Tp程序运行时间,Tgc垃圾收集时间:

  • 吞吐量;Tp/Tjvm。Tjvm=Tp+Tgc。
  • 垃圾收集开销;Tgc/Tjvm
  • 暂停时间;stop_world时间
  • 收集频率;Tjvm/Tgc
  • 堆空间;
  • 快速;

垃圾标记:根搜索算法
引用计数算法;无法处理循环引用。
根搜索算法;可达性分析。不可达对象在Mark Word中标记为垃圾对象。
根对象集合,栈对象(堆空间)、方法区对象引用(常量池对象、静态属性对象、Class对象):

  • Java栈中的对象引用
  • 本地方法栈中的对象引用
  • 运行时常量池中的对象引用
  • 方法区中类静态属性的对象引用
  • 与一个类对应的唯一数据类型的Class对象

引用类型:

  • 强引用;
  • 软引用SoftReference;GC内存不足时回收
  • 弱引用WeakReference;GC时回收
  • 虚引用;对象被回收时接收通知

垃圾回收:分代收集算法
两阶段:垃圾标记、内存回收。
GC:Minor GC、Major GC(也称Full GC)。
标记-清除算法;简单。

  • 效率低;内存碎片。用于OldGen。

复制算法;快速,适用于YoungGen。

  • Eden-to,from-to。清空Eden、from。
  • 默认情况下,Eden:s1:s2=8:1:1;-XX:SurvivoRatio指定。
  • 若存活对象超过-XX:MaxTenuringThreshold,或to空间不足;移到OldGen。

标记-压缩算法;内存占用小,适用于OldGen。

  • 将存活对象移动到规整且连续的内存空间中。
  • 新建对象可使用指针碰撞分配内存。

选择垃圾回收算法时,YoungGen要求快速、高效,OldGen要求节省内存。

6.6 垃圾收集器

串行回收;单CPU执行垃圾回收,暂停工作线程。用于对暂停时间要求不高的场合,单CPU。
并行回收;多CPU同时执行垃圾回收,提高程序吞吐量。使用Stop-the-World机制和复制算法。
Stop-the-World机制;垃圾收集器在内存回收过程中暂停程序所有的工作线程;直至内存回收完毕。
并发回收;同一时段内,工作线程、垃圾收集线程同时或交叉运行。缩短Full GC时Stop-the-World延时。
Serial收集器;串行
用于YoungGen;采用复制算法、串行回收、Stop-the-World机制。
HotSpot Client模式YoungGen,默认收集器。
Serial Old收集器,用于OldGen;采用标记-压缩算法、串行回收、Stop-theWorld机制。
ParNew收集器;并行
用于YoungGen;Serial收集器的多线程版本;并行回收、复制算法、Stop-the-World机制。
Parallel收集器;吞吐量优先
用于YoungGen;并行回收、复制算法、Stop-the-World机制。
Parallel Old收集器;用于OldGen。标记-压缩算法。
可调整吞吐量,暂停时间:

  • -XX:GCTimeRatio;占空比,吞吐量,Tp/Tgc,默认99;1%的时间用于GC。
  • -XX:MaxGCPauseMillis;最大暂停时间。
  • -XX:UseAdaptiveSizePolicy;自动匹配

垃圾收集器中吞吐量、低延迟,存在相互竞争的矛盾。

  • 吞吐量优先,需降低内存回收频率;会导致GC需要更长的暂停时间。
  • 低延迟优先,需增大内存回收频率;引起新生代内存的缩减、程序吞吐量降低
    CMS(Concurrent–Mark-Sweep)收集器;低延迟
    用于OldGen;并发回收、标记-清除算法、Stop-the-World机制。
    执行过程:
  • 初始标记;Stop-the-World,标记不可达对象。
  • 并发标记;并发标记不可达对象为垃圾对象。
  • 再次标记;Stop-the-World,移除垃圾对象中的可达对象,确保正确标记。
  • 并发清除;

清除内存碎片:

  • -XX:+UseCMSCompactAtFullCollection;Full GC后压缩,会增大延迟
  • -XX:CMSFullGCsBeforeCompaction;N次Full GC后进行内存压缩

-XX:CMSInitiatingOccupancyFraction;设置自动内存回收阈值。默认92%。避免Full GC。
G1(Garbage-First)收集器
YoungGen、OldGen;逻辑区分。
将堆区划分为2048个大小相等的Region;1-32M。可缩短GC延迟,提高吞吐量。
G1优先回收占用内存较大的Region。避免全堆扫描。
G1执行过程,类似CMS:

  • 初始标记;
  • 根区域扫描;
  • 并发标记;
  • 再次标记;
  • 清除;
  • 拷贝;

GC组合

参数MinorGC
-XX:+UseSerialGCSerialSerialOld
-XX:+UseParNewGCParNewSerialOld
-XX:+UseParallelGCParallelSerialOld
-XX:+UseParallelOldGCParallelParallelOld
-XX:+UseConcMarkSweepGCParNewCMS主,Serial备
-XX:+UseConcMarkSweepGC-XX:-UseParNewGCSerialCMS主,Serial备
-XX:+UseG1GCG1G1

-XX:+UseParallelOldGC;吞吐量优先场景
-XX:+UseConcMarkSweepGC;低延时的场景

6.7 GC日志分析

日志中记录新生代、老年代、堆区、永久代内存情况,GC时间信息;对应匹配。
CMS、G1日志;对比执行过程理解。

6.8 分析dump文件

dump文件为堆内存镜像文件;通过jmap生成。

  • jmap -dump:live,format=b,file=FILE PID;生成指定PID的堆内存镜像文件。

jhat、HeapAnalyzer工具分析dump文件。

7,类加载机制

类加载过程:加载、连接、初始化。

7.1 类加载器

类加载器:读取二进制字节流,转换为Class对象实例。
JVM支持两种类加载器:启动类加载器、自定义类加载器(继承ClassLoader)。
常见类加载器:

  • Bootstrap ClassLoader;启动类加载器,加载JAVA_HOME/lib。
  • ExtClassLoader;自定义类加载器,加载JAVA_HOME/lib/ext。
  • AppClassLoader;自定义类加载器,加载ClassPath目录。

自定义类加载器,继承ClassLoader,重写findClass(),读入byte[],调用defineClass()转为Class。

  • defineClass();字节流转为Class对象,可解密加密的字节码文件。

双亲委派模型
除启动类加载器外,每个类加载器都有一个超类加载器。getParent()获取。

  • AppClassLoader的超类加载器为ExtClassLoader;
  • 自定义类加载器的超类加载器为AppClassLoader

类加载器加载类时,先将任务委派给超类加载器,直至启动类加载器;由启动类加载器加载类。
若超类加载器无法加载类,则将任务退回给下级类加载器。
双亲委派模型,可保证一个类的全局唯一性。
Tomcat中,默认的类加载器加载类时,先自行加载;若加载失败,则交由超类加载器加载。

7.2 类的加载过程

类加载过程:

  • 类加载;加载字节码文件,生成Class实例。
  • 连接;验证、准备、解析。
  • 初始化;静态变量赋值,执行static块。

类在首次主动使用时,执行初始化操作。调用非常量成员、子类初始化。

  • 创建实例;如:new、反射、序列化
  • 引用静态成员,常量除外。
  • 反射调用方法。
  • 初始化子类。只初始化父类,接口在调用非常量成员时初始化。
  • jvm启动包含main()方法的类。

加载字节码
加载二进制字节流,转为Class对象,存入方法区。
Class只用于java语法层面的反射调用。
验证阶段
格式验证;验证magic、版本号,加载阶段进行。
语义分析;java语法验证
操作验证;验证操作是否正确
符号引用验证;验证常量池符号引用,解析阶段进行。
准备阶段
按声明顺序,静态变量赋初值。常量不赋值。
解析阶段
字节码常量池中的符号引用转为直接引用;包括:类、接口、方法、字段。
初始化阶段
按声明顺序,静态变量、常量赋值,执行静态块。
字节码中对应()方法;javap反编译后,对应static{}方法。

8,剖析HotSpot的架构模型与执行引擎

HotSpot,解释器、编译器并存。

8.1 栈帧的组成结构

JVM的架构模型基于栈,栈声明周期与线程一致。
栈存储栈帧;栈帧存储:局部变量表、操作数栈、动态链接、方法返回值等。
栈帧是一种支持JVM调用/执行程序方法的数据结构,是方法的执行环境。
方法调用时创建栈帧,入栈;方法执行结束销毁栈帧,出栈。不同线程的栈帧不允许相互引用。
局部变量表、操作数栈的大小在编译期确定,并保存在方法的Code属性中。故栈帧的大小取决于JVM实现和调用方法时实际分配的内存。
局部变量表
也称本地变量表,包含在栈帧中;存储:方法参数、方法体内的局部变量(原始数据类型、对象引用、返回地址等)。
局部变量表类似线性表;最小单位为Slot,默认32bit。可根据CPU、OS、JVM改变。

  • 2个Slot存储long、double。64bit。
  • 1个slot存储其他基本类型、对象引用、返回地址。

通过索引范围局部变量表,0为当前对象(this);后续按方法中声明顺序访问。
操作数栈
也称表达式栈;栈结构,每个操作数栈的深度在编译期确定并存入Code属性。

  • 1个栈深度存32bit数据,2个栈深度存64bit。
  • 操作数栈的数据取自局部变量表,运算结果先入栈,再存入局部变量表。

JVM的任何操作都需经过入栈、出栈(操作数栈)完成,CPU和操作数栈交互;故JVM架构基于栈。
CPU执行指令位置?程序计数器确定。
动态链接
指针;指向运行时常量池中栈帧所属方法,实际运行时调用的方法。
链接:
静态链接;编译期确定、运行期不变;早期绑定,final方法。
动态链接;编译期无法确定,运行期根据实际类型绑定方法。晚期绑定,支持多态。
方法返回值
方法正常结束时可有返回值,异常结束时无返回值。
有返回值时,将其压入操作数栈;更新PC寄存器。

8.2 HotSpot中执行引擎的架构模型

执行引擎,字节码解释/编译为本地机器指令。供OS运行。
寄存器架构虚拟机,性能好;栈架构虚拟机,简单。
本地机器指令
机器指令组成:

  • 操作码;指令功能
  • 操作数;指定参与运算的操作数,哪里获取操作数,存储返回值
    寄存器架构和栈式架构
    指令:零地址指令、一地址指令、二地址指令…n地址指令
  • iload_1,iload_2,iadd,istore_3;零地址加法,4个指令
  • op dest, src1, src2;三地址加法,一个指令

栈架构优点:

  • 设计和实现简单,适用于资源受限的系统
  • 避开寄存器分配难题;跨平台
  • 指令集更紧凑。用零地址指令,需更多的出栈、入栈指令

寄存器架构优点:

  • 性能好
  • 同一操作指令更少。n地址指令。

基于栈式架构的设计
创建新线程时,JVM会为其分配一个独立的PC寄存器和java栈空间。
栈顶缓存
寄存器缓存本地机器指令、数值、下一条指令地址等。
将栈顶元素缓存在一个或多个寄存器中。局部变量表直接和寄存器交互。
链接
如果启用栈顶缓存,那么,栈操作将被替换成解释器方法内部的本地变量的赋值操作。而这个本地变量在编译器的优化下在寄存器上分配,那么本来的内存操作就成了寄存器操作。因为寄存器的速度远远大于内存。

8.3 解释器和JIT编译器

JIT编译器,Just In Time Compiler。
JVM:解释器、C1编译器(Client Compiler)、C2编译器(Server Compiler)
java命令参数:

  • -Xint;纯解释器
  • -Xcomp;纯编译器,编译器无法进行时使用解释器
  • -Xmixed;编译器、解释器共同运行。默。
  • -client;jvm client模式,使用C1编译器
  • -server;jvm server模式,分层编译,使用C1编译器、
    C2编译器

查看HotSpot的运行时执行模式
java -version;最后一行显示jvm模式,执行模式(默认mixed)。
java -Xint -version;查看解释器执行信息
解释器
Interpreter模块;字节码指令转本地机器指令。
Code模块;管理运行时生成的本地机器指令。

  • CodeCache子模块;缓存本地机器指令。指令缓存区、方法区,合称非堆内存。
  • -XX:CodeCacheExpansionSize;增量
  • -XX:InitialCodeCacheSize;初始值
  • -XX:ReservedCodeCacheSize;最大值

JIT编译器
JVM执行过程:

  • 取字节码,查指令缓存区、标准编译。
  • 若无缓存或编译,则解释执行;同时对热点代码进行编译。

JIT模块:

  • C1模块;Client编译器,C1
  • Opto模块;Server编译器,C2
  • Shark模块;基于LLVM的编译器。

分层编译策略

  • 第0层:解释器执行
  • 第1层:C1编译器进行简单优化
  • 第2层:C2编译器进行耗时更长的优化
  • 第3层:C1编译器采集性能数据进行优化
  • 第4层:C2编译器进行完全优化

热点探测
对热点代码(方法、循环块);总是编译整个方法。
统计方法调用计数器、回边计数器之和。

  • -XX:CompileThreshold;方法调用计数器阈值。client默认1500,server默认10000.
  • -XX:-UseCounterDecay;关闭热度消退。默认超时后消退50%
  • -XX:CounterHalfLifeTime;消退时间阈值。GC时执行消退操作

优化技术
公共子表达式消除
数组边界检查消除
方法内联
逃逸分析

个人总结

JRE=JVM+运行时类库
JDK=JRE+开发工具包
运行时数据区=堆、栈(java栈–栈帧、方法栈–本地方法)、方法区、程序计数器
堆区=Young(Eden、s1、s2)+Old
栈帧=局部变量表、操作数栈、动态链接、返回值等。
JMM,Java Memory Model。堆–栈数据通信。
栈架构,寄存器架构。java基于栈架构(CPU和操作数栈交互),栈顶缓存利用寄存器。
GC;minorGC、fullGC。算法:复制算法、标记–清除、标记–压缩。
垃圾标记;根搜索算法。栈可达、方法区引用。
对象创建:

  • 初始化。类加载,分配空间,零值初始化
  • 实例化。初始化对象头,实例化数据
  • 对象入栈,更新寄存器。

类加载: 加载,连接(准备–静态变量零值,解析–常量赋值),初始化(静态变量赋值)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值