深入理解Java虚拟机 JVM高级特性及最佳实践

深入理解Java虚拟机 JVM高级特性及最佳实践


目录:
第一部分 走近Java
第1章 走近java


第二部分 自动内存管理机制
第2章 java内存区域与内存溢出异常
第3章 垃圾收集器与内存分配策略
第4章 虚拟机性能监控与故障处理工具
第5章 调优案例分析与实战


第三部分 虚拟机执行子系统
第6章 类文件结构
第7章 虚拟机类加载机制
第8章 虚拟机字节码执行引擎
第9章 类加载及执行子系统的案例与实战


第四部分 程序编译与代码优化
第10章 早期(编译期)优化
第11章 晚期(运行期)优化


第五部分 高效并发
第12章 java内存模型与线程
第13章 线程安全与锁优化


附录
附录c 主要参数表


第一部分 走近java
第1章 走近java


第二部分 自动内存管理机制
第2章 java内存区域与内存溢出异常
2.2  运行时数据区域
2.2.1 java内存的分布
(1) 本地方法栈:NATIVE本地方法 C
(2) 虚拟机栈:局部变量表(编译期确定的基本类型及引用)、操作数栈、动态链接、方法出口等消息
(3) 程序计数器:执行class文件时候的计数
(4) 堆:对象实例(对象头、实例数据、对齐填充)
(5) 堆(方法区):已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
方法区和栈的区别,一个是运行期才能确定,而栈的是在编译期就能确定了。


2.2.7 JDK1.4 直接内存
JDK1.4新加入了NIO类,通过缓冲区及I/O方式显著提高性能,避免Java堆和Native堆来回复制数据,这控制的就是直接内存部分


2.3 hotspot虚拟机对象探秘
2.3.1 对象的创建new
(1) new对象时,先检查常量池有没有类加载,没有就先执行类加载
(2) 类加载后即确定所需内存,就在堆上划分出相应大小的空间
(3) 将分配到的初始空间置0,对象不需要赋初始值就可以在堆里直接使用
(4) 虚拟机给对象设置相应的对象头,类的元数据、hashCode、GC分代信息
(5) 执行init方法


2.3.1 对象的内存布局
2.3.2.1 内存的划分
(1) 指针移动法:移动一部分划出对应大小的内存(垃圾回收有压缩算法的情况)
(2) 空闲列表法:在内存列表中找到一块符合大小的进行分配(垃圾回收不进行回收,内存不连续的块)


2.3.2.2 对象头里的信息
(1) 运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
(2) 类型指针:用来找类,即方法区里的class文件


2.3.3 对象的访问定位
(1) 句柄方式:两个指针,一个控制class一个控制object实例
(2) 指针方式:直接控制object实例,object实例的对象头再用类指针控制class


2.4 内存OutOfMemoryError异常
OOM异常
IDE VM上设置相关参数避免溢出
2.4.1 堆溢出
设置 -Xms最小值 和 -Xmx最大值设置一样,比如20MB, 避免堆自动扩展 ,分析快照
2.4.2 虚拟机栈和本地方法栈溢出
线程请求栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常
虚拟机在扩展栈时无法申请到足够的内存空间,抛出OOM异常
最大堆容量 Xmx 最大方法区容量MaxPermSize
2.4.3 方法区和运行时常量池溢出
限制方法区大小 -XX:PermSize 和 -XX:MaxPermSize
String.intern() 是一个Native方法,如果字符常量池中已经包含一个等于此对象的字符串,则返回代表池中这个字符串的String对象;否则
将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用
2.4.4 本机直接内存溢出
可通过 -XX: MaxDirectMemorySize指定 如果不指定默认与Java堆最大值 -Xmx指定 一样


