深入JVM读书笔记

2018/01/18

今年买了本深入java虚拟机的书来学习,由于工作时间,只读了一半,争取后续读完,顺便贴一下自己的读书笔记。

一、JVM介绍

Classic VM:第一代虚拟机
Exact VM:生命周期短
Sun HotSpot VM:目前最广的虚拟机
JRockit VM:BEA公司开发,速度快(后期与HotSpot融合)
Dalvik VM:Android平台的虚拟机(没有遵循虚拟机规范)

二、内存管理

1.JAVA内存区域

1.1.程序计数器:记录线程所执行的字节码的行号指示器。(对于native方法为空)

1.2.Java虚拟机栈:方法执行创建栈帧(局部变量表(编译期间就知道分配多大内存),操作数栈,动态链接,方法出口等)用于入栈出栈,局部变量表:存放基本数据类型,对象引用。(long和double占用2个局部变量空间)。如果栈深度大于虚拟机运行的深度,StackOverflowError,无法申请足够的内存OutOfMemoryError(线程私有)

1.3.本地方法栈:与JAVA虚拟机栈相似,为了执行native方法服务。

1.4.Java堆:线程共享,虚拟机启动时创建,存放对象实例和数组(类的成员变量)(堆还能细分:新生代,老年代等),堆没有内存可扩展OutOfMemoryError

1.5.方法区:线程共享,存储被虚拟机加载的类信息, 常量,静态变量,即时编译器编译后的代码等(方法区:垃圾回收低,但也是有回收的必要)OutOfMemoryError(线程共享)

1.6.运行时常量池:方法区的一部分,Class文件有常量池:存放编译期生成的各种字面量和符号引用,类加载后进入方法区,这部分存放运行时常量池,运行期间也可能将新的常量放入池中,String.intern(),也会抛出OutOfMemoryError

1.7.直接内存:不是虚拟机规范的内存区域,在NIO中使用native方法直接分配堆外内存,虽然不受虚拟机限制,但是可能超过本机内存,可能会出现OutOfMemoryError

2.对象创建过程

new指令:检查常量池中定为到一个类的符号引用,并且检查是否被加载、解析和初始化。没有就执行类加载过程,为新生对象分配内存,将分配到的内存空间都初始化为零值,堆对象进行设置(哪个类的实例,哈希码等),执行init方法。

堆的分配:

指针碰撞:堆是规整的情况下,只需要堆上的指针移动相对应大小的距离就是分配内存

空闲列表:堆是不规则的情况,从列表中找到足够大的空间划分给对象实例。

堆是否规整取决于垃圾收集器是否有压缩整理功能

内存分配对于线程同步情况:CAS配上失败重试保证原子性,宁外一种每个线程在堆中预先分配一小块内存(本地线程分配缓冲TLAB)只有TLAB用完之后分配新的才需要同步锁定

3.对象的访问定为

1.句柄访问:堆中划分一块内存做句柄池,栈上存放句柄地址

2.直接指针访问:需要考虑类型数据的相关信息,存储对象的地址 句柄的好处:对象移动只需要修改句柄,直接指针定位快(HotSpot用第2种)

3.内存溢出异常

堆溢出:不断创建对象同时不被回收,(-Xmx-Xms来设置堆最大值与最小值)

栈溢出:不断创建本地变量, 通过设置-Xss栈内存容量 方法区溢出:在OutOfMemoryError会跟着PermGen space (-XX:PermSize -XX:MaxPermSize设置方法区大小)

三、垃圾收集器与内存分配策略

过程:设置安全点等待线程到达进行GC,根据可达性算法分析对象是否收集,通过收集算法处理内存。实现就是垃圾收集,根据新生代和老牛年还有CPU等分了很多种

1.垃圾收集

程序计数器,虚拟机栈,本地方法栈:随线程而生而灭,栈帧也基本是类结构就确定,无须过多考虑,主要考虑堆和方法区

2.判断对象是否存活

2.1.引用计数算法

通过对象中添加计数器,有引用加1,失效减1,为0不被使用。无法解决2个对象互相应用(oa.i=ob,ob.i=oa)虚拟机不是使用这种

2.2.可达性分析算法

以GC Roots作为起点开始向下搜索引用链,当一个对象到GC Roots没有引用链就被当做不可用。(栈中的引用的对象,方法区引用的对象都可以作为GC Roots)

3.引用
强引用:Object a = new Object();只要强引用,永远不会被回收掉
软引用:有用但非必须的对象,在系统快发生内存溢出之前,会对这些对象进行二次回收,如果还不够才会抛出异常
弱引用:有用但非必须的对象,比软引用更弱一些,只能生存到下次垃圾收集发生之前
虚引用:最弱的引用,在被对象回收时收到一个系统通知
4.对象的回收

不可达对象,在第一次标记之后会执行finalize方法,如果在改方法中又重新建立了关联,就不会被回收,如果第二次被回收,即使在finalize中关联也没用,也会被回收。只能拯救一次

