[转][读书笔记]深入理解java虚拟机

时间:2024-10-28 15:34:14

原文地址:http://blog.****.net/hanekawa/article/details/51972259

第二章 Java内存区域与内存溢出异常

一,运行时数据区域:

1.        程序计数器:当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一跳需要执行的字节码指令。如果执行的是native方法,这个计数器的值则为空。

2.        Java虚拟机栈

线程私有,生命周期与线程相同。描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译器可知的各种基本类型数据、对象引用、returnAddress类型

3.        本地方法栈

为虚拟机使用到的native方法提供虚拟机栈的服务

4.        Java堆

被所有线程共享的一块内存区域,在虚拟机启动时创建,存放对象实例。几乎所有对象实例都在这里分配内存。是垃圾收集器管理的主要区域。

5.        方法区

各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。又叫永久代

6.        运行时常量池

方法区的一部分,用于保存编译器生成的各种字面量和符号引用

7.        直接内存

NIO使用native函数库直接分配的堆外内存

二,对象的创建过程

虚拟机遇到一条new指令后,首先将去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载解析初始化过。如果没有那必须先执行相应的类加载过程。类加载检查通过后,接下来虚拟机将为新生对象在堆中分配内存。分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。接下来虚拟机要为对象设置对象头,写入对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码、gc分代年龄等。从虚拟机的角度来看新的对象已经完成,但对java程序还需要进行初始化工作,<init>方法执行。

三,对象的访问定位

1.        通过句柄访问

[转][读书笔记]深入理解java虚拟机

2.        通过直接指针访问

[转][读书笔记]深入理解java虚拟机

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

一,判断对象生死的算法

1,  引用计数法

给对象中添加一个引用计数器,每当有一个地方引用他时计数器+1,当引用失效时计数器-1;任何时刻计数器为0的对象就是不可能再被使用的。缺点是难以解决对象之间循环引用的问题。

2,  可达性分析算法

通过一系列的gc root的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到gc roots没有任何引用链相连时,此对象是不可用的,可回收。Gc roots有以下对象:虚拟机栈(本地变量表)中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI引用的对象。

二,引用类型:强引用,软引用,弱引用,虚引用。

强引用:程序代码中普遍的存在

软引用:描述一些还有用但非必须的对象。系统会在发生内存溢出异常之前把这些对象列进回收范围之中进行第二次回收

弱引用:用来描述非必需对象,比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。

虚引用:最弱的一种引用关系,为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

三,finalize()方法在对象被第一次标记并被虚拟机认为有必要执行后触发,这个对象将会放置在fqueue中,并由稍后建立的finalizer线程去触发这个方法(不保证执行完),来使执行一些回收前的操作。只被执行一次。

四,垃圾收集算法

1,  标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:标记和清除两个过程的效率都不高;标记清除后会留下大量的不连续内存碎片,导致程序需要分配较大对象时无法找到足够的连续内存而提前触发另一次垃圾回收

2,  复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块用完了九江还存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清除掉。

3,  标记-整理算法:标记过程和标记清除算法一样,之后让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

4,  分代收集算法

五,垃圾收集器

1,   Serial收集器

2,   ParNew收集器

3,   Parallel scavenge收集器

4,   Serial old收集器

5,   CMS收集器:以获取最短回收停顿时间为目标的收集器。基于标记清除算法,基于四个步骤:

a)        初始标记(stop)标记一下gc roots能直接关联到的对象

b)        并发标记进行gc roots tracing

c)        重新标记(stop)修正并发标记期间因用户程序运作而导致标记产生变动的一部分对象的标记记录

d)        并发清除

缺点:对cpu资源非常敏感

无法处理浮动垃圾

收集结束会有大量空间碎片

6,   G1收集器:将整个java堆划分成多个大小相等的独立区域。

特点:

a)        并行与并发

b)        分代收集

c)        空间整合

d)        可预测的停顿

G1收集器之所以可以建立可预测的停顿时间模型,是因为他可以有计划的避免在整个java堆中进行全区域的垃圾回收。G1根据各个区域里面的垃圾堆积价值大小维护一个优先列表,每次根据收集时间优先回收价值最大的区域。

步骤:

a)        初始标记(stop)

b)        并发标记

c)        最终标记(stop)

d)        筛选回收(stop)

六,内存分类与回收策略

1,   对象优先在eden区分配

2,   大对象直接进入老年代

3,   长期存活的对象将进入老年代

4,   动态对象年龄判定(如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代)

5,   空间分配担保

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

一,jps:虚拟机进程状况工具

二,jstat:虚拟机统计信息监视工具

三,jinfo:java配置信息工具

四,jmap:java内存映像工具

