说明

讲师:李智慧

JVM虚拟机原理

JVM 组成架构

Java 是一种跨平台的语言,JVM 屏蔽了底层系统的不同,为Java 字节码文件构造了一个统一的运行环境。
在这里插入图片描述

Windows, Mac OS, Android OS每个不同的环境下,执行引擎会不一样, 实现跨平台。

Tomcat 启动命令

java org.apache.catalina.startup.Bootstrap "@" start

当启动Tomcat之前,先会初始化JVM 虚拟机,Java虚拟机会启动一个主线程,Bootstrap会执行main方法,在方法区。 new 出来的对象A,是放在堆中的,创建对象是由执行引擎处理的。

Java字节码文件

Java 如何实现在不同操作系统、不同硬件平台上,都可以不用修改代码就能顺畅地执行?

计算机领域的任何问题都可以通过增加个中间层(虚拟层)来解决。

Java 所有的指令有 200 个左右,一个字节(8位)可以存储256种不同的指令信息,一个这样的字节称为字节码(Bytecode). 在代码的执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖,JVM也可以将字节码编译执行,如果是热点代码,会通过JIT(just-in-time compilation即时编译)动态地编译为机器码,提高执行效率。
在这里插入图片描述
cafe babe 表示Java的字节码。

字节码执行流程

在这里插入图片描述
热点代码就提交编译,达到复用的效果。

Java 字节码文件编译过程

在这里插入图片描述

类加载器的双亲委托模型

低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,需要上级类加载器确认。只有当上级类加载器没有加载过这个类,也允许加载的时候,才让当前类加载器加载这个未知类。

在这里插入图片描述

自定义类加载器

  1. 隔离加载类:同一个JVM中不同组件加载同一个类的不同版本。
  2. 扩展加载源:从网络、数据库等处加载字节码。
  3. 字节码加密:加载自定义的加密字节码,在ClassLoader中解密。
    在这里插入图片描述

堆 & 栈

堆:每个JVM实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享。

堆栈:JVM为每个新创建的线程都分配一个堆栈。也就是说,对于一个 Java 程序来说,它的运行就是通过对堆栈的操作来完成的。

Java 中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在创建一个对象时从两个地方都分配内存。在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的引用而已。

方法区 & 程序计数器

方法区主要存放从磁盘加载进来的字节码,而在程序运行过程中创建的类实例则存放在堆里。程序运行的时候,实际上是以线程为单位运行的,当JVM进入启动类的 main 方法的时候,就会为应用程序创建一个主线程。 main 方法里的代码就会被这个主线程执行,每个线程有自己的 Java 栈,栈里存放着方法运行期的局部变量。而当前线程执行到哪一行字节码指令,这个信息则被存放在程序计数寄存器。
在这里插入图片描述

在这里插入图片描述
静态变量、静态方法放在方法区,只会有一个。

Java (线程)栈

所有在方法内定义的基本数据类型变量,都会被每个运行这个方法的线程放入自己的栈中,线程的栈彼此隔离,所以这些变量一定是线程安全的。
在这里插入图片描述

线程工作内存 & volatile

Java 内存模型规定在多线程情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行。

一个共享变量(类的成员变量、类的静态成员变量)被volatile 修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其它线程来说是可见的。
  2. 禁止进行指令重排序。
    在这里插入图片描述
    工作内存:CPU的cache,寄存器。
    用volatile修饰的变量,当该变量被修改时,确保数据刷到主内存中。保证数据的可见性,并不能保证数据的安全性,还是得需要锁。

Java 运行环境

在这里插入图片描述

JVM 的垃圾回收

JVM 垃圾回收即使将 JVM 堆中的已经不再被使用的对象清理掉,释放宝贵的内存资源。

JVM 通过一种可达性分析算法进行垃圾对象的标识。具体过程是:

  1. 从线程栈帧中的局部变量,或者方法区的静态变量出发,将这个变量引用的对象进行标记;
  2. 然后看这些被标记的对象是否引用了其它对象,继续进行标记;
  3. 所有被标记过的对象都是被使用的对象,而那些没有被标记的对象就是可回收的垃圾对象了。

进行完标记以后,JVM 就会对垃圾对象占用的内存进行回收,回收主要有三种方法:

  1. 清理:将垃圾对象占据的内存清理掉,其实 JVM 并不会真的将这些垃圾内存进行清理,而是将这些垃圾对象占用的内存空间标记为空闲,记录在一个空闲的列表里,当应用程序需要创建新的对象的时候,就从空闲列表中找一段空闲内存分配给这个对象。
  2. 压缩:从堆空间的头部开始,将存活的对象拷贝到一段连续的内存空间中,那么其余的空间就是连续的空闲空间。
  3. 复制:将堆空间分成两部分,只在其中一部分创建对象,当这个部分空间用完的时候,将标记过的可用对象复制到另一个空间中。
    在这里插入图片描述
    清理:把已经没用的空间标记为可用空间。
    压缩:清理有个短板,空间不是连续的,压缩就是把还在用的copy放在前面。
    复制:压缩还是需要成本的,速度较慢;分两块区域会更好一点。