第3章 垃圾收集器与内存分配策略
3.1 概述
本地方法栈 虚拟机栈 程序计数器 都是随着线程的开启和完成存在和消失,不需要内存回收
而 方法区和堆区的对象产生是动态的,需要进行回收处理,哪些对象需要回收,怎么回收,什么时候回收
3.2 对象已死吗
3.2.2 可达性分析算法
GC Roots对象不可达,包括以下对象:
(1) 虚拟机栈(栈中的本地变量表) 中引用的对象
(2) 方法区中类静态属性引用的对象
(3) 方法区中常用引用的对象
(4) 本地方法栈中JNI(Native方法)引用的对象


3.2.3 引用
(1) 强引用: 比如 Object obj = new Object() 永远不会回收
(2) 软引用:指一些还有用但非必须的对象。在系统发生溢出前,会进行回收
(3) 弱引用: 弱引用的对象在下次垃圾回收时候会进行回收
(4) 虚引用:用来返回系统通知


3.2.4 生存还是死亡
在回收之前会调用一次finalize()方法,如果引用没有连上,那么就会被回收掉
重写finlize()方法把对象再连接到成员变量或者类变量上,就又复活了


3.2.5 回收方法区
两部分:废弃常量和无用的类
常量池,比如字符串"abc"在常量池,但没有任何引用,就会被回收,其他的接口、方法、字段的符号引用也是如此
无用的类:条件,该类所有实例被回收、加载该类的ClassLoader已经被回收,对应的java.lang.Class对象没有在任何地方被引用
无法在任何地方通过反射访问该类方法,即可回收


3.3 垃圾收集算法
3.3.1 标记清除
标记可回收对象、存活对象和正在使用对象,容易产生内存碎片,效率降低
3.3.2 复制算法
内存分为两块,用其中一块,清理时将存活对象复制到另一块,另一块统一清理,这样效率提高了,内存减少了一半
3.3.3 标记整理算法
在以上进一步,将存活对象移动到一端,另一端清理,这样不会浪费内存
3.3.4 分代收集算法
新生代:使用复制算法清理,因为存活对象少
老年代:使用"标记-清理"或"标记-整理"进行回收,因为存活对象多,没有额外空间进行复制


3.4 hotspot的算法实现
3.4.1 枚举根节点
GC Roots节点主要在全局性引用(常量或类静态属性)[方法区]与执行上下文(如栈帧中的本地变量表)中[栈区]
逐个检查会消耗时间,并且还存在GC停顿
JIT编译时,会在特定位置记录下栈和寄存器中哪些位置引用,这样GC扫描时可以直接得到这些信息,相当于一个索引
OopMap数据结构
3.4.2 安全点
抢先式中断,就线程主动跑到安全点上,然后暂停线程相应GC事件
主动式中断,GC需要中断线程时,设置一个标志,各个线程执行时去轮询这个标志,发现标志为真就自己中断挂起
3.4.3 安全区域
如果线程Sleep或者Blocked,无法相应JVM中断请求走到安全的地方挂起,就需要安全区域来解决
首先标识自己进入了安全区域代码,这段时间发生GC时,就不用管标识为安全区域Safe Region状态的线程了


3.5 垃圾收集器
收集算法是内存回收的方法论,那垃圾收集器就是内存回收的具体实现。
7种作用于不同分代的收集器,可以搭配使用


3.5.1 Serial收集器
老收集器
简单高效,单线程,适用于客户端,
新生代采用复制算法 老年代 采用标记-整理算法  GC暂停所有用户线程
3.5.2 ParNew收集器
Serial的多线程版本,只有它能配合CMS收集使用,适用于服务端
3.5.3 Parallel Scavenge 收集器
新生代复制算法收集器,吞吐量优先,并行并发收集器,通过控制每次的回收时间,充分利用CPU
而CMS等是尽可能的缩短垃圾收集时,线程的停顿时间
3.5.4 Serial Old收集器
老年代版本,单线程,标记-整理,用来配合 Parallel Scavenge使用,另一种作为CMS的备用
3.5.5 Parallel Old收集器
老年代版本,配合ParNew使用
3.5.6 CMS收集器
一种以获取最短回收停顿时间为目标的收集器,标记-清除算法
初始标记
并发标记
重新标记
并发清除
3.5.7 G1收集器
用来替代CMS ,特点:
(1) 并行与并发
(2) 分代收集
(3) 空间整合:标记整理+复制
(4) 可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不能超过N毫秒
初始标记-->并发标记-->最终标记-->筛选回收
内存被划分为多个大小相等的部分


