深入理解java虚拟机学习笔记

1.JDK(Java Development Kit) = Java程序设计语言 + Java虚拟机 + Java API类库

2.Java堆溢出
Java存储用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制来清除这些对象,就会在对象到达最大堆的容量限制后产生内存溢出异常

3.关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverError
  • 如果虚拟机在扩展栈时无法,则抛出OutOfMemoryError

4.运行时常量池溢出
向常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。
该方法的作用是:如果池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

5.方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。当运行时产生大量的类去填满方法区时,便会溢出。

6.运行时数据区域
深入理解java虚拟机学习笔记
7.引用计数器

  • 原理:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减一;任何时候计数器都为0时的对象就是不可能再被使用的。
  • 问题:很难解决对象之间的相互循环引用的问题。
    例子:对象objA和objB都有字段instance,赋值令objA.instance及objB.instance,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能在被访问,但是,他们之间互相引用着对方,导致他们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

8.永生代的垃圾回收两部分内容:废弃常亮和无用的类

9.“无用的类”的定义

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类 的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

10.垃圾收集算法

  • 标记-清除算法(Mark-Sweep)
    顾名思义,该算法有两个阶段:标记和清除。首先,标记出所有需要回收的对象,在标记完成后统一收掉所有被标记的对象。
    主要缺点:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
  • 复制算法
    将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将存活着的对象复制到另外一块上去,然后再把已使用过的内存空间清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
    主要缺点:将内存缩小为原来的一半,成本偏高。
  • 标记-整理算法
    标记的步骤和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  • 分代收集算法(Generational Collection)
    根据对象存活周期的不同将内存划分为几块。一般是将Java堆分成新生代和老年代,这样子可以根据各个年代的特点采用最合适的收集算法。
    在新生代中,每次垃圾收集时都会发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

11.垃圾收集器
如果说手机算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。

12.内存分配策略

  • 对象优先在Eden分配
  • 大对象直接进入老年代
    所谓大对象就是指,需要大量连续空间的Java对象,最典型的大对象就是那种很长的字符串及数组。
  • 长久存活的对象将进入老年代
    虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后依然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁)时,就会被晋升到老年代中。
  • 动态对象年龄判断
    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保
    在发生Minor GC时,虚拟机会检测之前每次晋升代老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果失败,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。

13.Java语言提供的语言无关性
深入理解java虚拟机学习笔记
14.Class文件

  • Class文件是一组以8位字节位基础的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上的空间的数据项时,则会按照高位在前面的方式分割成若干个8位字节进行存储。
  • Class文件采用类似于C语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1 个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值,或者按照UTF-8编码构成字符串值。表是由多个符号数或其他表作为数据项构成的复合数据类型,所有表习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
  • 魔数:每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
  • Class文件的版本号:紧接着魔数的4个字节存储的Class文件的版本号
  • 次版本号:第5和第6个字节
  • 常量池:紧接着主次版本号之后是常量池入口,常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。同时,它还是在Class文件中第一个出现的表类型数据项目。
  • 访问标志:常量池结束之后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final,等等。

15.类索引、父类索引与接口索引集合

  • 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。
  • Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,因此除了java.lang.Object外,所有java类的父类索引都不为0。接口索引就用来描述这个类实现了哪些接口,这些被实现的接口将按照implements语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口的索引集合中。

16.字段表集合

  • 字段表(field_info)用于描述接口类中声明的变量。字段(field)包括了类级变量或实例级变量,但不包括在方法内部声明的变量。
  • Java字段中可能包含的信息:字段的作用域(public、private、protected、修饰符)、是类级变量还是实例级变量(static修饰符)、可变性(final)、并发可见性(volatitle修饰符,是否强制从主内存读取)、可否序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。

17.方法表集合
结构:访问标志(access-flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attribute)

18.虚拟机的类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
深入理解java虚拟机学习笔记
19.在Java语言里,类型的加载和连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

20.必须立即对类进行“初始化”的四种情况

  • 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行初始化,则需要先触发其初始化
  • 使用java.lang.reflect包的方法对类进行发射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

21.类加载器双亲委派模型
深入理解java虚拟机学习笔记
22.虚拟机的执行引擎是由自己实现的,可以自行制定指令集与执行引擎的结构体系,并且能执行那些不被硬件直接支持的指令集格式。

23.运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

24.语法糖(Syntactic Sugar):也称糖衣语法,指在计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。通常来说,使用语法糖能够增加程序的可读性,减少程序代码出错的机会。

25.Java语言的“编译期”是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一点)把*.java文件转变成*.class文件的过程;也可能是指虚拟机的后端运行期编译器(JIT编译器,Just in Time Compiler)把字节码转变成机器码的过程;还可能是指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler)直接把*.java文件编译成本地机器代码的过程。下面列举了这三类编译过程中比较有代表性的编译器:
- 前端编译器:Sun的javac、Eclipse JDT中的增量式编译器(ECJ)
- JIT编译器:HotSpot VM的C1、C2编译器
- AOT编译器:GNU compiler for the java(GCJ)、Excelsior JET

26.JIT编译器
在部分的商用虚拟机(Sun HotSpot、IBM J9)中,Java程序最初是通过解释器(interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,即JIT编译器)

27.解释器

  • 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行
  • 解释器还可以作为编译器激进优化时的一个“逃生门”,让便一起去根据概率选择一些大多数时候能提升运行速度的优化手段。

28.解释器与编译器的交互
深入理解java虚拟机学习笔记
29.原子性、可见性与有序性

  • 原子性(Atomicty):从Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个。
  • 可见性(Visibility):指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
  • 有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。

30.Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式(Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive Threads-Scheduling)线程调度。

31.Java语言中的5种进程状态

  • 新建(New):创建后尚未启动的线程处于这种状态
  • 运行(Runnable):Runnable包含了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间
  • 无限期等待(Waiting):处于这种状态的进程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:
    • 没有设置Timeout参数的Object.wait()方法
    • 没有设置Timeout参数的Thread.join()方法
    • LockSupport.park()方法
  • 限期等待(Timed Waitting):处于这种状态的进程也不会被分配CPU执行时间,不过无须等待被其他程序显式的唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
    • Thread.sleep()方法
    • 设置了Timeout参数的Object.wait()方法
    • 设置了Timeout参数的Object.join()方法
    • LockSupport.parkNanos()方法
    • LockSupport.parkUntil()方法
  • 阻塞(Blocked):进程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程状态,线程已经结束执行。

上面5种状态之间的相互转换关系如下:
深入理解java虚拟机学习笔记