深入理解 Java 虚拟机

深入理解Java虚拟机

第一部分:自动内存管理机制

一. 运行时数据区域

1. 程序计数器

程序计数器可以看作当前线程执行的字节码的行号指示器
  • 如果线程正在执行的是一个 Java 方法,这个计数器记录的是当前线程正在执行的字节码的指令的地址
  • 如果线程正在执行的是一个 Native 方法,这个计数器则为空
  • 这个内存区域是唯一一个没有规定 OutOfMemoryError 情况的区域

2. Java 虚拟机栈

JVM 栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack 
Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈出栈的过程
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError
  • 如果虚拟机扩展时无法申请到足够的内存,将抛出 OutOfMemoryError

3. 本地方法栈

Java虚拟机栈为虚拟机执行 java 方法(也就是字节码) 服务,而本地方法栈则是为虚拟机使用到的 
Native 方法服务
  • Native 方法:调用非 java 代码的接口

4. Java 堆

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存
  • Java 堆是垃圾收集器管理的主要区域
  • Java 堆可以处于物理上不连续的内存空间上,但逻辑上一定要连续
  • Java堆又分为: 新生代、老年代
  • 如果在 java 堆中没有内存完成实例分配,并且堆也无法再扩展,将抛出 OutOfMemoryError

5. 方法区

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
方法区又叫永久代
运行时常量池
用于存放编译期生成的各种字面量和符号引用

6. 直接内存

在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储
在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著
提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

二. 垃圾回收机制 (如何进行垃圾回收?)

垃圾收集主要是针对堆和方法区进行。

程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束
之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。

1. 判断一个对象是否可被回收

(1)引用计数法

  • 描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

  • 缺陷:很难解决对象间相互循环引用的问题
    内存回收中引用计数算法的循环引用问题

(2) 可达性分析算法

将 GC Roots 作为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。

Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

(3)方法区的回收

主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射、动态代理的场景都需要虚拟机具备类卸载功能。

类的卸载条件很多,需要满足以下三个条件,但是满足了也不一定会被卸载:

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

(4)finalize

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重
新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后
面回收时不会再调用该方法。

2. 垃圾收集算法—分代收集

一般将堆分为新生代和老年代。

  • 新生代使用:复制算法
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,每次使用 Eden 和其中一块
Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor,保证了内存的利用率达到 90%。如果每次回收有多于 10%的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年
代的空间存储放不下的对象。
  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法

3. 垃圾收集器–G1收集器

三. 内存分配与回收策略 (什么时候进行垃圾回收?)

1. Minor GC 和 Full GC

  • Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度
    一般也会比较快。

    • 新生代中的垃圾收集动作,采用的是复制算法
    • 对于较大的对象,在 Minor GC 的时候可以直接进入老年代
  • Full GC:发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

    • Full GC 是发生在老年代的垃圾收集动作,采用的是 标记-清除/整理 算法。
    • 由于老年代的对象几乎都是在 Survivor 区熬过来的,不会那么容易死掉。因此 Full GC 发生的次数不会有 Minor GC 那么频繁,并且 Time(Full GC)>Time(Minor GC)

2. 内存分配策略

  • 对象优先在 Eden 分配,Eden空间不够,发起 Minor GC.

  • 大对象直接进入老年代,避免在 Eden 区和 Survivor 区之间的大量内存复制。大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

  • 长期存活的对象进入老年代

  • 动态对象年龄判定:如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代。

  • 空间分配担保

    使用复制算法的 Minor GC 需要老年代的内存空间作担保, 在 Minor GC 之前,虚拟机先检查老年代
    最大可用的连续空间是否大于新生代所有对象总空间,如果大于,那么 Minor GC 可以确认是安全的。
    如果小于,虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会
    继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进
    行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一
    次 Full GC。
    

3. 什么时候触发Full GC?

  • 调用 System.gc() 但不一定触发

  • 老年代空间不足

  • 空间担保失败

  • JDK 1.7 及以前的永久代空间不足

  • Concurrent Mode Failure

四. 虚拟机性能监控与故障处理工具

JDK 本身提供了很多方便的 JVM 性能调优监控工具,除了 jps、jstat、jinfo、jmap、jhat、jstack 等小巧的工具,还有集成式的 jvisualvm 和 jconsole。

1. jps

