一、JVM架构

JVM概述 - 图1

二、字节码文件

JVM概述 - 图2

2.1前端编译器

前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM探范的字节码文件。javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。

2.1.1 javac(IDEA默认使用的)

  1. javac是一种能够将Java源码编译为字节码的前端编译器

2.1.2 ECJ编译器

  1. Java的前端编译器领域,除了javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的EC(Eclipse Compiler for Java)编译器。和Javac的全量式编译不同,ECJ是一种增量式编译器。

JVM概述 - 图3

2.3Class的对象

  1. (1) class:
  2. 外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
  3. ( 2 interface:接口
  4. (3)[]:数组
  5. (4) enum:枚举
  6. (5 annotation:注解@interface
  7. (6) primitive type:基本数据类型
  8. ( 7) void

i++的过程

  1. bipush 10 10放入操作数栈
  2. istore_1 将栈中值放入局部变量表下标为1的位置
  3. iload_1 将局部变量表中的值放入栈中
  4. iinc 1 by 1 将局部变量表中的值加1
  5. istore_1 将栈中的值放入局部变量表中

JVM概述 - 图4

  1. static void test2(){
  2. Integer a = 128;
  3. Integer b = 128;
  4. log.info("输出的结果为{}",a == b);//false -128~127都会存入缓存中
  5. }

JVM概述 - 图5

  1. public void test3(){
  2. Integer a = 4;
  3. int b = 4;
  4. log.info("是否相等{}",a == b );//true Integer会自动进行拆箱(能拆不装)
  5. }

JVM概述 - 图6

2.4class文件的组成部分(16进制)

  1. 进制在线转换
  2. https://www.sojson.com/hexconvert.html
  3. 魔数
  4. Class文件版本
  5. 常量池
  6. 访问标识(或标志)
  7. 类索引,父类索引,接口索引集合
  8. 字段表集合
  9. 方法表集合
  10. 属性表集合

JVM概述 - 图7
常量池

  1. 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。
  2. 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
  3. 字面量:1.文本字符串 2.声明为final的常量值
  4. 符号引用: 1.类和接口的全限定名 2.字段的名称和描述符 3.方法的名称和描述符

常量类型和结构

  1. 常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。如下表格所示:

JVM概述 - 图8
字节码指令

  1. https://blog.csdn.net/qq_33521184/article/details/105622903?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163939550716780274184839%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163939550716780274184839&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-105622903.first_rank_v2_pc_rank_v29&utm_term=java%E5%AD%97%E8%8A%82%E7%A0%81%E6%8C%87%E4%BB%A4&spm=1018.2226.3001.4187

java数据类型
JVM概述 - 图9

三、类加载器

3.1类的加载过程

JVM概述 - 图10

3.1.1 Loading(装载)阶段

  1. Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
  2. 步骤:
  3. 1.通过类的全名,获取类的二进制数据流。
  4. 2.解析类的二进制数据流为方法区内的数据结构(Java类模型)
  5. 3.创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
  6. 类模型的位置
  7. 加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;JDK1.8及之后元空间)。
  8. Class实例的位置
  9. 类将.class文件加载至元空间后,会在堆中创建一个Java.lang.class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个class类型的对象。

JVM概述 - 图11

3.1.2 Linking(连接)阶段
①验证

  1. 保证加载的字节码是合法、合理并符合规范的

JVM概述 - 图12
②准备

  1. 为类的静态变量分配内存,并将其初始化为默认值。
  2. 需要注意的点:
  3. 1.这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。
  4. 2.注意这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。
  5. 3.在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。

③解析

  1. 将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或"偏移量。如果直接引用存在,那么可以肯定系统中存在该类、方法或者段。但只存在夺号引用,不能确定系统中一定存在该结构。

JVM概述 - 图13
3.1.3 Initialization(初始化)阶段

  1. 为类的静态变量赋予正确的初始值,以及加载静态代码块。执行类的初始化方法:<clinit>()方法
  2. <clinit>():只有在给类的中的static的变量显式赋值或在静态代码块中赋值了。才会生成此方法。
  3. <init>():一定会出现在Classmethod表中。

不会有()的类

  1. public class Test {
  2. //场景1:对于非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
  3. public int num = 1;
  4. //场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
  5. public static int num1;
  6. //场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
  7. public static final int num2 = 1;
  8. }

()产生死锁的问题

  1. class StaticA {
  2. static {
  3. try {
  4. Thread.sleep(1000);
  5. } catch (InterruptedException e) {
  6. }
  7. try {
  8. Class.forName("com.atguigu.java.StaticB");
  9. } catch (ClassNotFoundException e) {
  10. e.printStackTrace();
  11. }
  12. System.out.println("StaticA init OK");
  13. }
  14. }
  15. class StaticB {
  16. static {
  17. try {
  18. Thread.sleep(1000);
  19. } catch (InterruptedException e) {
  20. }
  21. try {
  22. Class.forName("com.atguigu.java.StaticA");
  23. } catch (ClassNotFoundException e) {
  24. e.printStackTrace();
  25. }
  26. System.out.println("StaticB init OK");
  27. }
  28. }
  29. public class StaticDeadLockMain extends Thread {
  30. private char flag;
  31. public StaticDeadLockMain(char flag) {
  32. this.flag = flag;
  33. this.setName("Thread" + flag);
  34. }
  35. @Override
  36. public void run() {
  37. try {
  38. Class.forName("com.atguigu.java.Static" + flag);
  39. } catch (ClassNotFoundException e) {
  40. e.printStackTrace();
  41. }
  42. System.out.println(getName() + " over");
  43. }
  44. public static void main(String[] args) throws InterruptedException {
  45. StaticDeadLockMain loadA = new StaticDeadLockMain('A');
  46. loadA.start();
  47. StaticDeadLockMain loadB = new StaticDeadLockMain('B');
  48. loadB.start();
  49. }
  50. }

3.2类的加载器

  1. 类的加载器只在loading阶段,只能影响到类加载的第一个阶段
  2. 显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用class.forName(name)或this.getClass().getClassLoader( ).loadClass()加载class对象。
  3. 隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

JVM概述 - 图14
3.2.1启动类加载器(引导类加载器)

  1. 1.这个类加载使用C/C++语言实现的,嵌套在VM内部。
  2. 2.它用来加载]ava的核心库(JAVA_HOME/jre/lib/rt.jarsun.boot.class.path路径下的内容)。用于提供VM自身需要的类。
  3. 3.并不继承自java.lang.ClassLoader,没有父加载器。
  4. 4.出于安全考虑,Bootstrap启动类加载器只加载包名为javajavaxsun等开头的类
  5. 5.加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  6. 使用以下参数配置会打印被加载的类()
  7. -XX:+TraceClassLoading

