《Java虚拟机》笔记

时间:2021-07-03 20:46:02

记录了自己读《Java虚拟机》的一些笔记,主要在内存管理和代码优化那一块。


1、JVM内存管理

运行时数据区

  • 方法区(Method Area)
  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Method Stack)
  • 堆(Heap)
  • 程序计数器(Program Counter Register)
    执行引擎、本地库接口

程序计数器
- 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的

Java虚拟机栈
- 线程私有的
- 存储局部变量表、操作数栈、动态链接、方法出口等信息

本地方法栈
- 本地方法栈为虚拟机使用到的Native方法服务

Java堆
- Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域

方法区
- 各个线程共享
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 运行时常量池:存放编译期生成的各种字面量和符号引用

直接内存

- 并不是虚拟机运行时数据区的一部分,如NIO

对象的创建
- (查找加载类)虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- (分配内存)对象所需内存的大小在类加载完成后便可完全确定, 内存绝对规整-指针碰撞,内存不是规整-空闲列表
- (初始化信息)虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息

对象的内存布局

对象的访问定位
- 句柄:划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址
- 直接指针访问:那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址

垃圾收集(Garbage Collection,GC)

判断对象是否存活
- 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1。至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
- 可达性分析算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。可作为GC Roots的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。

- 本地方法栈中JNI(即一般说的Native方法)引用的对象

是否必须回收
- 筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或 者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
- 重写finalize,将自己赋值给某个类变量或者对象的成员变量,就会逃过回收,但finalize使用执行一次,所以只能逃过一次

回收方法区

垃圾收集算法

标记-清除算法
- 标记和清除两个过程的效率都不高
- 标记清除之后会产生大量不连续的内存碎片

复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块

标记-整理算法

分代收集算法
一般是把Java堆
分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代
中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付
出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间
对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

2、JAVA程序编译与代码优化

解析与填充符号表
- 1.词法、语法分析
- 2.填充符号表

注解处理器

语义分析与字节码生成
- 1.标注检查
- 2.数据及控制流分析
- 3.解语法糖

- 4.字节码生成

泛型与类型擦除
- 泛型:在编译后的字节码文件中就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码

自动装箱、拆箱与遍历循环

**条件编译**if(true)

3、高效并发

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

volatile
- 保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。 操作栈顶的值就变成了过期的数据,(堆内存可见,但操作栈内存不可见)++volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题++

- 第二个语义是禁止指令重排序优化

原子性、可见性与有序性

原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是long和double的非原子性协定

可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

有序性(Ordering):Java内存模型的有序性在前面讲解volatile时也详细地讨论过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表
现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

先行发生原则
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存*享变量的值、发送了消息、调用了方法等

实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。

分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)

线程安全当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的

线程安全的实现方法
- 互斥同步Synchronization ReentrantLock
- 非阻塞同步
- 无同步方案java.lang.ThreadLocal

自旋锁
共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值,为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁

自适应的自旋锁
1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

锁粗化
只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

轻量级锁

偏向锁它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。