JVM知识点复习

2023/03/14 Java JVM

对Java虚拟机知识点的复习,参照《深入理解JVM虚拟机》。

JVM虚拟机

1 Java 内存区域

1.1 Java 内存区域模型

1.1.1 程序计数器

记录程序字节码执行到某个位置的功能,方便线程切换之后再切换回来能从上次停止的地方继续执行。此区域为线程独有的内存区域。此区域为 Java 虚拟机规范中定义的唯一不会出现 OOM 的区域。

1.1.2 Java虚拟机栈

每执行一个 Java 方法,虚拟机就会创建一个栈帧。栈帧中包含局部变量表,操作数栈,动态链接和方法出口。局部变量表中包含基本数据类型和对象引用。此区域为线程独有的内存区域。当栈帧超过虚拟机可容纳的最大值时,会出现 StackOverFlow 异常。

局部变量表中以变量槽(slot)为最小单位,只有long和double占据2个slot,其余基本数据类型均占1个slot。虚拟机通过索引定位局部变量表。正常方法从索引0出开始,第0位为this。静态方法从索引1出开始定位。

1.1.3 本地方法栈

本地方法栈和Java虚拟机作用相似。一个是针对Java方法,一个是针对native方法。

1.1.4 Java堆

Java堆中存放着对象实例,为线程公有的内存区域。

1.1.5 方法区

线程公有的内存区域,存储着类加载器加载的类信息,常量及静态变量。

1.2 创建对象

  • 检测类加载器是否加载了这个类,没有加载则进行类加载过程。
  • 为对象实例在Java堆上分配空间。 空间如果是规整的,采用指针碰撞进行分配,如果不规整,采用空闲列表分配。
  • 虚拟机将分配的内存空间初始化零值。
  • 执行 方法对对象实例进行赋值。
  • 对象引用指向对象实例。

1.3 对象的内存布局

对象包含对象头,实例数据和对齐填充。

对象头包含(Mark Word) 哈希值信息,分代年龄,GC标志,线程持有的锁(偏向锁),锁状态(用来记录当前处于锁升级的何种阶段),锁偏向id(上一个持有偏向锁的线程Id)。 对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

1.4 对象的访问定位

句柄和直接指针。直接指针访问是速度快,只有一次定位时间。但是GC时对象移动需要重新修改,而句柄只需要修改句柄中的实例数据指针,reference本身不需要修改。

2. 垃圾收集器和内存分配策略

三个问题。

  • 哪些对象需要回收?
  • 什么时候回收?
  • 如何回收?

2.1 判断对象是否需要回收

可采用 引用计数法和可达性分析算法。

2.1.1 引用计数法

对象被引用一次那么数量加1,移除引用的时候数量减1,当计数为0的时候说明对象可回收。但没办法解决相互引用的问题。

2.1.2 可达性分析算法

对象从GC Root出发,在引用链上的对象都不能进行回收。当对象不在引用链上,那么对象属于可回收状态。 GC Root: 1. Java虚拟机栈中引用的对象。 2. 常量引用的对象 3. 静态变量引用的对象 4. 本地方法栈中引用的对象

2.2 GC算法

GC算法分为三种,其中针对新生代老年代采用不同算法。

2.2.1 标记清除算法

对回收对象进行标记,然后将标记的对象进行GC。

2.2.2 复制算法

将存活的对象复制到内存的另一侧,然后将这一侧的内存回收。

2.2.3 标记整理算法

第一步和标记清除的第一步一样,标记出需要回收的对象,然后将存活的对象统统移动到内存的一侧,然后清理端边界之外的内存。

2.2.4 分代收集算法

由于新生代的特性是对象回收的多,存活的少。而老年代的对象经过GC后存活的多。所以新生代适用复制算法,老年代适合标记清除或标记整理算法。

3. 虚拟机类加载机制

3.1 类加载过程

一共有七步。分别是 加载,验证,准备,解析, 初始化, 使用 和 卸载。

3.2 双亲委派模型

引导类加载器,扩展类加载器,应用程序类加载器,自定义加载器。

双亲委派模型,要求先查该类是否已经被加载过了,如果没有加载过,那么首先子加载器不加载,传递给父加载器进行加载。如果到最上层引导类加载器还没有加载过该类,那么引导类加载器试图加载此类,如果父类加载器无法加载时,再传递给子加载器进行加载。

4. Java内存模型与线程

4.1 Java 内存模型

  1. 主内存和工作内存 所有变量都存储在主内存中,每个线程有自己的工作内存。线程从主内存中将变量值取到工作内存中,然后在工作内存中进行使用,赋值等,最后将工作内存中值再写入到主内存中。其中一共涉及到八个步骤,每个步骤操作均为原子性。
    Lock,Read, Load, Use, Assign, Store, Write, UnLock。

4.2 volatile

有两个特性,一个是 可见性,保证此变量对所有线程的可见性。也就是被 volatile 修饰的变量被一个线程修改之后,对于其他线程是立刻可以得知的。第二个是禁止指令重排。

4.3 Java 线程

实现线程有3个方式,内核线程、用户线程、用户线程和轻量级进程实现。Java 线程是映射到操作系统线程的。

5. 线程安全与锁优化

5.1 线程安全的实现方法。

  • 互斥同步 Synchronized就是互斥同步。 synchronized 是可重入的。互斥同步是悲观的。 synchronized 和 ReentrantLock 区别。
    1. ReentrantLock 是等待可中断,而Synchronized不可以。等待可中断是线程在等待持有锁的时候可以选择放弃等待,改做其他事情。
    2. ReentrantLock 可实现乐观锁。而Synchronized 是悲观锁。
    3. ReentrantLock 可绑定多个锁条件。Synchronized 中,锁对象的wait() notify()或 notifyAll() 只能实现一个。
  • 非阻塞同步 CAS 就是非阻塞同步。CAS为线程中变量的值V,旧的预期值为A,新值为B。当且仅当V如何旧值预期A时,将V值更新为B。否则就不执行操作。CAS会出现ABA问题,解决的方式可以通过添加版本号。

Synchronized是可重入的。可重入的意思是在代码执行的任何时候中断它,转而去执行另外一段代码,在控制权返回后,原来的程序不会出现错误。
可重入一定线程安全,但线程安全不一定可重入。

5.2 锁优化

首先锁是无锁,碰到多线程时升级为偏向锁,如果当前线程和Mark Word中存储的锁偏向id不一致时,升级为轻量级锁,也即CAS去进行更新,失败的话则自选等待。自璇失败到一定次数时,升级为重量级锁。

Search

    Table of Contents