jps(JVM Process Status Tool,虚拟机进程监控工具),这个命令可以列出正
在运行的虚拟机进程,并显示虚拟机执行主类名称,以及这些进程的本地虚拟机唯一
ID。

语法格式

jps [options] [hostid]

options参数

-q 不输出类名、Jar名和传入main方法的参数
-m 输出传入main方法的参数
-l 输出main类或Jar的全限名
-v 输出传入JVM的参数

2. jstat

jstat(JVM Statistics Monitoring Tool,虚拟机统计信息监视工具),这个
命令用于监视虚拟机各种运行状态信息。它可以显示本地或者远程虚拟机进程中的类装
载、内存、垃圾收集、JIT编译等运行数据,是运行期间定位虚拟机性能问题的首选工
具。

需要每 1000 毫秒查询一次进程 3692 垃圾收集状况,一共查询 10 次

jstat -gcutil 3692 1000 10

3. jinfo

jinfo (Configuration Info for Java,配置信息工具) 这个命令可以实时地	查看和调整虚拟机各项参数。

4. jmap

5. jhat

6. jstack

jstack(Java Stack Trace,Java堆栈跟踪工具),这个命令用于查看虚拟机当
前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集
合。

7. jconsole

JConsole 中,您将能够监视 JVM 内存的使用情况、线程堆栈跟踪、已装入的类和 VM 信息以及 CE MBean。

jconsole:一个 java GUI 监视工具,可以以图表化的形式显示各种数据。并可通	过远程连接监视远程的服务器VM。

8. jvisualvm

jvisualvm 同 jconsole 都是一个基于图形化界面的、可以查看本地及远程的
JAVA GUI 监控工具,Jvisualvm 同 jconsole 的使用方式一样,直接在命令行
打入 jvisualvm 即可启动,jvisualvm 界面更美观一些,数据更实时

五. 内存泄露

1. 什么是内存泄露?

在 Java 中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点

  • 这些对象是可达的,即在有向图中,存在通路可以与其相连;
  • 这些对象是无用的,即程序以后不会再使用这些对象。

2. 内存泄漏典型例子

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

分析:Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。

3. 内存泄露查询工具

  • MemoryAnalyzer
  • EclipseMAT
  • JProbe

第二部分:虚拟机执行子系统

一. 虚拟机类加载机制

1. 类加载过程

(1)加载
Step1: 通过一个类的全限定名来获取定义此类的二进制字节流
Step2: 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构
Step3: 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据
       的访问入口
(2)验证

目的:确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

文件格式验证
元数据验证
字节码验证
符号引用验证
(3)准备
准备阶段正式为类变量(static修饰的变量)分配内存并设置变量的初始值。这些变
量使用的内存都将在方法区中进行分配。
  • 默认值:int 0, boolean false, float 0.0, char ‘0’, 抽象数据类型 null
public static int value = 123; //value 被初始化为 0 而不是 123
  • 对于 static final 类型,在准备阶段就会被赋上正确的值
public static final int value = 123;//value 被初始化为123
(4)解析

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

  • 符号引用:符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。

  • 直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,引用的目标对象必定已经在内存中存在。

(5)初始化

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 () 方法的过程。

在准备阶段,类变量(static变量)已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

2. 类初始化时机

3. 类加载器

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

(1)类与类加载器
  • 两个类相等:只有被同一个类加载器加载的类才可能会相等。相同的字节码被不同的类加载器加载的类不相等。
(2)类加载器分类

从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
  • 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象
    类 java.lang.ClassLoader。

从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用程序类加载器(Application ClassLoader)
  • 自定义类加载器
(3)双亲委派模型

为什么要使用双亲委派模型?

主要是为了避免重复加载的问题

双亲委派概念

子加载器通过组合来复用父加载器的代码,而不是使用继承。

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这
个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请
求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加
载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。

第三部分:高效并发

这部分介绍虚拟机如何实现多线程、多线程之间由于共享和竞争数据而导致的一系列问
题以及解决方案。

一. Java 内存模型

Java 虚拟机规范中试图定义一种 java 内存模型来屏蔽掉各种硬件和操作系统的内
存访问差异,以实现让 java 程序在各种平台下到达一致的内存访问效果。

Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。JMM是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特性来建立的。

这里的变量指的是线程共享的变量,包括实例字段、静态字段和构成数组对象的元素。