JVM概述 - 图15
3.2.2拓展类加载器

  1. 1.Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。继承于classLoader
  2. 2.父类加载器为启动类加载器
  3. 3.java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

3.2.3系统类加载器

  1. 1.java语言编写,由sun.misc.Launcher$AppClassLoader实现继承于ClassLoader
  2. 2.父类加载器为扩展类加载器
  3. 3.它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库应用程序中的类加载器默认是系统类加载器。
  4. 4.它是用户自定义类加载器的默认父加载器
  5. 5.通过classLoadergetSystemClassLoader()方法可以获取到该类加载器

3.2.4自定义类加载器

  1. 1.通过类加载器可以实现非常绝妙的插件机制,如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
  2. 2.自定义加载器能够实现应用隔离,例如TomcatSpring等中间件和组件框架都在内部实现了自定义的加载器
  3. 3.所有用户自定义类加载器通常需要继承于抽象类java.lang.classLoader

3.3ClassLoader源码

ClassLoader

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. //查看当前类是否有被加载过 First, check if the class has already been loaded
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. c = parent.loadClass(name, false);
  12. } else {//给引导类加载器加载
  13. c = findBootstrapClassOrNull(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. // ClassNotFoundException thrown if class not found
  17. // from the non-null parent class loader
  18. }
  19. if (c == null) {
  20. // If still not found, then invoke findClass in order
  21. // to find the class. 如果没有类加载器加载,就自己加载当前类
  22. long t1 = System.nanoTime();
  23. c = findClass(name);
  24. // this is the defining class loader; record the stats
  25. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  26. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  27. sun.misc.PerfCounter.getFindClasses().increment();
  28. }
  29. }
  30. if (resolve) {//是否需要解析,默认为false
  31. resolveClass(c);
  32. }
  33. return c;
  34. }
  35. }

