JVM总结

《深入理解Java虚拟机》这本书看了三五遍,笔记也写了几十页,今天写一篇博客作为最后的总结。

第二章 Java内存区域与内存

程序计数器:代表当前线程所执行字节码的行号指示器
Java虚拟机栈:每个方法对应一个栈帧,用于存储局部变量表,操作数栈,动态链接(指向运行时常量池中改栈帧所属方法的引用),方法出口(调用者的PC计数器的值)。long和double占两个slot,其余占一个slot。会抛出StackOverFlowError异常,OutOfMemoryError异常。
本地方法栈:为Native方法服务。
方法区:存储虚拟机加载的类信息,常量,静态变量,即时编译器编译的代码。内存回收主要是常量池的回收和对类型的卸载。会抛出OutOfMemoryError异常。
运行时常量池:方法区的一部分,常量池在类加载后进入方法区的运行时常量池中存放。
Java堆:存放对象实例,会抛出OutOfMemoryError异常。
隔离性:方法区和Java堆线程共享,Java虚拟机栈,本地方法栈,程序计数器线程隔离。
对象的创建:1)检查类是否加载,2)分配内存(空闲列表,指针碰撞),3)将内存空间初始化为零值,不包括对象头,4)对对象头设置(是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分代年龄),5)执行init方法。
对象的内存布局:1)对象头,第一部分,存储对象自身的运行时数据(哈希码,GC分代年龄),第二部分,类型指针,即对象指向它的类元数据的指针,如果是数组,需要记录数组长度,2)实例数据,在程序代码中定义的各种类型的字段内容,分配策略:相同宽度的字段总是在一起,在满足这个前提条件的情况下,如果CompactFields参数为true(默认值),子类变量可能会插入到父类变量的空隙之中,3)对齐填充,起始地址必须是8字节的整数倍,所以需要对齐填充。
对象的访问定位:1)使用句柄,2)直接指针,Java使用直接指针。使用句柄的好处:在对象被移动时,只会改变句柄中的实例数据指针,reference本身不需要修改,直接指针的好处:节省一次指针定位的开销。
HotSpot虚拟机对象头的Mark Word

存储内容标志位状态
指向锁记录的指针00轻量级锁定
对象哈希码,对象分代年龄01未锁定
偏向线程ID,偏向时间戳,对象分代年龄01可偏向
指向重量级锁的指针10重量级锁定
空,不需要记录信息11GC标记

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

引用计数算法:很难解决循环引用的问题
可达性分析算法:将GC Roots作为起始点,向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,即GC Roots到这个对象不可达时,说明这个对象是不可用的。
可作为GC Roots的对象:虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象。
引用:强引用:只要强引用还在,垃圾回收器永远不会回收掉被引用的对象,软引用:内存溢出之前,对软引用关联的对象进行回收,如果还没有足够的内存,抛出内存溢出异常,弱引用:无论当前内存是否足够,都会回收掉只被弱引用关联的对象,虚引用:在这个对象被回收时收到一个系统通知。
回收方法区:1)该类的所有实例都已经被回收,2)加载该类的ClassLoader已经被回收,3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法:1)标记清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,不足:1,标记和清除的效率都不高,2,产生内存碎片。2)复制算法,将内存分为一块Eden,两块Survivor,比例8:1,每次使用Eden和其中一块Survivor,回收时,将Eden和Survivor中存活着的对象一次性地复制到另一块Survivor上,最后清理掉刚才的Eden和Survivor空间。当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。3)标记整理算法:和标记清除算法的区别是,让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。4)分代收集算法,新生代采用复制算法,老年代采用标记清除或者标记整理算法。
安全点:只有到达安全点时,才能停顿下来开始GC,安全点的选定,以程序“是否具有让程序长时间执行的特征”为标准进行选定,这样的指令有:方法调用,循环跳转,异常跳转等。
安全区域:在一段代码片段中,引用关系不会发生变化。
Serial收集器:单线程,采用复制算法,Client模式下默认的新生代收集器,简单高效。
ParNew收集器:Serial的多线程版本。Server模式下首选的新生代收集器,单CPU时,不会比Serial更好。
Parallel Scavenge收集器:注重吞吐量,-XX:+UseAdaptiveSizePolicy:自适应的调节策略
Serial Old收集器:单线程,标记整理算法,可作为CMS的后备预案,在发生Concurrent Mode Failure时使用
Parallel Old收集器:多线程,标记整理算法,是Parallel Scavenge的老年代版本。
CMS收集器:以最短回收停顿时间为目标,标记清除算法。分为4个步骤:1,初始标记,2,并发标记,3,重新标记,4,并发清除。初始标记:标记一下GCRoots能直接关联到的对象,需要Stop the world,并发标记:进行GCRoots Tracing,重新标记:修正标记产生变动的那一部分对象的标记记录,需要Stop the world。优点:并发收集,低停顿。缺点:1,对CPU资源非常敏感,当CPU少时,对用户程序影响大,2,无法处理浮动垃圾,可能出现Concurrent Mode Failure(CMS预留的内存无法满足程序需要),会临时启用Serial Old来收集,3,产生内存碎片,-XX:UseCMSCompactAtFullCollection,默认开启,-XX:CMSFullGCsBeforeCompaction,默认为0,每次FullGC都进行碎片整理。
G1收集器:将整个Java堆划分为多个大小相等的区域,新生代老年代不再是物理隔离的,都是一部分Region(不需要连续)的集合。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的回收时间,优先回收价值最大的Region,这就是Garbage First名称的由来。每个Region都有一个与之对应的Remembered Set,如果A引用了B,及A->B,且AB属于不同的Region,那么在B的Remebered Set中记录A。步骤:1)初始标记,2)并发标记,3)重新标记,4)筛选回收,在筛选回收阶段,首先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。从整体来看,基于标记整理,从局部来看,基于复制。
垃圾收集器参数:

参数描述
SurvivorRatioEden与Sorvivor的比值,默认为8
PretenureSizeThreshold直接晋升到老年代的对象大小
MaxTenuringThreshold晋升到老年代的对象年龄,默认15
GCTimeRatioGC时间占总时间的比率,默认99,即允许1%的GC时间,针对Parallel Scavenge
MaxGCPauseMillis设置GC的最大停顿时间,针对Parallel Scavenge
UseAdaptiveSizePolicy动态调整Java堆中各个区域的大小以及进入老年代的年龄,针对Parallel Scavenge
CMSIntiatingOccupancyFraction设置CMS收集器在老年代空间被使用多少后触发垃圾收集,92%,针对CMS
UseCMSCompactAtFullCollection设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理,针对CMS
CMSFullGCsBeforeCompaction设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理,针对CMS

内存分配与回收策略
对象优先在Eden分配:大多数情况下,对象在Eden分配,当Eden区没有足够空间时,虚拟机将发起一次MinorGC。
大对象直接进入老年代:-XX:PretenureSizeThreshold,避免在Eden区及两个Survivor区之间发生大量的内存复制。
长期存活的对象进入老年代:-XX:MaxTenuringThreshold
动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行MinorGC,否则进行FullGC。
垃圾收集器图
在这里插入图片描述

第四章 虚拟机性能监控与故障处理工具

jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具
jinfo:Java配置信息工具
jmap:Java内存映像工具
jhat:虚拟机堆转储快照分析工具
jstack:Java堆栈跟踪工具
HSDIS:JIT生成代码反汇编
JConsole:Java监视与管理控制平台
VisualVM:多合一故障处理工具

第六章 类文件结构

常量池容量计数从1开始而不是从0开始:在异常表中catch_type为0表示所有异常,在内部类中,匿名内部类的inner_name_index为0。
常量池重要存放两大类常量:字面量和符号引用。
字面量:比较接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值等。
符号引用包括:1)类和接口的全限定名,2)字段的名称和描述符,3)方法的名称和描述符。
常用常量:CONSTANT_Utf8_info,CONSTANT_Class_info,CONSTANT_String_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_NameAndType_info。
常用属性

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
Exceptions方法表方法抛出的异常
InnerClasses类文件内部类列表
Signature类,方法表,字段表记录泛型中的相关信息
SourceFile类文件记录源文件名称

异常表的结构

类型名称数量
u2start_pc1
u2end_pc1
u2handler_pc1
u2catch_type1

表示:如果当字节码在第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type的异常,则转到第handler_pc行进行处理,catch_type为0,代表任意异常情况都需要转向到第handler_pc行进行处理。
描述符:J表示long,Z表示boolean,L表示对象类型,如LJava/lang/Object,用描述符描述方法时,先参数列表,后返回值,如()V,()Ljava/lang/String
字段表集合:字段表集合中不会列出从超类或者父接口中继承而来的字段。
方法表集合:如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。
Class文件格式

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_poll_count1
cp_infoconstant_pollconstant_poll_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_count