1. 主内存与工作内存

  • Java 内存模型规定了所有的变量都存储在主内存(Main Memory)。
  • 每条线程有自己的工作内存(Work Memory)
  • 主内存就对应于物理硬件的内存;工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问的就是工作内存。

2. 内存间交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内
存,如何从工作内存同步回主内之类的细节实现,java内存模型定义了八种操作:(这
八个操作都具有原子性)
  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态。

  • unclock(解锁):作用于主内存的变量,把一个处于锁定的状态的变量释放出来。

  • read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中

  • load(载入):作用于工作内存的变量,把read操作从主内存得到的变量值放入工作内存的变量副本中。

  • use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

  • assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值 赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传递到主内存,以便write操作使用。

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

3. 对于 volatile 变量的特殊规则

关键字 volatile 可以说是Java虚拟机提供的最轻量级的同步机制。

规则总结:

  • 在工作内存中,每次使用volatile类型变量前都要先从主内存刷新最新的值,用于保证能看见其它线程对变量V所做的修改后的值。

  • 在工作内存中,每次修改volatile类型变量后都立刻同步到主内存中,用于保证其它线程看到自己对变量所做的修改。

  • 对volatile变量的修改不会被指令重排序优化 ,保证代码的执行顺序与程序的顺序相同。

两个特点:

  • 保证此变量对所有线程的可见性。但是 Java 里面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的。运算场景一定要符合下面的条件才能使用volatile:

    • (1) 运算结果并不依赖于变量的当前值,或者能够确保只有单一的线程修改变量的值 。
    • (2) 变量不需要与其它状态变量共同参与不变约束。
  • 禁止指令重排序优化。

4. 对于 long 和 double 型变量的特殊规则

允许虚拟机将没有被volatile修饰的64位数据读写操作划分为2次32位的操作来进
行,即允许虚拟机实现选择可以不保证64位数据类型的read、load、store、write
这4个操作的原子性。这点就是所谓的long和double的非原子协定。

目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,
因为我们再编写代码时无需把long和double类型变量专门声明为volatile。

5. 原子性、可见性、有序性

  • 原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

    • 由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store、write,我们大致认为基本数据类型的访问读写是具备原子性的。
    • lock 和 unlock 对应的synchronized关键字也具备原子性。
  • 可见性:可见性指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

    • 普通变量和volatile变量都可以保证变量的可见性,区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,所以yvolatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点。
    • 除了volatile之外,java中还有2个关键字能实现可见性,即synchronized和final(final修饰的变量,线程安全级别最高)。

6. 先行发生原则(happens-before)

它是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是JMM中定义的两项
操作之间的偏序关系。

下面这些先行发生关系无需任何同步协助器:

  • 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。

  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。

  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(即先中断,后发现被中断),可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 传递性:若操作A先行发生于操作B,操作B先行发生于操作C,那么就可以得出操作A先行发生于操作C。

二. 线程安全

1. Java语言中的线程安全

Java语言中各种操作共享的数据分为以下5类:

  • 不可变: 不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要采取任何的线程安全保障措施。

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

  • 相对线程安全:相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要额外的保障,但是对于特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

  • 线程兼容:对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。

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

2. 线程安全的实现方法

(1)互斥同步(阻塞同步)

同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)使用,而互斥是实现同步的一种手段。

互斥同步属于一种悲观的并发策略,最主要的问题就是进行线程阻塞和唤醒所带来的性能问题。

  • synchronized

  • RetrantLock (java.util.courrent)

(2)非阻塞同步

非阻塞同步属于一种了乐观的并发策略。

  • CAS
(3)无同步方案

无同步方案就是避免共享数据,天生就是线程安全的。

  • 可重入代码:如果一个方法,它的结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,就是线程安全的。

  • 线程本地存储

    java.lang.ThreadLocal

Student s = new Student(); 在内存中做了哪些事情

  1. 加载 Student.class 文件进内存
  2. 在栈内存为 s 开辟空间
  3. 在堆内存为 Student 对象开辟空间
  4. 对 Student 对象的成员变量进行默认初始化
  5. 对 Student 对象的成员变量进行显示初始化
  6. 通过构造方法对 Student 对象的成员变量赋值
  7. Student 对象初始化完毕,把对象地址赋值给 s 变量

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值