保护机制,即使我们重写了classLoader破坏双亲委派机制,引导类加载器也会加载java下的类

  1. /* Determine protection domain, and check that:
  2. - not define java.* class,
  3. - signer of this class matches signers for the rest of the classes in
  4. package.
  5. */
  6. private ProtectionDomain preDefineClass(String name,
  7. ProtectionDomain pd)
  8. {
  9. if (!checkName(name))
  10. throw new NoClassDefFoundError("IllegalName: " + name);
  11. // Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
  12. // relies on the fact that spoofing is impossible if a class has a name
  13. // of the form "java.*"
  14. if ((name != null) && name.startsWith("java.")) {
  15. throw new SecurityException
  16. ("Prohibited package name: " +
  17. name.substring(0, name.lastIndexOf('.')));
  18. }
  19. if (pd == null) {
  20. pd = defaultDomain;
  21. }
  22. if (name != null) checkCerts(name, pd.getCodeSource());
  23. return pd;
  24. }

3.5自定义类加载器

3.5.1自定义类加载器的用途

  1. 1.隔离加载类
  2. 2.修改类加载的方式
  3. 3.扩展加载源
  4. 4.防止源码泄漏

3.6Tomcat类加载机制

JVM概述 - 图16
JVM概述 - 图17

四、运行时内存

JVM概述 - 图18
JVM概述 - 图19

4.1程序计数器

JVM概述 - 图20
定义

  1. 为了保证程序(在操作系统中理解为进程)能够连续地执行下去,CPU必须具有某些手段来确定下一条指令的地址。而程序计数器正是起到这种作用,所以通常又称为指令计数器。
  2. JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。

作用

  1. 1.在程序开始执行前,必须将它的起始地址,即程序的一条指令所在的内存单元地址送入PC,因此程序计数器(PC)的内容即是从内存提取的第一条指令的地址。当执行指令时,CPU将自动修改PC的内容,即每执行一条指令PC增加一个量,这个量等于指令所含的字节数,以便使其保持的总是将要执行的下一条指令的地址。
  2. 2.由于大多数指令都是按顺序来执行的,所以修改的过程通常只是简单的对PC1
  3. 3.当程序转移时,转移指令执行的最终结果就是要改变PC的值,此PC值就是转去的地址,以此实现转移。有些机器中也称PC为指令指针IP Instruction Pointer)。

PC寄存器

  1. PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。执行引擎的字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

PC寄存器为什么会被设定为线程私有?

  1. 为了能够准确地记录各个线程正在执行的当前字节码指令地址,每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

为什么执行native方法是,是undefined

  1. native 本地方法是大多是通过C实现,并未编译成需要执行的字节码指令,所以在计数器中当然是空(undefined)。

4.2栈

  1. 它是一种运算受限的线性表,是线程私有的,主管程序运行,生命周期和线程同步,线程结束,栈内存就释放了。不存在垃圾回收问题。默认1024k

4.2.1栈帧

  1. 在这个线程上正在执行的每个方法都各自对应一个栈帧〈Stack Frame .枝帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

4.2.2栈帧的结构

1.局部变量表

  1. 局部变量表也被称之为局部变量数组或本地变量表,局部变量表所需的容量大小是在编译期确定下来的
  2. 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(8种)、对象引用(reference),以及returnAddress类型。

JVM概述 - 图21

  1. 注意:非静态方法默认会有this存储在局部变量表中下标0的位置,doublelong会占用两个slot(槽位)一个slot占用四个字节
  2. 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

2.操作数栈

  1. 在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。
  2. 具体来说便是:执行每一条指令之前,Java 虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。

3.动态链接

  1. 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking
  2. 作用:将符号引用转换为调用方法的直接引用