5.垃圾收集算法

5.1.标记清除算法:标记和清除两个阶段,先标记需要回收的对象,在标记完统一回收所有被标记对象(效率不高,产生内存碎片)

5.2.复制算法:把内存一份为二,每次只用其中一块,回收时,把存活的对象全部复制到宁外一块上,在一次性清理使用过的内存,(不用考虑内存碎片,浪费了一半内存)。新生代就是使用这种(回收率高)

5.3.标记整理算法:标记过程与标记清除算法一致,在整理时,让存活对象全部向一端移动,然后直接清除掉端边界以外内存。(适用老年代)

5.4.分代收集算法:大部分虚拟机用这种,把堆划分老年代,新生代,根据年代特点,选择适合的算法。

6.HotSpot算法实现(何时进行GC)

6.1.枚举根节点:GC的时候为了保证一致性要系统停顿,使用OopMap来保存引用位置,GC扫描就可以直接知道信息。

6.2.安全点:为了不让每条指令都生成OopMap,需要设置安全点,在特定位置记录。在GC的时候让所有线程都到安全点。

6.2.1.抢先式中断:把全部线程停止,恢复不再安全点的线程,让它运行到安全点。

6.2.2.主动式中断:GC时候设置标志位,所有线程自己轮询判断是否到达。(都采用这种)

6.3.安全区域:有些Sleep状态的线程无法走到安全点,引入安全区域(拉伸的安全点),在安全区域的任意地方GC都是安全的。

7.垃圾收集器

7.1.Serial收集器:最基本,早悠久的收集器,使用单线程收集,使用复制算法,收集时停止全部线程(使用于新生代,Client模式下的虚拟机,简单高效)

7.2.ParNew收集器:Serial收集器的多线程版本。默认开启的线程数等于cpu数量,在不是非常多的CPU下,效率不如Serial收集器

7.3.Parallel Scavenge收集器:也是多线程新生代收集器。专注于吞吐量,运行代码时间和垃圾收集时间的比例(MaxGCPauseMillis,GCTimeRatio设置)

7.4.Serial Old收集器:Serial收集器的老年代版本,使用标记整理算法

7.5.Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法

7.6.CMS收集器:基于标记-清除算法,初始标记(stop world),并发标记,重新标记,并发清除

7.7.G1收集器:最新的收集器。

8.内存分配策略

8.1.对象优先在Eden分配

8.2.大对象直接进入老年代:-XX:PretenureSizeThreshold 来设置对象的限制大小,超过就是大对象

8.3.长期存活的对象进入老年代:根据年龄来判断对象(每次minor gc 年龄加1)-XX:MaxTenuringThreshold设置年龄限制

8.4.动态对象年龄判定:在Survivor空间相同年龄的对象大小之和大于空间一半,年龄大于等于的对象都进入老年代

8.5.分配空间担保:HandlePromotionFailure来设置是否启用。判定老年代的连续空间是否大于新生代所以对象总空间。如果开启,在没有大于,会判定每次晋升老年代的平均大小是否超过,超过了就进行full gc,没开启也进行 full gc。

三、性能监控与故障处理工具

1.JDK的命令行工具(bin里面的程序都是对tools.jar的包装,实际实现都在jar内部)

1.1.jps:虚拟机进程状况工具—-可以列出正在运行的虚拟机进程,并显示虚拟机执行的主类名称以及唯一ID(LVMID) 如:jps -l:输出全部信息,-v:启动指定的参数列表

1.2.jstat:虚拟机统计信息监视工具—-显示类装载、内存等运行数据,非gui图形界面,定位虚拟机性能首选工具

jstat  option vmid interval count (option:-class,-gc,-gcutil等)
如:jstat -gc 2764  250 10 监控进程2764的gc情况,250毫秒查询一次查询20次

1.3.jinfo:Java配置信息工具—-实时查看和调整虚拟机各项参数 如:jinfo -flag CMSInitiatingOccupancyFraction 14032

1.4.jmap:Java内存映像工具—-生成堆转储快照文件(headdump) 如:jmap -dump:format=b,file=xx 14032

1.5.jhat:虚拟机堆转储快照分析工具—-与jmap配合使用分析jmap生成的快照 如:jhat xx,可以打开浏览器查看

1.6.jstack:Java堆栈跟踪工具—-生成当前时刻的线程快照,定位长时间停顿原因等 如:jstack -l 14032

1.7.HSIDS:JIT生成代码反汇编

2.JDK的可视化工具

2.1.JConsole:Java监视与管理控制台—-基于JMX的可视化工具

2.2.VisualVM:多合-故障处理工具–最强大的监视处理工具

可添加插件:工具->插件

四、调优案例分析与实战

1.在16GB内存上,分配了12GB的堆内存导致FullGc时间很长——使用集群来部署,一个应用2GB。或者能控制FullGc的频率很低