JVM 分代垃圾回收

在这里插入图片描述

  1. 创建新对象在Eden区,当Eden区满了以后,就会进行Young GC,把在用的对象copy到From区;
  2. 当Eden区再次满了以后,再次进行YoungGC,把Eden区和From区的在用对象,copy到To区;
  3. 当多次Eden发送YoungGC,把一些还用的对象放到老年代;
  4. 当老年代满了以后,就会进行Full GC。这是要应用线程都要暂停,进行Full GC。

JVM垃圾回收器算法

在这里插入图片描述

  1. 串行回收器:当JVM空间满了以后,stop-the-world 停止所有应用程序的线程,进行垃圾回收。(以前的都是单CPU)
  2. 并行回收器:多CPU时代,当JVM空间满了以后,stop-the-world 多线程进行垃圾回收。(大数据用这种方式,效率比较高)
  3. 并发回收器CMS:初始化标记静态变量等以及引用的对象 > 并发标记 > 重标记(永远表不完对象,因为对象一直在创建。所以这个阶段也会stop-the-world) > 并发清理。(早期Web应用都是用这种方式,但是浪费计算资源比较多。)
  4. G1回收器(主流):效率更高,下面详细介绍。每次stop-the-world的时候,只会清理一部分区域,控制范围更小一些。

G1垃圾回收内存管理机制

在这里插入图片描述
把大的内存分为2000个小块。
配置参数: -XX:MaxGCPauseMillis

Java 启动参数

标准参数,所有的JVM 实现都必须实现这些参数的功能,而且向后兼容:

  • 运行模式: -server, -client
  • 类加载路径: -cp, -classpath
  • 运行调试: -verbose
  • 系统遍历: -D

非标准参数,默认 JVM 实现这些参数,但不保证所有 JVM 实现都实现,且不保证向后兼容:

  • -Xms 初始堆大小
  • -Xmx 最大堆大小
  • -Xmn 新生代大小
  • -Xss 线程堆栈大小

一般 -Xms-XMx 都是配置一样的。

非 Stable 参数,此类参数各个 JVM 实现会有所不同,将来可能会随时取消

  • -XX:-UseConcMarkSweepGC 启用 CMS 垃圾回收

JVM 性能诊断工具

基本工具: JPS, JSTAT, JMAP, JSTACK
集成工具: JConsole, JVisualVM

JPS

JPS 用来查看 host 上运行的所有 java 进程 pid (jvmid), 一般情况下所用这个工具的目的只是为了找出运行 JVM 继承 ID,即 Ivmid,然后可以进一步使用其它的工具来监控和分析 JVM。

常用的几个参数:

  • -l 输出 java 应用程序的 main class 的完整包
  • -q 仅显示 pid,不显示其它任何相关信息
  • -m 输出传递给 main 方法的参数
  • -v 输出传递给 JVM 的参数。在诊断 JVM 相关问题的时候,这个参数可以查看 JVM 相关参数设置。

JSTAT

JSTAT (Java Virtual Machine statistics monitoring tool) 是 JDK 自带的一个轻量级小工具。主要对 Java 应用程序的资源和性能进行实时的命令行的监控,包括了对 Heap size 和垃圾回收情况的监控。

语法结构如下:jstat [Options] vmid [interval] [count]

  • Options – 选项,我们一般使用 - gcutil 查看 gc 情况。
  • vmid – VM 的进程号,即当前运行的 java 进程号。
  • interval – 间隔时间,单位为毫秒。
  • count – 打印次数,如果缺省则打印无数次。
    在这里插入图片描述
    参数解析:
  • S0 – Heap 上的 Survivor sapce 0 区已经使用空间的百分比;
  • S1 – Heap 上的 Survivor sapce 1 区已经使用空间的百分比;
  • E – Heap 上的 Eden sapce 区已经使用空间的百分比;
  • O – Heap 上的 Old sapce 区已经使用空间的百分比;
  • YGC – 从应用程序启动到采样时发生 Young GC 的次数;
  • YGCT – 从应用程序启动到采样时发生 Young GC 所用的时间(单位秒);
  • FGC – 从应用程序启动到采样时发生 Full GC 的次数;
  • FGCT – 从应用程序启动到采样时发生 Full GC 所用的时间(单位秒);
  • GCT – 从应用程序启动到采样时用于垃圾回收的总时间(单位秒).

JMAP

JMAP 是一个可以输出所有内存中对象的工具,甚至可以将 JVM 中的 heap,以二进制输出成文本。