五,jhat:虚拟机堆转储快照分析工具

六,jstack:java堆栈跟踪工具

七,HSDIS:JIT生成代码反汇编

八,JConsole:java监视与管理控制台

九,VisualVM:多合一故障处理工具

第六章 类文件结构

一,Class文件结构

1.        魔数与Class文件版本

2.        常量池:主要存放两大类常量:字面量(常量,如文本字符串、声明为final的常量值等)和符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)

3.        访问标志(class是类还是接口,public类型?abstract?final?)

4.        类索引、父类索引、接口索引集合:用来确定类的继承关系

5.        字段表集合:用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,不包括在方法内部声明的局部变量。

全限定名:org/sun/clazz/TestCase;

简单名称:没有类型和参数修饰的方法或者字段名称inc() :-> inc

描述符:描述字段的数据类型、方法的参数列表和返回值

字段表集合不会列出从超类或者父接口中继承而来的字段,但有可能列出原本java代码中不存在的字段。

6.        方法表集合:包括了访问标志、名称索引、描述符索引、属性表集合

在java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此在java语言里是无法仅仅依靠返回值的不同来对一个已有的方法重载的。但是在class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于一个class文件中的。

7.        属性表集合

第七章 虚拟机类加载机制

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

二,类加载生命周期:加载、验证、准备、解析、初始化、使用、卸载。

三,有且只有五种情况必须立刻对类进行初始化:

1.        遇到new、getstatic、putstatic、invokestatic这4条字节码指令时。

2.        使用java.lang.reflect包的方法对类进行反射调用的时候。

3.        当初始化一个类的时候,如果其父类还没有进行过初始化则需要初始化其父类。

4.        当虚拟机启动时,用户需要指定一个要执行的主类(main())虚拟机会先初始化这个主类

5.        当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStatic REF_invokeStatic的方法句柄,并且这个方法句柄对所对应的类没有进行过初始化,则需要先触发其初始化

四,类加载过程

1.        加载

a)        通过一个类的全限定名来获取定义此类的二进制字节流

b)        将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

c)        在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

2.        验证

a)

3.        准备:准备阶段是正是为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

a)        进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量。

b)        初始值通常情况下是数据类型的零值(public static int val=123,准备阶段后val=0,把val赋值为123是在程序被编译之后初始化阶段执行,存放于类构造器<clinit>()方法之中)

4.        解析

a)        符号引用:以一组符号来描述所引用的目标,符号可以是类和形式的字面量,只要使用时能无歧义的定位到目标即可,与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。

b)        直接引用:可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,引用的目标必定已经在内存中存在。

5.        初始化:执行类构造器<clinit>()方法的过程

a)        <clinit>()是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器手机的顺序是由语句在源文件中出现的顺序所决定。静态语句块中只能访问到定义在静态语句块之前的变量,定义在之后的变量在前面的静态语句块可以赋值但不能访问

b)        <clinit>()方法与类的构造函数不同,不需要显示地调用父类构造器,虚拟机会保证在子类<clinit>()方法执行之前执行父类的<clinit>()。第一个被执行<clinit>()的肯定是java.lang.Object

c)        父类的<clinit>()先执行,意味着父类中定义的静态语句块要先于子类的变量赋值操作

d)        <clinit>()方法对于类和接口不是必须的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()

e)        接口不能使用静态语句块,但仍然有变量初始化的赋值操作。执行接口的<clinit>()不需要先执行父接口的<clinit>()。接口的实现类在初始化时也不会执行接口的<clinit>()方法

f)         虚拟机会保证一个类的<clinit>()在多线程环境中被正确的加锁、同步。

五,类加载器

1.        对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个class文件,被同一个虚拟机家在,只要家在他们的类加载器不同,那这两个类就必定不相等。

2.        双亲委派模型

a)        对于虚拟机只存在两种类加载器:启动类加载器(Bootstrap ClassLoader,用C++语言实现,是虚拟机自身的一部分)和所有其他类加载器(java编写独立于虚拟机外部,全部继承于java.lang.ClassLoader)

b)        对于开发人员有三类加载器:启动类加载器(<JAVA_HOME>\lib)、扩展类加载器(<JAVA_HOME>\lib\ext)、应用程序加载器(程序中默认的类加载器,ClassPath)

[转][读书笔记]深入理解java虚拟机

3.        双亲委派模型工作流程:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,二十八这个请求委派给父类加载器去完成,每一层次的类加载器都是如此。只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载

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

一,运行时栈帧结构

1.        栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

2.        局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。虚拟机通过索引定位的方式使用局部变量表。

3.        操作数栈:是一个后入先出栈。当一个方法刚刚开始执行的时候,这个方法的操作数是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,即入栈和出栈。