3.5.8 理解GC日志
3.5.9 垃圾回收器参数总结


3.6 内存分配与回收策略
3.6.1 对象优先在Eden分配
Minor GC 新生代GC
Major GC 老年代GC
Full GC 全量GC


3.6.2 大对象直接进入老年代
比如很长的字符串、数组都是大对象


3.6.3 长期存活的对象进入老年代
每次存活 age+1


3.6.4 动态对象年龄判定
大于Survivor空间一半对象的年龄,进入老年代


3.6.5 空间分配担保
如果不够空间了只能进行一次Full GC


第4章 虚拟机性能监控与故障处理工具
4.1 概述
知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段
数据分析:运行日志、异常堆栈、GC日志、线程快照、堆转储快照等


4.2 JDK的命令行工具
4.2.1 jps:虚拟机进程状况工具
4.2.2 jstat:虚拟机统计信息监视工具
4.2.3 jinfo: java配置信息工具
4.2.4 jmap:java内存映像工具
4.2.5 jhat:虚拟机堆转储快照分析工具
4.2.6 jstack:java堆栈跟踪工具
4.2.7 hsdis:jit生成代码反汇编


4.3 jdk的可视化工具
4.3.1 jconsole:java监视与管理控制台
4.3.2 visualvm:多合一故障处理工具


第5章 调优案例分析与实战


5.2.1 高性能硬件上的程序部署策略
(1)高性能硬件上部署程序,目前主要有两种方式:
通过64位JDK来使用大内存
使用若干个32位虚拟机建立逻辑集群来利用硬件资源
(2) 64位JDK来管理大内存,要面临问题:a.内存回收导致的长时间停滞;b.现阶段,64位JDK的性能测试结果普遍D低于32位JDK;c.需要保证程序足够稳定,因为这种应用产生堆溢出几乎就无法产生堆转储快照(因为要产生十几GB乃至更大的Dump文件),哪怕产生了快照也几乎无法进行分析;d.相同程序在64位JDK消耗的内存一般比32位的JDK大,这是由指针膨胀,以及数据类型及对齐补白等因素导致的


不少管理员用第二种方式:使用若干个32位虚拟机建立逻辑集群来利用硬件资源。具体做法是在一台物理及其上启动多个应用服务进程,每个服务器进程分配不同端口,然后在前端搭建一个负载均衡,以反向代理的方式来分配访问请求
使用无Session复制的亲合式集群是一个相当不错的选择。均衡器按一定的规则算法(一般根据SessionID分配)将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可。


很少没有确定的方案,使用逻辑集群方式部署,可能会遇到下列问题
a. 尽量避免节点竞争全局的资源,最典型的就是磁盘竞争,各个节点如果同时访问某个磁盘文件的化(尤其是并发写操作容易出现问题),很容易导致IO异常
b. 很难最高效率的利用某些资源池,譬如连接池,一般都是在各个节点建立自己独立的连接池,这样有可能导致一些节点池满了而另外一些节点仍有较多空余。尽管可以使用集中式的JNDI,但这个有一定的复杂性并且可能带来额外的性能开销
c.各个节点仍然不可避免地受到32位的内存限制,在32位Windows平台中每个进程只能使用2GB的内存,考虑到堆以外的内存开销,堆一般最多只能开到1.5GB。在某些Linux或Unix(如Solari)系统中,可以提升到3GB乃至接近4GB的内存,但32位中仍然受最高4GB(2的32次方)内存的限制
d.大量使用本地缓存(如大量使用HashMap作为K/V缓存)的应用,在逻辑集群中会造成较大的内存浪费,因为每个逻辑节点上都有一份缓存,这时候可以考虑把本地缓存改为集中式缓存


