Java虚拟机(JVM)读书笔记

Java虚拟机(JVM)第二版


前言:内容基本是其他博主博客笔记或链接,偶尔会加些个人补充,仅作为复习巩固用途。如有错误,请指出,谢谢。
提示:明珠在前,就不赘述了。大量图形辅助少量文字加强理解。部分总结了书上第2、6、7、12、13章内容。

2、Java 内存区域与内存溢出异常

2.1、运行时数据区域

1、参考博客:Java虚拟机(JVM)你只要看这一篇就够了!
Java虚拟机(JVM)读书笔记

1、程序计数器

2、Java 虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:
每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、 操作数栈、 动态链接、 方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

注意:2个异常
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

3、本地方法栈

注意:和虚拟机栈的区别

4、 Java 堆

注意:是几乎,而不是绝对。但初步学习暂时可认为绝对。垃圾回收。
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域。
注意:1个异常
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

5、方法区

注意:1个异常
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出
OutOfMemoryError异常。
Java虚拟机(JVM)读书笔记

运行时常量池

注意:1个异常。编译器和运行期(String 的 intern() )都可以将常量放入池中。这个暂时不理解
运行时常量池(Runtime Constant Pool)是方法区的一部分。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申
请到内存时会抛出OutOfMemoryError异常。

直接内存

注意:1个异常
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

2.2、HotSpot 虚拟机对象探秘

这里直接看书

1、 对象的创建,类加载比较重要

1、虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一
个类的符号引用,并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。 如果没
有,那必须先执行相应的类加载过程。
2、在类加载检查通过后,接下来虚拟机将为新生对象分配内存。 对象所需内存的大小在类
加载完成后便可完全确定,为对象分配空间的任务等同于把
一块确定大小的内存从Java堆中划分出来。
3、接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、 如何才能找
到类的元数据信息、 对象的哈希码、 对象的GC分代年龄等信息。 这些信息存放在对象的对
象头(Object Header)之中。

2、 对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
1、对象头(Header):包含两部分,
第一部分用于存储对象自身的运行时数据,如哈希码、锁,线程等。
第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。
2、实例数据(Instance Data)部分

3、对齐填充(Padding):保证对象大小是某个字节的整数倍。

3、 对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象。
3.1、句柄访问
Java虚拟机(JVM)读书笔记
3.2、直接指针访问
Java虚拟机(JVM)读书笔记

2.3、 OutOfMemoryError(OOM)异常

注意:极其关键,对于日常工作中,主要是快速定位异常位置,解决异常。

1、 Java堆溢出

原因:垃圾回收机制因某些特殊原因没及时清除对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

2、 虚拟机栈和本地方法栈溢出

注意:
这里不多说明,目前碰到的这种情况是刷题中,递归没有退出或正确退出条件,无限递归,栈爆了。

3、 方法区和运行时常量池溢出,了解

1、String.intern()是一个Native方法,字符串常量池相关
2、当前的很多主流框架,如Spring、 在对类进行增强时,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。

3、垃圾回收器与内存分配策略

这句话简明扼要:
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想
进去,墙里面的人却想出来。

只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

3.1、对象已死吗

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一
件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径
使用的对象)。

1、 引用计数法,可以看博客中的示意图

大部分情况下,给对象添加一个引用计数器。但是难以解决循环引用问题。

2、 可达性分析算法

存在‘GC Roots’ 的对象作为起始点Java虚拟机(JVM)读书笔记

3、 再谈引用之强引用、软引用、弱引用、虚引用

4、生存还是死亡,对象的finalize() 方法

5、回收方法区,类加载与类卸载有关

3.2、垃圾收集算法

最初思路是标记-清除算法,然后慢慢考虑存活率优化得到新算法

1、 标记-清除算法

2、 复制算法

把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。

3、标记-整理算法

4、分代收集算法

注意:
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

3.3、HotSpot 的算法实现,看书