4.        动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。类文件的常量池中存在的符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,称为静态解析;另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

5.        方法返回地址

a)        当一个方法开始执行后,只有两种退出方法:执行引擎遇到任意一个方法返回的字节码指令,这种推出方法称为正常完成出口;另一种退出方法是在方法执行过程中遇到了异常,并且这个异常没有在方法体内的到处理,只要在本方法的异常表中没有搜索到匹配的一场处理器,会导致异常退出,这种退出的方式成为异常完成出口。无论采用何种退出方式,在方法退出之后都要返回到方法被调用的位置,程序才能继续执行。

b)        方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。

6.        附加信息

二,方法调用

1.        分派:分为静态分派和动态分派。

a)        静态分派:虚拟机在重载overload时通过参数的静态类型而不是实际类型作为判定的依据,并且静态类型是编译期可知的,因此在编译阶段javac编译器会根据参数的静态类型决定使用哪个重载版本。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用方法是方法重载。静态分派发生在编译期间,因此确定静态分派的动作实际上不是由虚拟机来执行的。

b)        动态分派:和多态性的重写override有密切的关联。

Invokevirtual指令运行时解析过程大致分为:

c)        找到操作数栈顶的第一个元素指向的对象的实际类型C;如果在类型C中找到与常量中的描述符和简单名称都相符地方法,则进行访问权限校验,若通过则返回这个方法的直接引用,查找过程结束;没通过权限则返回Error;如果未找到相符的方法则按照继承关系从下往上依次对C的各个父类进行搜索和验证;若始终没有找到合式的方法,则抛出Error

d)        方法的接收者和方法的参数统称为方法的宗量。根据分派基于多少种宗量可以讲分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择。多分派是根据多余一个宗量对目标方法进行选择。

Java语言的静态分派属于多分派类型;动态分派属于单分派类型。

e)        使用虚方法表索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向弗雷德实现入口;如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。

第十一章运行期优化

一,优化技术

1.        公共子表达式消除:如果一个表达式E已经计算过了,并且从先前的计算到现在的E中所有变量值都没发生变化,那么E的这次出现就成为了公共子表达式,只需要直接用前面计算过的表达式结果代替E就可以

2.        数组边界检查消除:如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远没有越界,那在整个循环中就可以把数组的上下界检查消除

3.        方法内联:把目标方法的代码“复制”到发起调用的方法之中,避免真实的方法调用。由于在编译期进行解析的只有私有方法、实例构造器、父类方法以及静态方法,其他的java方法调用都需要在运行时进行方法接收者的多态选择,即java语言中默认的实例方法是虚方法。未解决这种问题,引入了CHA类型继承关系分析技术,用于确定在目前已加载的类中某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等

4.        逃逸分析:基本行为就是分析对象动态作用域。他可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸,甚至可能被外部线程访问到,称为线程逃逸。

如果能证明一个对象不会逃逸到方法或线程之外,则可以为这个变量进行一些高效的优化:

i.             栈上分配:将线程内对象在栈上分配内存

ii.             同步消除:此变量上实施的同步措施可以消除

iii.             标量替换:如果把一个java对象拆散,根据程序访问的情况将其使用到的成员变量恢复原始类型来访问就叫做标量替换。

二,Java与C/C++编译器对比:java虚拟机的即时编译器与C/C++的静态优化编译器相比有以下劣势

1.        因为即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,他能提供的优化手段也严重受制于编译成本

2.        Java语言是动态的类型安全语言,这就意味着需要有虚拟机来确保程序不会违反语言语义或访问非结构化内存,意味着虚拟机必须频繁的进行动态检查。

3.        因为java语言中虽然没有virtual关键字,但是使用虚方法的频率却远大于C/C++,即时编译器在进行一些优化(内联)时的难度要远大于C/C++

4.        Java语言是可以动态拓展的语言,运行时加载新的类可能改变程序类型的继承关系,这使得很多全局的优化难以进行。

5.        Java语言中对象的内存分配都是堆上进行的,只有方法中的局部变量才能在栈上分配,而C/C++有多种分配方式,如果在栈上分配线程私有对象将减轻内存回收压力。另外C/C++中主要由用户程序代码来回收分配的内存,运行时效率上比垃圾回收机制要高。

Java语言的这些性能上的劣势都是为了换取开发效率上的优势而付出的代价,动态安全、动态扩展、垃圾回收为java语言的开发效率做出了很大贡献

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

一,java内存模型

[转][读书笔记]深入理解java虚拟机

二,先行发生原则:是java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B其实就是说在发生操作B之前操作A产生的影响能被操作B观察到。

三,线程状态转换

[转][读书笔记]深入理解java虚拟机