方案调整: 部署5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用10GB,另外建立一个Apache服务作为前端均衡代理访问门户。考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,CPU资源敏感度较低,因此改为CMS收集器进行垃圾回收。部署方式调整后,服务再没有出现长时间的停顿,速度比硬件升级前有较大提升。


5.2.2 集群间同步导致的内存溢出
JBossCache缺陷+MIS系统实现方式上的缺陷


5.2.3 堆外内存导致的溢出错误
NIO操作导致的堆外内存溢出,因为它不记录在堆内存分布中,容易在有较多NIO操作时候容量不足,被忽略,不断的抛出溢出错误


5.2.4 外部命令导致系统缓慢
持续的进程创建导致该类现象


5.2.5 服务器JVM崩溃
累积的web服务没有调用完成,导致在等待的线程和Socket连接越来越多,最终在超过虚拟机的承受能力后使得虚拟机进程崩溃。解决方法:通知OA门户方修复无法使用的集成接口,并将异步调用改为生产者/消费者模式的消息队列实现后,系统恢复正常。


5.2.6  不恰当数据结构导致内存占用过大
每10分钟就加载一个约80MB的数据文件到内存进行数据分析,产生100万个HashMap Entry,在这段时间里MinorGC就会造成超过500毫秒的停顿。


5.2.7 由windows虚拟内存导致的长时间停顿
加入参数 -Dsun.awt.keepWorkingSetOnMinimize=true 来解决,这个参数在很多AWT的程序上都有应用
用于保证程序在恢复最小化时能够立即响应。


5.3 Eclipse运行速度调优
5.3.2 升级JDK带来的性能变化
永久代参数调整(即堆里的方法区)


5.3.3 编译时间和类加载时间的优化
5.3.4 调整内存设置控制垃圾收集频率
把新生代容量提升到128MB,避免频繁GC
把Java堆、永久代的容量分别固定为512MB和96MB,避免内存扩展
改动在Eclipse.ini文件中
5.3.5 选择收集器降低延迟
在ini文件中加入参数指定收集器
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
(ParNew收集器是使用CMS收集器后的默认新生代收集器写上仅是为了配置更加清晰)




第三部分
第6章 类文件结构
类文件处于方法区,在new对象的时候使用,如果没有就加载一个
6.3 class文件的结构
class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列,这种伪结构只有两种数据类型:无符号数和表


6.3.1 魔数与class文件版本
魔数+版本号, 采用16进制进行记录
JAVA语言是 咖啡宝贝  0xCAFEBABE 


6.3.2 常量池
字面量:表示常见的final等修饰的字面值
符号引用:属于编译部分,包括三类:
类和接口的全限定名Full Qualified Name
字段的名称和描述符Descriptor
方法的名称和描述符


第7章 虚拟机加载
7.1 概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制


class文件不特指存在于某个具体磁盘的文件,而应当是一串二进制的字节流。不论以何种形式存在。


7.2 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、和卸载7个阶段,其中验证、准备、解析3个部分统称为Linking连接
有且仅有以下5种情况会触发被动引用:
1) 遇见new、getstatic、putstatic 或invokestatic 这4条指令时,如果类没有进行初始化,则需要先出发至初始化。常见场景为,使用new关键字实例化对象的时候、读取和设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2) reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要先出发其初始化
3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先出发其父类的初始化
4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5) 当使用JDK1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对所对应的类没有进行过初始化,则需要先触发其初始化。


被动使用类字段不触发初始化的情况
1) 子类引用父类静态变量,只触发父类初始化,子类不初始化
2) 数组类型不触发初始化
3) 常量在编译阶段会存入调用类的常量池中,本质上本质上并没有引用到定义常量的类,因此不会触发定义常量的类的初始化