4.方法返回地址

  1. 只要在本万法的异常表中没有搜索到匹配的异常处理器,就会导致万法退出。简称异常完成出口。

5.一些附加信息

  1. 虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

4.3 本地方法栈

  1. 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
  2. 1.本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。·
  3. 2.甚至可以直接使用本地处理器中的寄存器
  4. 3.直接从本地内存的堆中分配任意数量的内存。

4.4堆

  1. 一个VM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  2. Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
  3. 堆内存的大小是可以调节的。
  4. 堆,是GC ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

JVM概述 - 图22
对象分配原则

  1. 1.优先分配到Eden
  2. 2.大对象直接分配到老年代
  3. · 尽量避免程序中出现过多的大对象
  4. 3长期存活的对象分配到老年代
  5. 4.动态对象年龄判断
  6. 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年
  7. 龄大于或等于该年龄的对象可以直接进入老年代,无须等MaxTenuringThreshold中要求的年龄。
  8. 5.空间分配担保
  9. -XX: HandlePromotionFailure

空间分配担保(jdk6之后默认开启)

  1. 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
  2. 如果大于,则此次Ninor Gc是安全的
  3. 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败
  4. 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Ninor Gc,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC

TLAB

  1. 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。(默认栈Eden空间的1%)
  2. 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

JVM概述 - 图23

4.4方法区

  1. HotSpot虚拟机:jdk1.7之前方法区称之为永久代,jdk8开始,使用元空间(方法区的具体实现)取代了永久代
  2. 元空间与永久代最大的区别在于:元空间不在虚打机设置的内存中,而是使用本地内存。|

JVM概述 - 图24

方法区溢出测试

  1. public class OOMTest extends ClassLoader {
  2. public static void main(String[] args) {
  3. int j = 0;
  4. try {
  5. OOMTest test = new OOMTest();
  6. for (int i = 0; i < 10000; i++) {
  7. //创建ClassWriter对象,用于生成类的二进制字节码
  8. ClassWriter classWriter = new ClassWriter(0);
  9. //指明版本号,修饰符,类名,包名,父类,接口
  10. classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
  11. //返回byte[]
  12. byte[] code = classWriter.toByteArray();
  13. //类的加载
  14. test.defineClass("Class" + i, code, 0, code.length);//Class对象
  15. j++;
  16. }
  17. } finally {
  18. System.out.println(j);
  19. }
  20. }
  21. }

五、对象的内存布局

5.1对象创建的步骤

5.1.1 判断对象对应的类是否加载,连接,初始化

虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。(即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以classLoader+包名+类名为Key进行查找对应的.class 文件。如果没有找到文件,则抛出classNotFoundException异常。·如果找到,则进行类加载,并生成对应的Class类对象。

5.1.2 为对象分配内存

首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。
如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。

  • 指针碰撞(内存空间连续)
    是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离。如果垃圾收集器选择的是Serial old、Par old这种基于压缩算法的,虚拟机采用这种分配方式

  • 空闲列表(内存空间零散)
    虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式称为“空闲列表(Free List)

5.1.3 处理并发问题

在分配内存空间时,另外一个问题是及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用了两种方式解决并发问题:

  1. CAS ( Compare And Swap )失败重试、区域加锁:保证指针更新操作的原子性;.
  2. TLAB 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区

5.1.4初始化分配到的空间

内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保正了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。

5.1.5设置对象头的信息

将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

5.1.6执行init初始化方法

初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

5.2对象的结构

JVM概述 - 图25

5.2.1对象头

  • 1)对象自身的运行时元数据(mark word)

    • 哈希值(hashcode):对象在堆空间中都有一个首地址值,栈空间的引用根据这
      个地址指向堆中的对象,这就是哈希值起的作用
    • GC分代年龄:对象首先是在Eden中创建的,在经过多次GC后,如果没有被进行回收,就会在survivor中来回移动,其对应的年龄计数器会发生变化,达到阈值后会进入养老区
    • 锁状态标志,在同步中判断该对象是否是锁·线程持有的锁
    • 线程偏向ID
    • 偏向时间戳
  • 2)类型指针,指向元数据区的类元数据InstanceKlass,确定该对象所属的类型