使用方法:

  • jmap -histo pid>a.log 可以将其保存到文本中去,在一段时间后,使用文本对比工具,可以对比出GC回收了哪些对象。
  • jmap -dump:format=b,file=f1 PID 可以将该 PID 进程的内存 heap 输出出来到 f1 文件里。

JSTAK

Jstack 可以查看jvm内的线程堆栈信息
在这里插入图片描述

JCONSOL

在这里插入图片描述

JVISULVM

在这里插入图片描述

Java代码优化

合理并谨慎使用多线程

  • 使用场景(I/O 阻塞,多 CPU 并发)
  • 资源争用与同步问题
  • java.util.concurrrent

启动线程数 = [任务执行时间 / (任务执行时间 - IO 等待时间)] * CPU 内核数

  • 最佳启动线程数和 CPU 内核数量成正比,和 IO 阻塞时间成反比。如果任务都是 CPU 计算型任务,那么线程数最多不超过 CPU 内核数,因为启动再多线程, CPU 也来不及调度;
  • 相反如果是任务需要等待磁盘操作,网络响应,那么多启动线程有助于提高任务并发度,提高系统吞吐能力,改善系统性能。

竞态条件与临界区

在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就存在竞态条件。导致竞态条件发生的代码区称作临界区。

在临界区中使用适当的同步就可以避免竞态条件。

Java 线程安全

允许被多个线程安全执行的代码称作线程安全的代码。

  • 方法局部变量:局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以基础类型的局部变量是线程安全的。
  • 方法局部的对象引用:如果在某个方法中创建的对象不会逃逸出该方法,那么它就是线程安全的。
  • 对象成员变量:对象成员存储在对上。如果两个线程同时更新同一个对象的同一个成员,那么这个代码就不是线程安全的。

问题

  1. Java Web 应用的多线程从哪儿来的?
    多线程来自于容器。

  2. Servlet 是线程安全的吗?
    不安全回答:Servlet是单实例的。所以多个线程访问的时候,就会访问堆中的变量,所以是不安全的。
    安全回答:一个Servlet本身是安全,但是你写了一个不安全的类,那么就是不安全的。

ThreadLocal

在这里插入图片描述

ThreadLocal 每个线程都单独维护自己的值

  • 创建一个 ThreadLocal 变量(X类静态成员变量):public static ThreadLocal myThreadLocal = new ThreadLocal();
  • 存储此对象的值(A类 a方法):X.myThreadLocal.set("A thread local value");
  • 读取一个 ThreadLocal 对象的值 (B类 b 方法):String threadLocalValue = (String)X.myThreadLocal.get();

ThreadLocal 的值都放在堆里面(既是共享的),每个线程都单独维护自己的值(又是独享的).
这是怎么办到的? 看看源码:

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap = map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}

void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

t是线程的成员变量,所以获取到的就是自己的。

public Thread getMap(Thread t) {
	return t.threadLocals;
}

Java 内存泄漏

Java 内存泄漏是由于开发人员的错误引起的。

如果程序保留对永远不再使用的对象的引用,这些对象将会占用并耗尽对象。

  • 长生命周期对象;
  • 静态容器;
  • 缓存。

解决办法:创建弱引用对象。

合理使用线程池和对象池

  • 复用线程或对象资源,避免在程序的生命周期中创建和删除大量对象。
  • 池管理算法(记录哪些对象是空的)

合理使用线程池和对象池

  • 复用线程或对象资源,避免在程序的生命周期中创建和删除大量对象。
  • 池管理算法(记录哪些对象是空闲的,哪些对象正在使用)。
  • 对象内容清除(ThreadLocal 的清空)。

使用合适的 JDK 容器类 (顺序表,链表,Hash)

  • LinkList和ArrayList 的区别及适用场景。
  • HashMap 的算法实现及应用场景。
  • 使用 concurrent 包,ConcurrentHashMap 和 HashMap 的线程安全特性有什么不同?

缩短对象生命周期,加速垃圾回收

  • 减少对象驻留内存的时间。
  • 在使用时创建对象,用完释放。
  • 创建对象的步骤(静态代码段 - 静态成员变量 - 父类构造函数 - 子类构造函数)

不用的对象,把对象设置为null,这样在在GC的时候就会被回收。

public class StaticClass {
	static {
		A a= new A();
	}
	
	public static void cc() {
		System.out.println("cc()");
	}
}

使用 I/O buffer 及 NIO

  • 延迟写与提前读策略
  • 异步无阻塞 IO 通信

优先使用组合代替继承

  • 减少对象耦合
  • 避免太深的继承层次带来的对象创建性能损失。

合理使用单例模式

  • 无状态对象
  • 线程安全

虚拟化所有层次

  • 计算机的任何问题都可以通过间接层解决。
  • 一致性 Hash 算法的虚拟化实现。
  • 面向接口编程。
  • 7层网络协议。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