接口也会初始化,不用static静态语句块,会为接口生成类构造器,用于初始化接口中所定义的成员变量,不要求父类全部初始化,只有需要用到父类时才初始化
interface extends interface


7.3 类加载的过程
7.3.1 加载
1) 通过一个类的全限定名来获取定义此类的二进制字节流
2) 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
3) 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口


开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)


数组类有所不同,
如果数组的组建类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载
该组建类型的类加载器的类名称空间上标识
如果数组的组建类型不是引用类型,Java虚拟机将会把数组C标记为与引导类加载器相关联
数组列的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public


7.3.2 验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
包含四方面:
(1) 文件格式验证:java文件
(2) 元数据验证:是否有父类,是否继承了不允许被继承的类(final类),这个类不是抽象类,是否实现了父类或接口之中要求实现的所有方法,类中的字段、方法是否与父类产生矛盾
(3) 字节码验证:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况,在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中;保证指令不会跳转到方法体以外的字节码指令上;保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型、这是安全的。但是把父类对象赋值给子类数据类型,甚至把对象赋值给它毫无继承关系、完全不相干的数据类型,则是危险和不合法的。
(4) 符号引用验证:字符描述的全限定名是否能找到对应的类、在类中存在符合方法的字段描述以及简单名称所描述的方法和字段,符合引用中的类、字段、方法的访问性是否可被当前类访问


7.3.3 准备
将static修饰的变量初始化为0值(初始化阶段时才会赋值为字面量),如果有final就准备时初始化为字面量


7.3.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关。引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接收的符号引用必须都是一致的。因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中


直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标已经在内存中存在。


解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行


7.3.5  初始化
执行类构造器方法,给变量赋上成员指定的初始化变量和其他资源


7.4 类加载器
7.4.1 类加载器
判断类相等,判断类加载器相等即可


7.4.2 双亲委派模型
启动类加载器
扩展类加载器
应用程序类加载器
自定义类加载器
自定类加载器


第8章 虚拟机字节码执行引擎


8.2 运行时栈帧结构
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
每个栈帧都包括了局部变量表,操作数栈、动态连接、方法返回地址和一些额外的附加信息


8.2.1 局部变量表
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的slot是否为院子操作,都不会引起数据安全问题


8.2.2 操作数栈
后入先出,方法中的计算,就在此进行,不执行时是空的


8.2.3 动态连接
每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)


8.2.4 方法返回地址
方法返回只有两种:正常return退出,throw异常退出;出栈,方法返回时候可能需要在栈帧中保存一些信息,用来恢复它的上层方法的执行状态。
恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整pc计数器的值以指向方法调用指令后面的一条指令


8.2.5 附加信息
比如增加部分与调试相关的信息


8.3 方法调用
不同于方法执行,唯一任务是确定被调用方法的版本


8.3.1 解析
5条指令调方法
invokestatic 调用静态方法
invokespecial 调用实例构造器方法、私有方法和父类方法
invokevirtual 调用所有的虚方法
invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象
invokedynamic 先在运行时动态解析出调用观点限定符所引用的方法,然后再执行该犯法,在此之前4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic


只要被invokestatic 和invokespecial调用的方法都可以在解析阶段确认
符合条件的有静态方法、私有方法、实例构造器、父类方法等4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。


8.3.2 分派
三特征:封装、继承、多态
比如重载和重写在java虚拟机之中是如何实现的。
重载是根据数据类型和数据数量来确定的
1) 静态分派
重载的时候都会调用静态父类的方法
2) 动态分派
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派
重写方法即是
3) 单分派与多分派
4) 虚拟机动态分派的实现
虚方法表


8.3.3 动态语言支持


8.4 基于栈的字节码解释执行引擎
8.4.1 解释执行


































































































你可能感兴趣的:(书籍及摘要,JVM调优)