第一部分 走进Java
在.class文件中表示是一样的
学习一门语言,不仅仅是要知道它的语法,它的运行机制,要更深层次的去剖析其设计思想,通过比较该语言和其他语言的相同与不同,能更好的掌握每种语言的特色
第二部分 自动内存管理机制
第一章 Java内存区域与内存溢出异常
一、Java运行时区域
虚拟机栈、本地方法栈、程序计数器(线程私有)
方法区、堆(线程公有)
虚拟机栈:存放局部变量表、操作栈等信息
本地方法栈:本地方法执行时候的栈
程序计数器:生成指令执行的地址
方法区:存放类信息
堆:存放对象实例
二、对象访问
两种方式:
1.句柄池
建立中间层,对对象的访问改成对句柄的访问
优点:灵活,对象移动后只要改变句柄的指向,所有指向该句柄的引用不用改变
缺点:类似于间接寻址,慢
2.直接指针
直接访问对象地址
优点:直接寻址,快
缺点:对象的引用会随着对象物理地址的移动而改变
第二章 垃圾收集器与内存分配策略
一、哪些垃圾需要进行回收?
主要是孤立的对象就会被看成垃圾,所以问题就归结于怎样判断一个对象有没有被孤立
1.引用计数法
如果一个对象没用引用,则肯定是被孤立了,就判定应该回收
缺点:无法处理循环引用问题
2.根搜索算法
指定一些(GC Roots),从这些根来向下遍历,最终哪些被孤立,则视为垃圾
3.引用类型
如果把对象简单的定义为有引用和没引用两类,太死板,Java中还定义类一些介于这两者之间的引用,当内存特别紧张时候,就回收这一类引用所指向的对象
二、如何进行垃圾回收
主要是探讨采用不同的方法对内存块的清除,会有不同的特点
1.标记--清除 和 标记--整理
主要都是直接释放内存,后者是先对剩余的对象进行整理,然后再统一清除,这样清除后内存区域是连续的,有利于下次分配
缺点:如果被清除的对象比较多,则工作量会比较大
2.复制算法
直接把存活的对象移到另一内存区域,然后对原内存区域进行统一清除
缺点:把原内存区域一分为二,减少了内存可用量,当清除对象比较少,存活对象比较多时,造成移动对象太多,工作量大
3.分代收集算法
结合上面1和2,对存活对象进行分类,新生代对象和老生代对象,不同的对象放在不同的内存区域,主要判定依据就是对象的存活时间,对于新生代对象采用2这种算法,对于老生代对象采用1这种算法
三、垃圾回收器
不同的垃圾回收器的着重点不一样,对于垃圾回收线程与用户线程轮流执行的情况,主要有以下两类,该两种情况一般是成反比的
1.减少垃圾回收停顿时间
主要是用于B/S的架构,带来更好的用户体验
2.减少整个系统的吞吐量
吞吐量是指在一段时间内垃圾回收执行的效率
第三部分 虚拟机执行子系统
第一章 类文件结构
与传统的二进制执行文件不一样,.class类文件结构可以在JVM中执行,不仅仅是Java,JRuby和Groovy都是通过各自的编译器编译成.class文件的,这样使得JVM在平台无关性的基础上拥有语言无关性的优势
.class文件于XML文件不一样,它是以字节为基本单位,字节与字节之间是紧挨着的,必须严格按照格式才能解析出数据
.class文件只有两种数据结构:无符号数和表
前者就是以字节为单位的无符号数,后者是由无符号数和其他表组成的
1.魔数和版本号
前者标记该文件是.class文件(而不是通过后缀名判断),后者就是单纯的版本号
2.常量池
这是.class文件中容量很大,也很重要的区域,后面的各个区域很多都有对该区域进行引用
常量池一般有两种类型:UTF-8编码的字符串和引用UTF-8编码的字符串的具体类型(整型,类,字段引用,方法引用等等)
UTF-8编码的字符串主要存放字面量(“abc”)和符号引用,符号引用主要描述如下三类信息:
a.类和接口的全限定名
b.字段的名称和描述符
c.方法的名称和描述符
3.访问标志(public,private,protected)
由于在语法中,访问标志是固定的,所以不用引用UTF-8编码的字符串,可以用固定的二进制表示固定的权限
4.类索引,父类索引,接口索引集合
这里先表示总共有多少个接口,然后直接引用常量池来描述
5.字段表集合
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
其中,带有_index后缀的都是需要引用常量池来描述,_info后缀代表是表类型
在java语言中字段是无法重载的,但对于字节码,如果描述符一致,也是合法的(黑客攻击)
6.方法表集合
与字段表集合大致相同,有以下不同:
a.访问标志要复杂一些
b.方法的特征签名和字段的特征签名也有区别
c.还有构造函数会默认写入.class文件
d.在attribute_info中有一个Code属性很重要
7.属性表集合
在方法表中有个Code属性,这里就是.class文件的核心地方,存放JVM可以执行的字节码
还有LineNumberTable属性(用来描述源代码和字节码行号的对应关系)
LocalVariableTable属性
//TO-DO
第二章 虚拟机类加载机制
注意加载类初始化前与初始化后的区别
一、类加载步骤:
1.加载(单个类的动作)
a.通过类的完全限定名来获得类的二进制流
b.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
c.在Java堆中生存一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入库
2.连接:验证(类与类之间的动作)
验证.class文件的合法性
3.连接:准备(类与类之间的动作)
对类的静态变量分配内存
设置类变量初始值
4.连接:静态解析(类与类之间的动作)
将.class文件中的常量池中的符号引用替换为直接引用,这是在加载中的b项的基础上进行的
5.初始化(非static类型的字段将在这里被初始化)
执行类的构造函数
6.使用和卸载
二、类的加载器
1.何时两个Class对象是相等的?
只有.class文件和加载这个.class文件的加载器都一样时,才能说这两个Class对象是相等的
2.类加载的双亲委派模式
对于一个.class文件,先用基类加载器进行加载,如果几倍加载器不能加载,则调用子类加载器进行加载,这保证了:如果系统中有多个加载器,那么java.lang.Object类只会有一个
第三章 虚拟机字节码执行引擎
Human man = new Man();
假如Man是Human的子类,这里Human就是静态类型,Man就是实际类型
一、运行时栈帧结构
栈帧是虚拟机栈的基本单位,包括如下几项:
局部变量表:内存空间,用于存放参数和方法内部定义的局部变量,以Slot(32比特)为最小单位
操作数栈:功能类似于寄存器,用于对局部变量表中的数据项进行运算
动态连接:和解析一样,把符号引用变为直接引用,在多态这种情况下
方法返回地址:有return和抛异常两种方法退出方式
二、方法调用
1.解析
调用目标在程序代码写好,编译器进行编译时就定下来
2.分派
指定位方法执行版本
静态分派依赖于静态类型(加载阶段,例如:重载)
动态分派依赖于实际类型(运行阶段,例如:多态)
三、基于栈的字节码解释执行引擎
Java编译器完成词法分析,语法分析到抽象语法树后,再遍历语法树生成线性字节码指令流,
1.基于栈的指令集
优点:跨平台,指令更紧凑(每个字节就是一条指令),编译器实现简单(不需要考虑空间分配问题,所有空间都在栈上操作)
缺点:比较慢(一是有出栈、入栈的操作;二是访问是基于内存的)
2.基于寄存器的指令集
优点:执行快速(访问是基于寄存器的)
缺点:不跨平台
第四部分 程序编译和代码优化
第一章 早期(编译期)优化
Java中一些局部变量的修饰符是没有反应到具体的.class文件中的
比如
void f(){
final int a = 1;
}
void f(){
int a = 1;
}
而对应C++,因为是静态编译,类与类之间的调用在编译后就决定好了,所有类的非静态实例变量在编译后其修饰符信息就没有了
一、Java编译过程
1.解析与填充符号表
a.词法分析和语法分析(生成语法树)
b.填充符号表
符号表与之前说的局部变量表不是一回事,前者是编译时候使用的,后者是运行时候使用的,前者是地址分配的依据,
由前者生成后者
2.注解处理器
Java特有的,可以使用插入式注解器实现原本只能在编码阶段实现的功能
3.语义分析和字节码生成
a.语义分析
主要是根据语法树,在上下文环境中进行一些逻辑分析
如:
int d = 1;
int d = 2;
这在语法上是有错误的
b.解语法糖
Java中常见的语法糖是泛型、变长参数、自动装箱拆箱等
JVM在运行的时候并不知道这些语法,所有需要在编译阶段将语法糖还原成基础语法结构,这个过程就是解语法糖
Java和C#的泛型实现
在Java中,对于List<String> list 和 List<Integer> list,在.class文件中都是一样的 List<E> list,只是在相应的地方插上类转化代码(需要一些时间),也就是说,只保证了在代码编写阶段能保证类型安全,是一种伪泛型
而C#相反,对于List<String> list 和 List<Integer> list,在.class文件中分别为List<String> list 和 List<Integer> list
c.字节码的生成
生成字节码的时候会自动把默认构造函数(如果类中没有自定义构造函数)加入到字节码中
第二章 晚期(运行期)优化
解释器与即时编译器的搭配使用模式在虚拟机中被称为“混合模式”
一、即时编译器
为了提高热点代码的执行效率,即时编译是把.class中的字节码编译成机器码,编译时间放在了运行期
同时,如果热点代码发生了变化,如加载了新类后类型继承结构发生了变化,可以通过逆优化,退回到解释状态继续执行
这就有两个问题
1.什么是热点代码?
一般只有多次调用的
方法和多次执行的
循环体能够成为热点代码,一般采用绝对发生次数(方法调用计数器和回边计数器)和衰减周期来判断是否为热点代码
2.即时编译时考虑速度优先还是质量优先?
HotSpot提供了两种编译器,Client Complier 和 Server Complier,前者注重编译速度,后者注重编译质量
二、编译优化技术
1.公共子表达式消除(经典)
例如: int a = b + c + b + c + a
优化成: int a = e * 2 + a (其中 e = b + c)
2.数组范围查找(经典)
Java在对数组进行操作时候,每次都会进行边界检查,这样会比较耗时,所以需要优化,比如在循环变量的使用,对数据进行数据流分析后,确定循环变量永远在指定范围内,这样可以在循环内把边界检查消除
3.方法内联(重要)
只有在解析阶段的方法才能进行方法内联,动态调用的方法不能进行内联,Java中默认的实例方法就是虚方法,这类方法是不能被内联的,除非进行激进优化
4.逃逸分析(前沿)
该方法并不是直接优化手段,而是为其他优化手段提供依据的分析技术,具体就是当得知一个对象不会逃逸到方法或线程之外,那么可以进行栈上分配、同步消除、标量替换等优化工作