Java中的四种引用:
1、强引用(Strong Reference):在程序代码中普遍存在的,类似Object object = new Objec(),只要强引用还存在,垃圾收集器永远不会回收被引用的对象
2、软引用(Soft Reference):用来描述还有一些用,但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中,
并进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
3、弱引用(Weak Reference):用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集器发生之前。当垃圾收集器工作时,
无论当前内存是否足够,都会回收掉被弱引用关联着的对象。
4、虚引用(Phantom Reference):也称幽灵引用或幻影引用,它使最弱的一种引用关系。一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用获得
一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望该对象被收集器回收时收到一个系统通知。
判断一个类是否是无用的类:
1、该类所有的实例都已经被回收,也就是java堆中不存在该类的所有实例
2、加载该类的ClassLoader已经被回收
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
判断对象是否存活的算法:
1、引用计数法
2、根搜索算法
垃圾收集算法:
1、标记-清除
2、复制
3、标记-整理
垃圾收集器:
1、Serial收集器:单线程高效的收集器
2、ParNew收集器:Serial收集器的多线程版本
3、Parallel Scavenge收集器:新生代收集器,使用复制算法,并行的,目标是达到一个可控制的吞吐量,由于与吞吐量关系密切,Parallel Scavenge收集器也被常称为“吞吐量优先”
收集器。
4、Serail Old收集器:Serial收集器的老年代版本,同样是一个单线程收集器
5、Parallel Old收集器:是Parallel Scavenge的老年代版本,使用多线程和标记整理算法。
6、CMS收集器:是一种以获取最短回收停顿时间为目标的收集器。
运作过程:
1、初始标记
2、并发标记
3、重新标记
4、并发清除
优点:并发收集,低停顿
7、G1收集器
新生代GC(Minor GC)
老年代GC(Major GC/Full GC)
内存分配和回收策略:
1、对象优先在Eden分配
2、大对象直接进入老年代:
大对象对虚拟机的内存分配来说是一个坏消息,比遇到一个大对象更坏的消息是遇到一群“朝生熄灭”的大对象,写程序的时候应该尽量避免。
-XX:PretenureSizeThreshold参数,令大于这个值的对象直接在老年代中分配。这样做避免在Eden去和连个servivor区之间发生大量的内存拷贝。
3、长期存活的对象将进入老年代
4、动态对象年龄判定
虚拟机并总是要求对象经过设定的年龄阈值后才进入老年代,如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该
年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold要求的年龄。
5、空间分配担保:
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC,如果允许,那只会进行
Minor GC,如果不允许则也要改为进行一次Full GC。
虚拟机执行子系统:
Class类文件结构:
1、魔数和Class文件的版本
魔数:用于确定该文件是否是一个能被虚拟机接收的Class文件,其值为0xCAFEBABE。
进阶着魔数的四个字节之后的2个字节是次版本号,再接着的2个字节是主版本号。
2、常量池
紧接着朱版本号是常量池入口
3、访问标识
在常量池结束后,紧接着的2个字节代表访问标识。这个标识用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否
定义为public类型,是否定义为abstract类型,如果是类的话是否被声明为final等等。
4、类索引、父类索引、接口索引集合
由这三项来确定类的继承关系。
5、字段表集合
用于描述或者类中声明的变量。这些信息包括:变量的作用域,类级别还是实例级别,可见性,并发可见性,是否可序列化,字段数据类型,字段名称
6、方法表集合
与字段表集合的描述方法一致
依次包括了访问标识、名称索引、描述符索引、属性表集合几项。
7、属性表集合
在Class文件、字段表和方法表中都可以携带自己的属性集合,用以描述某些场景专有的信息。
1、code属性:
Java程序方法体里面的代码经过Javac编译器处理后,最终变为字节码指令存储在code属性内。
如果把一个Java程序里的信息分为代码和元数据,那么在整个class文件里,code属性用于描述代码,所有的其他数据项目就是用于描述元数据。
2、Exceptions属性
作用是列出方法中可能抛出的受检查异常,也就是方法描述时在throws关键字后面列举的异常。
3、LineNumberTable属性
用以描述源码行号和字节码行号的对应关系
4、LocalVarialableTable属性
用以描述栈帧中局部变量表中的变量与Java源代码中定义的变量之间的关系,它不是运行时必须的属性。
5、SourceFile属性
用以记录生成这个Class文件的源码文件名称
6、ConstantValue属性
通知虚拟机自动为静态变量赋值。只有被静态关键字(static)修改的变量才可以使用这项属性
7、innerClasses属性
用以记录内部类和宿主类之间的关联
8、Depreated及Synthetic属性
都是布尔属性,只存在有和没有的区别,没有属性值的概念。
synthetic代表此字段不是由java源码直接产生的,而是由编译器自行添加的。
Class文件结构的发展:
类加载的时机:
类的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载,其中验证、准备和解析称为连接。
虚拟机规范严格规定了有且只有四种情况必须对类进行初始化:
1、使用new关键字实例化对象的时候、读取或设置类的静态字段以及调用一个类的静态方法的时候
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则必须先对其进行初始化
3、当初始化一个类的时候,如果发现其父类还没有初始化,则会触发父类的初始化
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
以上4中情况称为主动引用,除此之外的所有引用都不会初始化类,称为被动引用。
被动引用举例如下:
1、通过子类引用父类的静态字段,不会触发子类的初始化。对于静态字段,只有直接定义这个字段的类才会被初始化,通过子类来引用父类,只会触发父
类的初始化,不会触发子类的初始化。
2、通过数组定义来引用类不会触发此类的初始化
TestClass[] testArray = new TestClass[10];
3、常量在编译阶段会存入调用类的常量池中,本质上没有引用到定义常量的类,因此不会触发定义常量的类的初始化。
接口的初始化过程与类是一致的,接口初始化与类的初始化真正的区别是:当一个类在初始化时,要求其父类已经全部初始化过了,但是一个接口在初始化时
并不要求其父接口全部完成了初始化,只有在真正使用到父接口的时候才初始化。
类加载的过程:
1、加载
在加载阶段,虚拟机要完成以下三件事情:
1、通过一个类的全限定名来获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个数据的访问入口
2、验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证的大致步骤:
1、文件格式验证:
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
验证完成后,字节流会进入方法区进行存储。所以后面的验证全部是基于方法区的存储结构进行的
2、元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
可能包含的验证点如下:
这个类是否有父类
这个类是否继承了不允许被继承的类
如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
类中的字段方法是否与父类产生了矛盾
...
3、字节码验证
是整个过程中最复杂的阶段,主要工作是进行数据流和控制流分析。
在第二阶段中对元数据信息中的数据类型做完校验后,这阶段对类的方法体进行校验分析。
这阶段的任务是保证被校验类的方法在运行不会做出危害虚拟机安全的行为。
4、符号引用验证
符号引用验证可以看成是对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验,通常需要校验以下内容:
符号引用中通过字符串描述的全限定名是否能找到对应的类
在指定类中是否能找到符合方法的字段描述及简单名称所描述的方法和字段。
符号引用中的类、字段和方法的访问性是否可被当前类访问
...
符号引用验证是确保解析能够正常进行,如果无法通过符号引用验证,则会抛出一个异常
如果全部代码全部验证过,可以通过-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间
3、准备
准备阶段正式为类分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
这时候会将静态变量的值赋值为默认初始值,如果静态变量是一个常量则会立即给其赋值
4、解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。
1、类或接口的解析
如果当前类为D,将从未解析过的符号引用N解析为一个类或接口C的引用,包括以下3个步骤:
1、如果C不是一个数组,JVM就会把代表N的全限定名传递给D的类加载器去加载这个C类,一旦过程出现任何异常,解析过程宣告失败。
2、如果C是一个数组类型,并且数组的元素类型为对象,那么久会按照第一点加载数组元素类型。
3、如果上面步骤有效进行,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但是在解析完成之前还要进行符号引用验证,
确认C是否具备对D的访问权限,如果发现不具备访问权限,则会抛出java.lang.IllegalAccessError异常
2、字段解析
要解析一个未被解析的字段符号引用,首先解析字段所属的类或接口的符号引用。解析完成后按照如下要求对C进行后续字段搜索:
先查找本身,然后查找继承关系中的类或接口,如果还未找到则会抛出java.lang.NoSuchFieldError异常
。
如果查找过程成功返回引用,则会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。
3、类方法解析
先解析方法所属的类或接口。然后按照如下要求对后续方法进行搜索:
先查找本身,然后查找继承关系中的父类
如果找到方法,则对其进行权限验证,同第二步
4、接口方法解析
先解析方法所属的类或接口。然后按如下要求对后续方法进行搜索:
先查本身,然后再查父接口
5、初始化
初始化阶段是执行类构造器clinit()方法的过程,clinit()方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。
虚拟机会保证一个类的clinit方法在多线程环境中被正确的加锁和同步,如果在clinit方法中有耗时很长的操作,那就有可能造成多个线程阻塞,
在实际中这种阻塞往往是很隐蔽的。
类加载器:
比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这2个类来源于同一个Class文件,只要加载它们的类
加载器不同,那这两个类就必定不相等。
双亲委派模型:
站在Java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个加载器是用C++语言实现,是虚拟机
自身的一部分,另外一种就是所有其他的类加载器,这些类加载器都是由java语言实现,独立于虚拟机外部,并且全部都继承自抽象类lava.lang.ClassLoader。
从Java开发人员的角度看,类加载器还可以划分的更细一点,绝大部分java程序都会使用到以下三种系统提供的类加载器:
1、启动类加载器(BootStrap ClassLoader)
负责将放在<JAVA_HOME>/lib目录下的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅仅按文件名识别,如rt.jar,名字
不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
2、扩展类加载器(Extension ClassLoader)
这个加载器由sum.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的
路径中的所有类库,开发者可以直接使用扩展类加载器。
3、应用程序类加载器(Application ClassLoader)
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以
一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有
自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型要求除了顶层的启动类加载器外,其余的加载器都应当有自己的父类加载器,这里类加载器之间的父子关系一般不会以继承的关系来实现,
都是使用组合关系来复用父加载器。
双亲委派模型的工作过程:
如果一个类加载器收到一个加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一个层次的加载器都是如此,
因此所有的加载请求最终都传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到这个类)时,
子加载器才会尝试自己去加载。
虚拟机字节码执行引擎:
运行时栈帧结构:
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它使虚拟机运行数据区中的虚拟机栈的栈元素。
每一个栈帧都包含了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
一个线程中方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所
关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
1、局部变量表
2、操作数栈
3、动态连接
4、方法返回地址
当一个方法被执行后,有2种方式退出这个方法:
1、执行引擎遇到任何一个方法返回的字节码指令,称为正常完成出口
2、在方法执行过程中遇到了异常,并且这个异常咩有在方法体内得到处理,这个退出称为异常完成出口,此时是不会给上层调用者任何返回值的。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:
恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
5、附加信息:
方法调用:
Class文件的编译过程不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都是符号引用,而不是在实际方法运行时内存布局中的入口地址(
相当于之前说的直接引用)。这个特性带来了java更强大的动态扩展能力,但也是Java方法的调用过程变得相对复杂起来,需要再类加载期间甚至在运行期间
才能确定目标方法的直接引用。
1、解析:
将“编译器可知,运行期不可变”的方法的符号引用转化为直接引用,符合这个要求的方法有:静态方法和私有方法。
java虚拟机提供了四条方法调用字节码指令:
1、invokestatic;调用静态方法
2、invokespecial:调用实例构造器<init>方法、私有方法和父类方法
3、invokevirtual:调用所有的虚方法
4、invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
2、分派
1、静态分派
2、动态分派
在运行期根据实际类型确定方法的运行版本称为动态分派
3、单分派和多分派
方法的接受者和方法的参数统称为方法的宗量。
单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个的宗量对目标方法进行选择。
java语言是一门静态多分配、动态单分派的语言。
4、虚拟机动态分派的实现
基于栈的字节码解释执行引擎:
虚拟机是如何执行方法中的字节码指令的。
1、解释执行:
在java语言中,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历抽象语法树生成线性字节码指令流的过程。
因为这一部分动作是虚拟机外执行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现。
2、基于栈的指令集和基于寄存器的指令集:
栈架构指令集慢与寄存器指令集
3、基于栈的解释器执行过程
类加载以及执行子系统案例与实战:
案例分析:
1、tomcat:正统的类加载器架构
2、OSGI:灵活的类加载器架构
3、字节码生成技术和动态代理的实现
4、Retrotranslator:跨越JDK的版本
程序编译与代码优化:
早期(编译器)优化:
从计算机程序出现的那一天起,对效率的追逐就是程序天生的坚定的信仰,这个过程犹如一场没有终点、永不停歇的方程式竞赛,程序猿是车手,
技术平台则是赛道上飞驰的赛车。
前端编译器:Sun的javac、Eclipse JDT中的增量式编译器(ECJ)
JIT编译器:HotSpot VM的C1、C2编译器
AOT编译器:GUN Compiler for Java(GCJ)、Excelsior JET
晚期(运行期)优化:
Java程序最终是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”,为了提高热点
代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译被称为即时编
器(Just In Time Compiler,JIT编译器)。
编译器优化技术:
方法内联
冗余访问消除
复写传播
无用代码消除
公共子表达式消除
如果一个表达式已经计算过了,并且从先前的计算到现在E中的所有变量值偶读没有发生变化,那么E的这次出现就称为了公共子表达式。
对于这种表达式没有必要再花时间进行计算,只要需要直接用前面计算过的表达式结果代替E就可以了。
数组边界消除检查
如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在区间[0, foo.length)之内,那整个循环过程中就可以数组上下界
检查消除掉,这可以节省很多次的条件判断操作。与语言相关的其他消除操作还有不少,如自动装箱消除、安全点消除、消除反射等。
方法内联
逃逸分析
是目前java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化手段提供依据的分
析技术。
分析对象动态作用域:当一个对象被定义后,它可能被外部方法所引用
如果能证明一个对象不会逃逸到方法或线程外,则可能为这个变量进行一些高效的优化:
栈上分配
同步消除
线程同步是一个耗时的过程,如果该对象不会逃逸,那么久不会有竞争,对这个变量实施的同步就可以消除了。
标量替换
标量是指一个数据无法再分成更小的数据来表示了,java虚拟机中的原始数据类型都不能再进一步分解,它们
就可以成为标量。相对的,如果一个数据可以继续分解,就称为聚合量,java中的对象就是最典型的聚合量。
如果把一个java对象拆散,根据程序访问的情况,将其使用到成员变量恢复原始类型来访问就叫标量替换。
对Java编译器的深入了解,有助于在工作中分辨哪些代码是编译器可以帮我们处理的,哪些代码需要自己调节以便更适合编译器的优化。