5.2.1实例数据

它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)

放置原则:

  • 相同宽度的字段总是被分配在一起
  • 父类中定义的变量会出现在子类之前(因为父类的加载是优先于子类加载的>
  • 如果CompactFields参数为true(默认为true):子类的窄变量可能插入间父类变量的空隙

5.2.3对齐填充

占位符的作用

5.3对象的访问定位

1.直接访问

2.句柄访问

  • 实现:堆需要划分出一块内存来做句柄池,reference中存储对象的句柄池地址,句柄中包含对象实例与类型数据各自具体的地址信息。|

六、执行引擎

6.1执行引擎的作用

JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被VM所识别的字节码指令、符号表,以及其他辅助信息。
那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

6.2 执行引擎的工作

JVM概述 - 图26

  • 1)执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
  • 2)每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
  • 3)当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型。

6.3 编译器

  1. Java 语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程; Sun 的javac Eclipse的JDT

  2. 也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In TimeCompiler)把字节码转变成机器码的过程。HotSpot VM的C1 C2编译器

  3. 还可能是指使用静态提前编译器(AOT 编译器,Ahead Of TimeCompiler)直接把.java文件编译成本地机器代码的过程。缺点:破坏一次编译到处运行,降低了java链接过程的动态性

6.3.1JIT编译器

在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器

  • 1.client:指定Java虚拟机运行在Client模式下,并使用c1编译器;

    • C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
    • client启动快,占用内存小,执行效率没有server快,默认情况下不进行动态编译,适用于桌面应用程序。
  • 2.server:指定Java虚拟机运行在Server模式下,并使用C2编译器。

    • C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高.
    • server启动慢,占用内存多,执行效率高,适用于服务器端应用;

6.3.2C1和C2编译器不同的优化策略

  • 在不同的编译器上有不同的优化策略,c1编译器上主要有方法内联,去虚拟化、冗余消除

    • 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
    • 去虚拟化:对唯一的实现类进行内联
    • 冗余消除:在运行期间把一些不会执行的代码折叠掉
  • C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在c2上有如下几种优化:

    • 标量替换:用标量值代替聚合对象的属性值
    • 栈上分配:对于未逃逸的对象分配对象在栈而不是堆
    • 同步消除:清除同步操作,通常指synchronized

总结:

  • 一般来讲,JIT编译出来的机器码性能比解释器高。
  • C2编译器启动时长比c1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器

java是半编译半解释型,根据什么选择JIT编译器
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR(On StackReplacement)编译。

一个方法究竞要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。

目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。

采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为
方法调用计数器(Invocation Counter)用于统计方法的调用次数
回边计数器(Back EdgeCounter)用于统计循环体执行的循环次数

这个计数器就用于统计方法被调用的次数,它的默认阈值在client模式下是 1500次,在 server模式下是 10000次。超过这个阈便,就会触发JIT编译。

当一个方法被调用时,会先检查该方法是否存在被JIT 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果已超过阙值,那么将会向即时编译器提交一个该方法的代码编译请求。

热度衰减
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(counter Decay),而这段时间就称为此方法统计的半衰周期(counter Half Life Time) 。

6.4解释器

当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字书码文件中的内容“翻译”为对应平台的本地机器指令执行。

七、垃圾回收

7.1垃圾的定义

运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾

7.2垃圾回收算法

  • 引用计数算法
    引用计数算法(Reference counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
    缺点:无法解决循环引用问题(手动解除, 使用弱引用)
  • 可达性分析算法

    • 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
    • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
    • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
    • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

7.3可达性分析算法

可达性分析算法中会出现内存泄漏吗

如果一个对象是垃圾但是没有被当做垃圾回收就是内存泄漏
在可达性算法中一个对象是垃圾但是被跟对象引用,导致不能被回收,就叫做内存泄漏

7.3.1GC Roots有哪些

  • 本地方法栈内JNI(通常说的本地方法)引用的对象类
  • 静态属性引用的对象

    • 比如: Java类的引用类型静态变量
  • 方法区中常量引用的对象

    • 比如:字符串常量池(string Table)里的引用
  • 所有被同步锁synchronized持有的对象
  • Java虚拟机内部的引用。

    • 基本数据类型对应的class对象,一些常驻的异常对象(如:NullpointerException、outofMemoryError),系统类加载器。
  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root 。

注意点
使用可达性算法,只能某一个快照下去对对象去做判定,这点也是导致Gc进行时必须“stop The world”的一个重要原因
成员变量,也叫实例变量,不同于类变量(静态变量),前面讲到类变量是存储在方法区中,而成员变量是存储在堆内存的对象中的,和对象共存亡,所以是不能作为GC Roots的

7.4垃圾清除算法

7.4.1标记清除算法

JVM概述 - 图27
缺点

  • 效率比较低:递归与全堆对象遍历两次
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
  • 这种方式清理出来的空闲内存是不连续的,产生内存碎片。

7.4.2复制算法

JVM概述 - 图28
核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点

  • 没有标记和清除过程,实现简单,运行高效
  • 复制过去以后保证空间的连续性,不会出现“碎片”问题。

缺点

  • 需要两倍的内存空间。
  • 对于G1这种分拆成为大量region的Gc,复制而不是移动,风意味着cc需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
  • 如果系统中的存活对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。

7.4.3标记整理算法

收集过程
第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
JVM概述 - 图29
优点

  • 消除了标记/清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
  • 消除了复制算法当中,内存减半的高额代价。

缺点

  • 从效率上来说,标记-压缩算法要低于复制算法。
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
  • 移动过程中,需要全程暂停用户应用程序。即:STW

7.5内存溢出和内存泄漏

7.5.1内存溢出

  • Java虚拟机的堆内存设置不够
  • 代码中创建了大量大对象,并长时间不能被垃圾回收期回收

7.5.2 内存泄漏

如果一个对象是垃圾但是没有被当做垃圾回收就是内存泄漏

内存泄漏的8种情况

  • 静态集合类
  • 单例模式
  • 内部类持有外部类
  • 各种连接,如数据库连接,网络连接
  • 变量不合理的左右域

    • 在方法内给成员变量赋值然后不置为null,导致成员变量长期不会被回收
  • 改变哈希值

    • 往hash集合内放入值,然后改变hash值

      1. HashSet<GCHash> objects = new HashSet();
      2. GCHash a = new GCHash("张三", "18");
      3. GCHash b = new GCHash("李四", "20");
      4. objects.add(a);
      5. objects.add(b);
      6. a.setName("王五");
      7. objects.remove(a);
      8. log.info("集合的对象有{}",objects);
  • 缓存泄漏

  • 监听器和回调

7.6安全点和安全区域

安全点
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始Gc,这些位置称为“安全点(Safepoint) ”

Safe Point的选择很重要,如果太少可能导致Gc等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。

如何在cc发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断:(目前没有虚拟机采用了)
    首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
  • 主动式中断:
    设置一个中断标志,各个线程运行到safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

安全区域(Safepoint)
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint 。但是,程序“不执行”的时候呢?例如线程处于Sleep状态或
Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。

  • 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程;
  • 当线程即将离开Safe Region时,会检查JVM是否已经完成Gc,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止;

八、垃圾回收器

8.1GC评估指标

  • 吞吐量:程序的运行时间(程序的运行时间+内存回收的时间)
  1. 运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
  • 垃圾收集开销:吞吐量的补数,垃圾收集器所占时间与总时间的比例。
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  • 收集频率:相对于应用程序的执行,收集操作发生的频率。
  • 内存占用:Java堆区所占的内存大小。
  • 快速:一个对象从诞生到被回收所经历的时间。

现在JVM调优标准:在最大吞吐量优先的情祝下,降低停顿时间。

8.2垃圾回收器的分类

  • 串行回收器: serial、 serial old
  • 并行回收器: ParNew、Parallel scavenge、Parallel old
  • 并发回收器:CMS、G1
    JVM概述 - 图30

8.3Parallel GC(注重吞吐量,JDK8默认垃圾回收器)

特点

  • Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集需。
  • 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。

JVM概述 - 图31

8.4CMS(注重停顿时间)

新生代与ParNew(默认)和Serial收集器搭配
JVM概述 - 图32
初始标记(STW):暂时时间非常短,标记与GC Roots直接关联的对象。
并发标记(最耗时):从GC Roots开始遍历整个对象图的过程。不会停顿用户线程
重新标记:(STW):修复并发标记环节,因为用户线程的执行,导致数据的不一致性问题(关注垃圾从不可达到可达,不关注可达到不可达—浮动垃圾)
并发清理(最耗时)将垃圾清除

优点

  • 并发收集
  • 低延迟

缺点:

  • 会产生内存碎片
  • CMS收集器对CPU资源非常敏感
  • CMS无法处理浮动垃圾(对象可达—-》不可达)

8.5G1(在保证最大吞吐量的同时,降低停顿时间,JDK9默认回收器)

JVM概述 - 图33

  • 并行与并发

    • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STw
    • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  • 分代收集

    • 从分代上看,61依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
    • 将堆空间分为若干个区域(Region) ,这些区域中包含了逻辑上的年轻代和老年代。
    • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
  1. //设置老年代占用比例默认是 新生代:老年代1:2
  2. -XX:NewRation=2
  3. //设置新生代占用最大内存
  4. -Xmn
  5. //设置Eden区占用比例 默认 Eden区:From Survivor 区:To Survivor 区 8:1:1
  6. -XX:SurvivorRatio
  7. //true为使用 false为不使用 空间分配担保策略
  8. -XX:HandlePromotionFailure
  9. //设置新生代去老年代的次数默认是15
  10. -XX:MaxTenuringThreshold=
  11. //打印GC详细信息
  12. -XX : +PrintGCDetails
  13. //TLAB设置是否开启默认为true
  14. -XX:+UseTLAB
  15. //设置tlab空间大小
  16. -XX:TLABwasteTargetPercent
  17. //设置永久代初始大小
  18. -XX:Metaspacesize=10m
  19. //设置永久代最大值
  20. -XX:MaxMetaspaceSize=10m
  21. //控制开启编译器的server还是client模式
  22. -XX:+RewriteFrequentPairs
  23. //设置JIT调用阈值,超过当前阈值就会触发JIT编译
  24. -XX: CompileThreshold
  25. //是否关闭热度衰减
  26. -XX:-UseCounterDecay
  27. //热度衰减时长,单位为秒
  28. -XX:CounterHalfLifeTime
  29. ------parallel gc相关参数设置
  30. //指定垃圾回收器
  31. -XX:+UseParallelGC
  32. //指定老年代垃圾回收器
  33. -XX:+UseParallelOldGC
  34. //设置年轻代并行收集线程数
  35. -XX:ParallerGCThreads
  36. 在默认情况下,当CPU 数量小于8个,ParallelGCThreads的值等于CPU 数量。
  37. CPU数量大于8个,Paralle1GCThreads的值等于3+[5* CPu_Count]/8]。
  38. //设置最大停顿时间
  39. -XX:MaxGCPauseMillis
  40. //设置垃圾总占比时间,用户衡量吞吐量的大小,取值范围(0,99)
  41. -XX:GCTiimeRatio
  42. //设置自适应调节策略 默认开启
  43. -XX:UseAdaptiveSizePolicy
  44. -------G1垃圾回收器参数设置
  45. //设置垃圾回收器
  46. -XX:+UseG1GC
  47. //设置Region大小默认是堆内存的1/2000,范围是1MB-32MB
  48. -XX:G1HeapRegionSize
  49. //设置期望最大GC停顿时间,默认是200ms
  50. -XX:MaxGCPauseMillis
  51. //设置STW时GC线程数,最多设置为8
  52. -XX:ParallelGCThreads
  53. //设置并发标记的线程数
  54. -XX:ConcGCThread
  55. //设置触发并发GC周期的java堆占用率阈值,超过就触发GC,默认是45
  56. -XX:InitiatingHeapOccupancyyPercent
  57. -----------------;