3.4、垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。有点哲学意味了。
下面是各种类型收集器,仅简单说明两种,其他看书
Java虚拟机(JVM)读书笔记书上原话:
从JDK 1.3开始,一直到现在最新的JDK 1.7,HotSpot虚拟机开发团队为消除或者减少工
作线程因内存回收而导致停顿的努力一直在进行着,从Serial收集器到Parallel收集器,再到
Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成果Garbage First(G1)收集器,我
们看到了一个个越来越优秀(也越来越复杂)的收集器的出现,用户线程的停顿时间在不断
缩短,但是仍然没有办法完全消除(这里暂不包括RTSJ中的收集器)。 寻找更优秀的垃圾收
集器的工作仍在继续!

1、CMS 收集器

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。
运作步骤:
初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
并发标记(CMS concurrent mark):进行 GC Roots Tracing
重新标记(CMS remark):修正并发标记期间的变动部分
并发清除(CMS concurrent sweep)
Java虚拟机(JVM)读书笔记
注意:上图中并发标记没标记上,对比G1的示意图就明白在哪个位置了。

2、G1 收集器

运作步骤:
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
Java虚拟机(JVM)读书笔记

3.5、内存分配与回收策略

注意:下面图感觉很好,但不懂为啥?
Java虚拟机(JVM)读书笔记 以下4种进入新老年达转换都是基于特殊策略的,简单了解
1、大对象直接进入老年代
2、长期存活的对象将进入老年代
3、动态对象年龄判定
4、空间分配担保

6、类文件结构

6.1、无关性的基石

Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

6.2、Class类文件的结构

注意:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接
口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存
储数据,这种伪结构中只有两种数据类型:无符号数和表。

1、常量池,理解很重要

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、 声明为final的常量值等。
而符号引用则属于编译原理方面的概念,包括了下面三类常量:
1、类和接口的全限定名(Fully Qualified Name)
2、字段的名称和描述符(Descriptor)
3、方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟
机加载Class文件的时候进行动态连接。 也就是说,在Class文件中不会保存各个方法、 字段
的最终内存布局信息,因此这些字段、 方法的符号引用不经过运行期转换的话无法得到真正
的内存入口地址,也就无法直接被虚拟机使用。 当虚拟机运行时,需要从常量池获得对应的
符号引用,再在类创建时或运行时解析、 翻译到具体的内存地址之中。

6.2、字节码指令简介

7、虚拟机类加载机制

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

2、与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载、 连接和
初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开
销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依
赖运行期动态加载和动态连接这个特点实现的。

7.1、类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期如下7个阶段:
Java虚拟机(JVM)读书笔记
再度参考博客:Java虚拟机(JVM)你只要看这一篇就够了!

7.2、类加载的过程

1、加载

在加载阶段,虚拟机需要完成以下3件事情
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据
的访问入口。

第1条,它没有指明二进制字节流要从一个Class文件中获取,准确地说是根本没有指明要从哪里获取、 怎样获取。
所以也就有了各种取法:
1、从ZIP包中读取,这很常见,最终成为日后JAR、 EAR、 WAR格式的基础。
2、从网络中获取,这种场景最典型的应用就是Applet。
3、运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy
中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类
的二进制字节流。
4、由其他文件生成,典型场景是JSP应用,即由JSP文件生成对应的Class类
5、从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)
可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

非数组类的加载与数组类的加载是有区别,具体看书。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之
中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据
结构。 然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对
于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这
个对象将作为程序访问方法区中的这些类型数据的外部接口。

2、验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息
符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

3、准备,两点注意尤为重要

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

两点注意:
1、这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将
会在对象实例化时随着对象一起分配在Java堆中。
2、这里所说的初始值“通常情况”下是数据类型的零值。看书理解
3、“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值

4、解析

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

5、初始化,重要

1、类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应
用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。
2、 到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
3、在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通
过程序制定的主观计划去初始化类变量和其他资源。看书

7.3、类加载器,太重要

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字
节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的
类。 实现这个动作的代码模块称为“类加载器”。

注意书中原话
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚
拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。 这句话可以表达得更通
俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意
义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类
加载器不同,那这两个类就必定不相等。
个人理解是:同一个类加载器加载同一个Class文件来初始化一个对象才为true

