之前面试的时候每次都避免不了虚拟机之类的问题,似乎不问下GC之类的问题、或者类加载之类的问题,java基础等于没过关。
恩,不得不说确实很重要。
国庆几天,花了4天时间把周志明的《深入理解Java虚拟机》第二版,剩余的10、11、12章、3、4、5章读完了,6、7、8、9章在9月份的时候看完了,这次阅读这本书,称的上是认认真真的阅读了,Class类结构那章是17年12月份开始看的,断断续续的18年1月份才看完那章,并认认真真的做了个例子,写了个文章。之后过完年就去看设计模式了,3、4、5月份主要把设计模式看了,也是那本经典的《Head First设计模式》,设计模式看了3个月,每个月的周末,后来6、7月份孩子们过来,耽误了俩月,之后8月份的时候,把剩余的两章终结了。然后9月份加上国庆这几天,把JVM的这本啃下来了。想想其实也就花了1个多月的时间,周末用起来,加上十一这几天假期,很快也就看完了。想想这本书我买的时间大概已经是3年前了。荀子说:学不可以已。我想我没有大的发展,跟没有持续的自律学习关系很大。但是今年的学习也让我认识到,学习、持续的学习,进步还是很大的。
啰里啰嗦的这么多,下边进入正题吧。
其实这次阅读的时候从第6章开始看,感觉挺好的,因为2、3章是讲内存部分的,内存划分、GC处理,这两部分,刚开始就看有些早了,对于阅读来说。而从第6章Class类结构开始,其实就好接受很多了,慢慢的从类结构开始你会解除越来越多的专业术语,这样 你再回过头来看前边5章,关于内存、GC部分的时候就会游刃有余很多,同时正好也复习了后边几章关于类结构、类加载、虚拟机执行子系统、早起优化、晚期优化之类的内容。
这次主要的深入理解觉得有一下几个方面:
1:class文件到底是什么样子?
那么就牵扯Class类结构了。Class类文件结构,详细的理解下常量池,其结构也不过以下:
Class文件是由一组8位字节为基础单位的二进制流。
魔数、次版本号、主版本号这些都一目了然,接下来常量池就是比较重要的了。常量池是接下来后续的各项需要用到的最频繁的地方了。常量池里14钟常量项的结构
Constant Type | Value |
---|---|
CONSTANT_Class |
7 |
CONSTANT_Fieldref |
9 |
CONSTANT_Methodref |
10 |
CONSTANT_InterfaceMethodref |
11 |
CONSTANT_String |
8 |
CONSTANT_Integer |
3 |
CONSTANT_Float |
4 |
CONSTANT_Long |
5 |
CONSTANT_Double |
6 |
CONSTANT_NameAndType |
12 |
CONSTANT_Utf8 |
1 |
CONSTANT_MethodHandle |
15 |
CONSTANT_MethodType |
16 |
CONSTANT_InvokeDynamic |
18 |
每一项第一个u1类型的tag对应上述表格中的数字,然后对应找到相应的结构就能知道里边存储的内容具体是什么了。
访问标志:是当前类的访问标志,即我们常用来形容类的关键字、Public、Final、Super、注解、接口Interface等等
类索引、接口索引、父类索引,这些去常量池中找到相应的String类型的全限定名即可。当然父类只有一个,接口可以多个,查看结构就知道了。
接下来就是字段表、方法表、属性表集合的内容了。
字段表根据结构,每一个字段包括:访问标志、名称索引、描述索引、属性表。访问标志跟之前的类似就是字段的诸如:final、private、public、protected、volatile、transient、enum、synthetic(字段是否由编译器自动生成,有些字段是编译器自动加上的,比如内部类为了保持对外部类的访问行,会自动添加指向外不来的实例的字段)等。名称索引指向的是简单名称也就是名称本身;描述索引就相对麻烦些用来描述字段类型的信息(数组、还是基础类型或者其他类的类型等)。
方法表结构的前几项与字段表基本没有差异,访问标志是方法的访问标志跟字段稍有不同,最核心的其实是方法表中的属性表部分,方法的名称、描述符都有了,那么代码去哪儿了,其实就是在方法表的属性信息中。
自然的就引出了属性表:
Table 4.6. Predefined class
file attributes
Attribute | Section | Java SE | class file |
---|---|---|---|
ConstantValue |
§4.7.2 | 1.0.2 | 45.3 |
Code |
§4.7.3 | 1.0.2 | 45.3 |
StackMapTable |
§4.7.4 | 6 | 50.0 |
Exceptions |
§4.7.5 | 1.0.2 | 45.3 |
InnerClasses |
§4.7.6 | 1.1 | 45.3 |
EnclosingMethod |
§4.7.7 | 5.0 | 49.0 |
Synthetic |
§4.7.8 | 1.1 | 45.3 |
Signature |
§4.7.9 | 5.0 | 49.0 |
SourceFile |
§4.7.10 | 1.0.2 | 45.3 |
SourceDebugExtension |
§4.7.11 | 5.0 | 49.0 |
LineNumberTable |
§4.7.12 | 1.0.2 | 45.3 |
LocalVariableTable |
§4.7.13 | 1.0.2 | 45.3 |
LocalVariableTypeTable |
§4.7.14 | 5.0 | 49.0 |
Deprecated |
§4.7.15 | 1.1 | 45.3 |
RuntimeVisibleAnnotations |
§4.7.16 | 5.0 | 49.0 |
RuntimeInvisibleAnnotations |
§4.7.17 | 5.0 | 49.0 |
RuntimeVisibleParameterAnnotations |
§4.7.18 | 5.0 | 49.0 |
RuntimeInvisibleParameterAnnotations |
§4.7.19 | 5.0 | 49.0 |
AnnotationDefault |
§4.7.20 | 5.0 | 49.0 |
BootstrapMethods |
§4.7.21 | 7 |
51.0 |
属性表的集中类型如上图
其中我们可能最看重的当然就是Code,用书上的话说,Code属性是java Class文件中最重要的一个属性,如果把Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。哈哈,这样说是不是够重视了。
The Code
attribute has the following format:
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
看到结构的时候,就明白了,在后面看早期优化的时候经常提到,类加载后方法的操作数栈的最大深度就已经确定了,这里看到类结构应该印证了这句话。javac编译后最大的深度就已经确定了。code_length和code[code_length]代表了java方法的代码部分,字节码长度和一系列字节流。一个字节码对应着一个字节码指令。一个八进制字节的表示:0x00到0xFF一共有256个字节可以表示,也就是说有256个字节码指令可以定义,目前定义了有约200条指令。书中的附录B可以查看详细。
基本类结构的核心就是这些,关于属性表的内容还可以花时间再详细学习,并完全分析几个常见的属性比如Exception的属性结构。
有了这一部分的了解,Java的Class文件结构就基本清晰的认识了。
2:关于类加载的内容
类加载采用双亲委派模型,我们已经聊的够多了,看的够多了
这次新学习到的东西Java的SPI机制,其实就是使用线程上下文类加载器实现的,还有spring的一些技术实现,比如tomcat中spring的jar包,放在外部lib中的时候,如何加载不同的项目中的类呢。这里使用的仍然是线程上下文类加载器。
其他的内容就是关于类加载的过程的各个说明和分析: 加载 -> 验证 -> 准备 -> 解析 -> 初始化。其中解析就是符号引用->直接引用的过程。
3:虚拟机执行引擎:局部变量表、操作栈、动态链接、返回地址 组成 虚拟机执行栈帧
动态链接的部分就是后续讲解的方法调用,方法调用包括:解析调用、静态分派、动态分派
解析调用:编译器可知、运行期不变 符合这两条的其实也就是静态方法、私有方法、实例构造器、父类方法4类。对应的调用指令:invokestatic 和 invokespecial,它们调用的方法都可以在解析阶段确定唯一的调用版本。
静态分派:其实就是java中方法重载特性的提供。方法执行的时候,调用者Receiver已经确定,具体执行的方法是根据参数的类型在编译器确定具体执行的方法的符号引用,然后将符号引用传入到invokevirtual的指令参数中。也需要注意的是,这里编译器也只能选择一个相对准确的参数。可参考书中实例。
动态分派:动态分派最典型的例子就是java的重写了。方法重写允许java代码在运行时再决定具体执行的方法接收者,这里由于接受者不确定(即:编译器不能确定方法的具体接受者),那么就要到运行期,根据invokevirtual的具体指令来了:
1)查找操作数栈顶的第一个元素所指向的对象实际类型,记做C
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
3)否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和验证过程
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
虚方法表:虚拟机动态分派的实现
4:早期编译优化+晚期编译优化
首先来说早期编译优化,这里针对的主要是javac编译的优化过程。
javac的编译过程包括:
1)词法、语法分析,生成语法树、插入符号表
2)语义分析,生成字节码
这里边还有一个插入式注解处理器API,利用这个api可以对语法树上的信息进行修改,每次修改之后都需要重新进行词法、语法分析、插入符号表这些操作重新进行一遍,重新生成语法树
这些过程里边,比如语义分析里就有常量折叠之类的优化、标注检查、数据及控制流分析。
然后就是语法糖的内容,java泛型是类型擦除,也就是编译阶段解语法糖到普通的类型来实现的泛型,并不是真实的C++的类型膨胀的泛型。
然后就是自动封箱、拆箱、遍历循环,条件编译这些,最后还有一个插入式注解处理器修改语法树的api的使用。
晚期优化:主要是在解释执行的基础上,Hotspot虚拟机又提供了两个即时编译器,C1(client编译器)和C2(server编译器),解释器和编译器目前默认都是混合模式使用,当然也可以使用参数
“-Xint”强制使用“解释模式”(Interpreted Mode)或者 “-Xcomp”强制使用“编译模式”(Compiled Mode)。
那么混合模式中,何时进行JIT即时编译。依据有两个:
1)被多次调用的方法
2)被多次执行的循环体
分别对应有两个计数器,方法调用计数器、回边计数器。两种情况对应的第一种是标准的JIT即时编译,第二种编译发生在方法执行中,因此又叫做栈上编译。
然后晚期优化具体的优化技术有很多,重点介绍了:公共子表达式消除、数组边界检查消除、方法内联、逃逸分析
重点讲下内联,内联优化是众多其他优化手段的基础优化,但是在java重却并不好实现,因为java中无处不在的方法重载,让内联很难施展,为了解决这个问题,jvm提供了一系列的方法,首先是引入了“类型继承关系分析”(Class Hierarchy Analysis, CHA)的技术,是一种基于整个应用程序的类型分析技术,来确定目前加载的类中,某个接口是否有多余一种的实现,某个类是否存在子类、子类是否为抽象类等信息。
编译器进行内联时,如果非虚方法直接内联就可以,如果是虚方法,那么就向CHA查询此方法是否有多个目标提供选择,如果只有一个版本就进行内联,这种方法属于激进优化,需要一个“逃生门”,称为守护内联。如果后续一直没有加载到令这个方法接受者发生变化的类,那么就继续使用内联优化后的代码,如果加载到了,那么就重新会到解释执行。
5:内存模型、线程
主要是volatile类型变量的原则讲述、原理讲述,以及happen-befores原则的讲述
还有之前少接触到的java线程的实现
6:线程安全和锁优化
经常提到的线程安全的方法:互斥同步、非阻塞同步、无同步方案(线程本地存储ThreadLocal之类)
锁优化技术:自旋锁、自适应锁、锁消除、锁粗化、轻量级锁、偏向锁。
7:GC相关
相关的GC的算法:标注-清楚算法、复制算法、标注-整理算法、分代收集算法
根据可达性分析来确定对象是否可以进行清除,这里引申除了引用的详细分析:强引用、弱引用、软引用、虚引用。
具体的GC收集器,讲解了常见的垃圾收集器,目前应用比较多的是CMS收集器、G1收集器,还要Client模式下默认的Serial收集器。
内存分配与回收策略
1)对象优先分配在Eden区
2)大对象直接进入老年代
3)长期存活对象将进入老年代
4)动态对象年龄判断
5)空间分配担保
8:相关的虚拟机分析命令和可视化工具
jps、jstat、jinfo、jmap、jhat、jstack。
JConsole、VisualIVM
至此,这本书核心的点基本就梳理完了,这就是JVM深入理解这本书,看完确实对JVM的方方面面有了相对全面的理解,对于很多语言层面的诸如:多态、GC、线程、锁相关的知识有了更深入的理解。因此不管怎么说,如果你是java程序员,并且想往上走,那么至少5年经验之前,这本书就该透彻的通读一遍了,以后在高级开发的道路上继续温习、不断深入其中各项技术和时间,相信是高级开发的必由之路,感恩2018年的自己,认认真真的读完这本书,虽然已经是8年的程序老兵,但惭愧现在才认真的通读了这本书,很多技术细节,现在看来已经学习了不少了,比如后边的关于线程和锁的两章,平时多关注锁、线程并发的部分,因此看这两章的时候相对轻松很多,因为大部分知识都已经学习过了,算是温故知新。另外比如3、4、5章的GC部分的东西,这么多年的编程从业经验,多少也接触些这方面的东西,这里全面的学习一下,有助于未来遇到类似问题时的深入分析和理解。不会得过且过不知其所以然的分析了。比较受用的主要是第6、7、8、9章吧,也是花费时间最多的,类结构的认识让自己对Class文件这个java最基础的东西,有了深入的理解,也通过第7章,虚拟机执行子系统,对JVM的解释执行、帧栈执行、寄存器执行有了深入的理解。关于早期优化、晚期优化则对jvm的执行有了更加强化和深入的理解。综上,是本书的知识点梳理和读后的敢想。
这本书想来会是今年和明年自己进阶路上的必备精品,揉碎了印在脑子里,然后忘掉,最后再回来品味,相信那时的自己会是自己钦佩的高手。在路上,别灰心,加油兄弟。
2018/10/5 22:00起笔,2018/10/6 10:56:52结笔, 写于昆山住处。