字节码指令
加载和存储指令:load:将一个局部变量加载到操作数栈,store:将一个数值从操作数栈存储到局部变量表。
运算指令:对两个操作数栈上的值进行某种特定运算,并把结果重新存到操作数栈顶,add,sub,mul,div…只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)中当出现除数为0时会导致虚拟机抛出ArithmeticException,其余任何整型运算场景都不应该抛出运行时异常。在处理浮点数运算时,不会抛出任何运行时异常。
类型转换指令,窄化类型转换(例如i2b),可能会发生上限溢出,下线溢出和精度丢失等情况,窄化类型转换指令永远不可能导致虚拟机抛出运行时异常。int到long,float,double自动转换,long到float,double自动转换,float到double自动转换。
对象创建与访问指令:创建实例:new,创建数组newarray,anewarray,multianewarray,访问类字段,实例字段:getstatic,putstatic,getfield,putfield。把数组元素加载到操作数栈:baload,caload,saload,faload,daload,aaload,把操作数栈的值存储到数组元素:bastore,castore,sastore,iastore,fastore,dastore,aastore 取数组长度的指令:arraylength 检查类实例类型的指令:instanceof checkcast
操作数栈管理指令:出栈pop,复制dup,交换swap
控制转移指令:条件分支if 复合条件分支 tableswitch,lookupswitch
方法调用和返回指令:invokevirtual,invokeinterface,invokespecial(初始化方法,私有方法,父类方法),invokestatic ireturn,lreturn,freturn,dreturn,areturn,return。
异常处理指令:athrow
同步指令:monitorentor,monitorexit 每条monitorentor指令都必须执行其对应的monitorexit指令,会自动产生一个异常处理器,可处理所有异常,用来执行monitorexit指令。

第七章 虚拟机类加载机制

类的生命周期:加载,验证,准备,解析,初始化,使用,卸载。验证,准备,解析这3个部分统称为连接。
必须立即对类进行初始化的情况:1)new,getstatic,putstatic,invokestatic,2)反射调用 3)初始化一个类,父类没有初始化,要先初始化其父类 4)先初始化主类(执行main()方法的那个类)
被动应用的3个例子:1)通过子类引用父类的静态字段,不会导致子类初始化 2)通过数组定义来引用类,不会触发此类的初始化 3)使用常量,不会触发定义常量的类的初始化。
加载:1)通过一个类的全限定名来获取定义此类的二进制字节流 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,1)文件格式验证 2)元数据验证,3)字节码验证,4)符号引用验证
准备:为类变量分配内存并设置类变量初始值(0值),类变量在方法区中进行分配。如果类字段是final的,准备阶段就会赋为指定的值。
解析:将常量池内的符号引用替换为直接引用,符号引用:以一组符号来描述所引用的目标,直接引用:直接指向目标的指针,相对偏移量,或是一个能间接定位到目标的句柄。1)类和接口的解析 2)字段解析 3)类方法解析 4)接口方法解析
初始化:执行类构造器clinit方法的过程,收集类变量的赋值和静态语句块中的语句合并产生,顺序由源文件中的顺序决定。 虚拟机保证父类的clinit方法在子类执行之前执行,不需要显示地调用父类构造器。执行接口的clinit方法不需要先执行父接口的clinit方法,接口的实现类在初始化时也一样不会执行父接口的clinit方法。
类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
双亲委派模型
在这里插入图片描述
类加载器之间的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器,一般使用组合关系来复用父加载器的代码。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,依次向上传递,只有当父加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。保证了每个类在程序的各种类加载器环境中都是同一个类。
启动类加载器:使用C++语言实现,是虚拟机自身的一部分,负责加载<JAVA_HOME>\lib目录中的类库。
扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中的类库。
应用程序类加载器:负责加载用户类路径上所指定的类库。

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

栈帧:局部变量表,操作数栈,动态连接:每个栈帧中都包含一个指向运行时常量池中该栈帧所属方法的引用,为了支持动态连接。方法返回地址:调用者的PC计数器的值。
解析:非虚方法:invokestatic,invokespecial(实例构造器,私有方法,父类方法)和final方法。在类加载的时候就会把符号引用解析为该方法的直接引用。
静态分派:在编译期根据静态类型来定位方法执行版本的过程称为静态分派。静态分派的典型应用是方法重载。
动态分派:在运行期根据实际类型来确定方法执行版本的过程称为动态分派。动态分派的典型应用是方法重写。invokevirtual的运行时解析过程:1)找到操作数栈顶的第一个元素所指向的对象的实际类型…
宗量:方法的接收者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。静态多分派(静态类型,方法参数),动态单分派(方法接收者的实际类型)。
动态分派的实现:为类在方法区中建立一个虚方法表,虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口一致。为了实现上的方便,具有相同签名的方法,在父类,子类的虚方法表中都应当具有一样的索引号。方法表一般在类加载的连接阶段进行初始化,准备了类变量的初始值后,虚拟机会把该类的方法表也初始化完毕。