1、双亲委派模型

从Java虚拟机的角度来讲,只存在两种不同的类加载器:
1、一种是启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分;
2、另一种就是所有其他的类加载器,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

从Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都
会使用到以下3种系统提供的类加载器:
1、启动类加载器(Bootstrap ClassLoader)
2、扩展类加载器(Extension ClassLoader)
3、应用程序类加载器(Application ClassLoader):一般也称它为系统类加载器。 它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
Java虚拟机(JVM)读书笔记

2、破坏双亲委派模型,暂时解除不到

12、Java 内存模型与线程

一个服务端同时对多个客户端提供服务则是另一个更具体的并发应用场景。 衡量一个服务性能的高低好坏,每秒事务处理数(TransactionsPer Second,TPS)是最重要的指标之一,它代表着一秒内服务端平均能响应的请求总数,而TPS值与程序的并发能力又有非常密切的关系。 对于计算量相同的任务,程序线程并发协调得越有条不紊,效率自然就会越高;反之,线程之间频繁阻塞甚至死锁,将会大大降低程序的并发能力。

无论语言、 中间件和框架如何先进,开发人员都不能期望它们能独立完成所有并发处理的事情,了解并发的内幕也是成为一个高级程序员不可缺少的课程。

12.1、Java内存模型

1、主内存与工作内存

Java虚拟机(JVM)读书笔记这里无法理解,就只好照搬书上内容
这里所讲的主内存、 工作内存与本书第2章所讲的Java内存区域中的Java堆、 栈、 方法区
等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起
来,那从变量、 主内存、 工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据
部分[6],而工作内存则对应于虚拟机栈中的部分区域。

2、内存间交互操作,看书

3、对于volatile型变量的特殊规则,内容有些复杂,留待日后

当一个变量定义为volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可
见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以
立即得知的。 而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来
完成。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通
过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。
运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
变量不需要与其他的状态变量共同参与不变约束。

大多数场景下volatile的总开销仍然要比锁低,我们在volatile与锁之中选择的唯一依据仅仅是volatile的语义能否满足使用场景的需求

4、先行发生原则,看书

12.2、Java与线程

Java语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行start()且还未结束的java.lang.Thread类的实例就代表了一个线程。

1、Java线程调度

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

2、状态转换,看书看图

Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种
状态,这5种状态分别如下。
1、新建(New)

2、运行(Runable)

3、无限期等待(Waiting)

4、限期等待(Timed Waiting)

5、阻塞(Blocked)

6、结束(Terminated)

Java虚拟机(JVM)读书笔记

13、线程安全与锁优化

13.1、线程安全,看书

《Java Concurrency In Practice》 的作者Brian Goetz对“线程安全”有一个比较恰当
的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交
替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象
的行为都可以获得正确的结果,那这个对象是线程安全的”。

为了更加深入地理解线程安全,在这里我们可以不把线程安全当做一个非真即假的二元
排他选项来看待,按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种
操作共享的数据分为以下5类:不可变、 绝对线程安全、 相对线程安全、 线程兼容和线程对
立。
1、不可变
“不可变”带来的安全性是最简单和最纯粹的。

2、绝对线程安全
在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。 java.util.Vector例子。

3、相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操
作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连
续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在Java语言中,大部分的线程安全类都属于这种类型,例如Vector、 HashTable、
Collections的synchronizedCollection()方法包装的集合等。

4、线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段
来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时
候指的是这一种情况。
Java API中大部分的类都是属于线程兼容的,如与前面的Vector和
HashTable相对应的集合类ArrayList和HashMap等。

5、线程对立
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。

13.2、线程安全的实现方法

1、互斥同步

1、在Java中,最基本的互斥同步手段就是synchronized关键字
2、除了synchronized之外,我们还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步
注意:
在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

2、非阻塞同步

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称
为阻塞同步(Blocking Synchronization)。

3、无同步方案

同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

13.3、锁优化

适应性自旋(Adaptive Spinning)、 锁消除(Lock Elimination)、 锁粗化(Lock Coarsening)、 轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等,
这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。