《深入理解Java虚拟机:JVM高级特性与最佳实践》笔记

时间:2022-12-29 10:24:22
一.Java内存区域与内存溢出

1.程序计数器是一块较小的内存空间,它可看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。各条线程都需要有一个独立的程序计数器,互不影响,独立存储。此内存区域是唯一一个在java虚拟机规范中没有规定任何oom情况的区域。

2.虚拟机栈描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。如果线程请求的栈深度大于虚拟机所允许的深度,就会出现soe异常,如果在动态扩展虚拟机栈时无法申请到足够内存,就会出现oom异常。局部变量表所需的内存空间在编译期间完成分配,一个方法需要在帧中分配多大的局部变量空间是完全确定的。

3.本地方法栈与虚拟机栈发挥的作用相似,本地方法栈为虚拟机使用到的native方法服务。

4.java堆对大多数应用来说,是虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域。此内存的唯一目的就是存放对象实例。所有的对象实例以及数组都要在堆上分配,但是不绝对。java堆是垃圾收集器管理的主要区域。java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

5.方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。1.7的hotspot已经把原本放在永久代的字符串常量池移出。

6.运行时常量池是方法区的一部分。String类的intern方法即是使用运行时常量池。

7.直接内存主要是NIO引入的一种基于通道与缓冲区的IO方式,它可以使用native函数库直接分配堆外内存。


二.对象的创建(针对HotSpot虚拟机)

1.java为对象在java堆中分配内存有两种方式:指针碰撞和空闲列表。

2.分配方式有一种叫做本地线程分配缓冲(TLAB),通过-XX:+/-UseTLAB设定。

3.内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。

4.new指令之后执行<init>方法,对象便实例完成了。

5.对象在内存中存储的布局可以分为3块区域:对象头,实例数据和对齐填充。

6.对象头包括两部分信息,第一部分用于存储对象自身运行时数据。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针。

7.实例数据部分是对象真正存储的有效信息。在分配策略中,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果compactFields参数值为true(默认true),那么子类之中比较窄的变量也可能会插入到父类变量的空隙中。第三部分对齐填充并不是必然存在的。


8.我们的java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。


三.对象已死吗?

1.(引用计数算法)引用算法,给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。主流的java虚拟机里面没有选用引用计数器算法,最主要的原因是它很难解决对象之间循环引用的问题。

2.(引用计数算法)可达性算法,主流实现中,都是通过可达性分析来判定对象是否存活。这个算法的基本思想就是通过一系列的成为 GC Roots 的对象作为起始点,开始向下搜索。

3.可作为 GC Roots的对象包括以下几种:虚拟机栈中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI引用的对象。

4.强引用,软引用,弱引用,虚引用。

5.要真正宣告一个对象死亡,至少要经历两次标记过程。

6.当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”finalize。

6.任何一个对象的finalize方法都只会被系统自动调用一次。不要使用finalize方法,它执行效率低,而且不确定性大。

7.永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

8.最基础的收集算法是“标记-清除”算法,之后是复制算法,标记-整理算法,分代算法。

四.HotSpot的算法实现

1.知识点:枚举根节点(OoMap),安全点,安全区域

2.垃圾收集器:

Serial收集器是最基本,发展历史最悠久的收集器,单线程,stop the world,简单高效,对于运行在client模式下的虚拟机来说是一个很好的选择。

ParNew收集器就是Serial收集器的多线程版本,是许多运行在Server模式下的虚拟机中首选的新生代收集器,一个很重要的原因是,出了Serial收集器外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge收集器是一个新生代收集器,它的目标是达到一个可控制的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法,收集器的主要意义也是在于给Client模式下的虚拟机使用。

Parallel Old是Parallel Scavenge收集器的老年代版本。CMS收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法,优点是并发收集,低停顿,对CPU资源非常敏感,但无法处理浮动垃圾,进行垃圾收集时会有大量空间碎片。

G1收集器当前很优秀的商业用收集器。


五.内存分配

1.大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

2.大对象直接进入老年代。

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

4.动态对象年龄判定。

5.空间分配担保。

六.class类文件结构

1.根据java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪机构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。

2.无符号数属于基本的数据类型,可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值。

3.表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合机构的数据,整个class文件本质上就是一张表。

4.每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号。Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的class文件。

5.紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是Class文件中第一个出现的表类型数据项目。常量池容量计数是从1而不是0开始的。常量池主要存放两大类常量:字面量和符号引用。Java程序中如果定义了超过64KB英文字符的变量或者方法名,将会无法编译。

6.在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类的话,是否被声明为final等。

7.类索引,父类索引与接口索引集合:类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,class文件中由这三项数据来确定这个类的继承关系。类索引,父类索引和接口索引都按顺序排列在访问标志之后。

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

9.方法表集合:Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法里的java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“code”的属性里面。

10.属性表集合:在Class文件,字段表,方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。


七.字节码指令简介

1.Java虚拟机的指令由一个字节长度,代表着某种特定操作含义的数字以及跟随其后的零至多个代表此操作所需参数而构成。

2.在Java虚拟机的指令集中,大部分的指令都包含了其操作所对应的数据类型信息。

3.加载和存储指令。

4.运算指令。

5.类型转换指令。

6.对象创建与访问指令。

7.操作数栈管理指令。

8.控制转移指令。

9.方法调用和返回指令。

10.异常处理指令:而在java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的。

11.同步指令。

八.虚拟机类加载机制

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

2.在Java语言里面,类型的加载,连接和初始化过程都是在程序运行期间完成的。

3.类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用和卸载七个阶段。其中验证,准备,解析3个部分统称为连接。

4.使用new 关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。触发初始化。

5.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

6.当初始化一个类的时候,如果发现其父类好没有进行过初始化,则需要先触发其父类的初始化。

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

8.当使用JDK1.7的动态语言支持时,如果MethodHandle实例最后解析结果是特定方法句柄,并且这个方法句柄对应的类没有进行初始化,则触发初始化。

9.通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

10.通过数组定义来引用类,不会触发此类的初始化。

11.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

12.接口在初始化,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化。

13.加载:通过一个类的全限定名获取二进制字节流;将字节流所代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class对象,作为访问入口。

14.验证:文件格式验证;元数据验证;字节码验证;符号引用验证。

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

16.解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

17.初始化:类初始化阶段是类加载过程的最后一步,到了这一步,才真正开始执行类中定义的Java程序代码。

18.静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

19.类加载器:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

20.双亲委派模型

九.虚拟机字节码执行引擎

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

2.局部变量表:一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

3.操作数栈:也常称为操作栈,它是一个后入先出栈。

4.动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

5.方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时还不涉及方法内部的具体运行过程。

6.解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,根据分派依据的宗量数可分为单分派和多分派。

7.所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。静态分派的典型应用是方法的重载。

8.我们把这种在运行期根据实际类型确定方法执行版本的分派过程成为动态分派。

9.1.7为止,java语言是一门静态多分派,动态单分派的语言。

10.动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期。变量无类型而变量值才有类型这个特点也是动态类型语言的一个重要特征。

11.MethodHandle与Reflection的区别

12.java语法糖:泛型,自动装箱,自动拆箱,遍历循环,变长参数,条件编译等等。

十.JAVA线程


1.volatile变量只能保证可见性。使用volatile的第二个语义是禁止指令重排序优化。

2.先行发生原则。

3.KLT与LWP;用户线程;混合线程。

4.协同式线程调度;抢占式线程调度。

5.线程的状态:新建,运行,无限期等待,限期等待,阻塞,结束。

(完)