第十章 早期(编译期)优化

泛型与类型擦除:对于运行期的Java语言来说,ArrayList与ArrayList是同一个类,泛型技术就是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,称为伪泛型。元数据中保留了泛型信息。Signature属性中包含了参数化类型的信息,擦除仅仅是对方法的code属性中的字节码进行擦除。泛型擦除的例子:
擦除前:

	public static void main(String[] args) {
		Map<String, String> map = new HashMap<>();
		map.put("hello", "你好");
		map.put("how are you?", "吃了没?");
		System.out.println(map.get("hello"));
		System.out.println(map.get("how are you?"));
	}

擦除后:

	public static void main(String[] args) {
		Map map = new HashMap();
		map.put("hello", "你好");
		map.put("how are you?", "吃了没?");
		System.out.println((String) map.get("hello"));
		System.out.println((String) map.get("how are you?"));

	}

自动装箱,拆箱,与遍历循环的例子
前:

	public static void main(String[] args) {
		List<Integer> list = Arrays.asList(1, 2, 3, 4);
		int sum = 0;
		for (int i : list) {
			sum += i;
		}
		System.out.println(sum);
	}

后:

	public static void main(String[] args) {
		List list = Arrays.asList(
				new Integer[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4) });
		int sum = 0;
		for (Iterator localIterator = list.iterator(); localIterator.hasNext();) {
			int i = ((Integer) localIterator.next()).intValue();
			sum += i;
		}
		System.out.println(sum);
	}

第十一章 晚期(运行期)优化

数组边界检查消除:尽可能把运行期检查提到编译期完成。
隐式异常处理(利用try,catch):空指针,除数为零。
方法内联:除了消除方法的调用成本之外,它更重要的意义是为其他优化手段建立良好的基础。
类型继承关系分析(Class Hierarchy Analysis,CHA):在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类,子类是否为抽象类等信息。
如果是非虚方法,直接内联。如果是虚方法,向CHA查询是否有多个目标版本,如果只有一个版本,可以内联,属于激进优化,需要预留一个逃生门,称为守护内联。如果加载了导致继承关系发生变化的类,需要抛弃已经编译的代码,退回到解释状态执行,或者重新进行编译。
如果有多个目标版本,使用内联缓存来完成方法内联,原理:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,每次调用比较接收者版本,如果一致,使用内联,否则,取消内联,查找虚方法表进行分派。
逃逸分析:并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。
如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化。
栈上分配:对象随着方法的结束而自动销毁,垃圾收集系统的压力将会减小很多。
同步消除:对这个变量实施的同步措施可以消除掉。
标量替换:不创建对象,改为创建它的若干个被这个方法使用到的成员变量来代替。可以让对象的成员变量在栈上分配和读写,还可以为后续进一步的优化手段创建条件。
逃逸分析不成熟,原因:不能保证逃逸分析的性能收益必定高于它的消耗。HotSpot不支持栈上分配,逃逸分析耗时较高。

第十二章 Java内存模型与线程

“先行发生”原则,如果操作A先行发生于操作B,就是说,在发生操作B之前,操作A产生的影响能被操作B观察到。时间先后顺序与先行发生原则之间基本没有太大的关系。
Java内存模型天然的先行发生关系:
程序次序规则,管程锁定规则,volatile变量规则,线程启动规则,线程终止规则,线程中断规则,对象终结规则,传递性。
不能太依赖线程优先级:1)Java线程的多个优先级可能对应系统的一个优先级,2)优先级可能会被系统改变。

第十三章 线程安全与锁优化

锁优化
1)自旋锁与自适应自旋
2)锁消除:编译期在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。
3)锁粗化:如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
4)轻量级锁
在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象的Mark Word的拷贝,然后使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,对象MarkWord的锁标志位变为00,解锁,使用CAS操作将线程中的Mark Word替换回来。
5)偏向锁
如果可偏向,当第一次获取时,使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word中,如果CAS成功,持有偏向锁的线程每次进入这个锁的同步块时,不再进行任何同步操作,当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。
线程安全问题:原子性,可见性,有序性。

参考

《深入理解Java虚拟机》周志明著

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值