2.集群间同步导致内存溢出,网络情况不好的情况下,所有请求都在等待,导致内存溢出——不应当过于频繁的写操作

3.堆外内存导致溢出,使用nio,jni代码,这些使用的内存不在堆中,分配不大的情况,也不会自动gc,会导致溢出

4.外部命令导致系统缓慢,使用Runtime.getRuntime().exec()执行shell脚本,消耗很大,占用cpu。减少这种操作

5.服务器JVM进程崩溃,异步调用外部接口,外部系统缓慢拖累,导致等待线程增多,使用生产消费队列模式

五、类文件结构

1.无关性的基石

jvm仅仅要求是class文件,可以支持其他语言,能编译成class文件即可,例如JRuby,Groovy等

2.class类文件结构

2.1.Class文件以8位字节的二进制流,使用无符号数和表来存储数据

2.2.Class头4字节为魔数,确定文件是否是class文件。值为(0xCAFEBABE)后面4字节,5-6字节是次版本号,7-8是主版本号,jdk从45开始计算

2.3.常量池:跟在主次版本之后,使用2字节标识常量的数量,每一种常量都使用不同的表结构来存储,如:CONSTANT_Class_info 7 类或接口的符号引用,0x07:代表是class类型,class_info的结构是u1:tag,u2:name_index,每个常量表结构第一位都是u1标志位表示常量类型。

一般class方法、字段都引用CONSTANT_Utf8_info来描述,它的length是u2即:65535,所以定义不能超过64KB英文字符。

2.4.访问标志:在常量池之后,u2来识别,有16个标志位可以使用(65535个),目前自定义了8个,如:0x0001:是否public类型,0x0010:是否final,计算满足的做一个 运算:0x0001 0x0010=0x0011,即是标志的值

2.5.类索引,父类索引,接口索引:占位:类索引:u2,父类索引:u2(java只能单继承),接口索引:u2(表示计数器,接口的索引的个数),后面跟着计数个u2接口索引。索引就是去常量池中查找第n个常量

2.6.字段表集合:标志位:u2(同类的访问标志,定义变量的public,final等),

name_index:u2(字段的简单名称的常量池索引)比如:inc(), m简单名称:inc,m ,
descriptor_index:u2(字段和方法的描述符的常量池索引),描述符:描述字段的数据类型(使用标识字符:byte用B,对象用L),方法的参数列表和返回值,如:String[][] 描述符:[[Ljava/lang/String,void inc(int num)描述符:(I)V
attributes_count:u2(标识额外信息的个数)
attributes:额外信息

2.7.方法表集合:同上已做了说明,方法内部的代码会放在attributes一个code的属性里面。如果没有重写父类方法,方法集合表是不会出现的

2.8.属性表集合:属性名称:u2(常量池的索引),属性的长度:u4,属性的内容

常见属性:

Code:程序方法体中的代码经过编译变成字节码指令存储在Code属性。其中有max_stack(u2):操作数栈深度的最大值
Exceptions:列举出方法中可能抛出的受查异常(throws 后面的异常)
3.字节码指令简介

3.1.jvm的指令有一个字节长度的、代表某种特定操作含义的数字(操作码)以及随后0-n个代表此操作所需参数(操作数)构成 一个字节限制了操作码总数不能超过256个

3.2.加载和存储指令:

iload、fload等:将一个数值从局部变量表加载到操作数栈
istore、fstore等:将一个数值从操作数栈存储到局部变量表
bipush、ldc、lconst等:常量加载到操作数栈

3.3.运算指令:

iadd(加法),isub(减法),imul(乘法),idiv(除法),irem(求余),ineg(取反),ishl(位移),ior(按位或),iand(按位与),ixor(按位异或),iinc(局部变量自增),dcmpg(比较)

3.4.类型转换指令

窄化类型转换时,必须显式的使用转换指令完成。i2b、i2c、i2s、l2i等

3.5.对象创建和访问指令

new:创建实例,newarray、anewarray创建数组,
getfield、putfield,getstatic,putstatic:访问类字段和实例字段,
baload、caload等:数组元素加载到操作数栈,bastore、castore等:将一个操作数存储到数组元素

3.6.操作数栈管理指令

pop、pop2:将操作数栈的栈顶一个或两个元素出栈
swap:将栈最顶端两个数值交换

3.7.控制转移指令

ifeq、iflt、ifnull、if_icrmplt等:条件分支
tableswitch、lookupswitch:复合条件分支
goto、jsr、ret:无条件分支

3.8.方法调用和返回指令

invokevirtual:调用对象的实例方法,根据对象的实际类型进行分派
invokeinterface:调用接口方法,在运行时搜索一个实现方法进行调用
invokestatic:调用类方法(static方法)
ireturn、lreturn等:方法返回

3.9.异常处理指令

athrow:显示抛出异常的操作(throw语句)

3.10.同步指令

monitorrenter、monitorrexit:支持synchronized关键字的语义

六、虚拟机类加载机制

未完待续

博客搜索