JVM——Java虚拟机

JVM——Java虚拟机

1. 内存结构

1.1 程序计数器

定义:Program Counter Register程序计数器(寄存器)
作用:记住下一条JVM指令的执行地址
特点:线程私有;不会内存溢出。

1.2 虚拟机栈

Java Virtual Machines Stacks(Java虚拟机栈)

  • 栈-每个线程运行需要的内存空间,每个栈由多个栈帧(Frame)组成
  • 栈帧-每个方法运行时需要的内存
  • 每个线程只能有一个活动栈帧,对应当前正在执行的那个方法

问题辨析:

  1. 垃圾回收是否涉及栈内存?不需要,方法调用结束就会弹出栈
  2. 栈内存分配越大越好吗?-Xss size设置栈内存。物理内存一定,栈内存越大,线程数越少。
  3. 方法内的局部变量是否线程安全?局部变量安全,引用的对象未必。

1.2.1 栈内存溢出StackOverFlow

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出

1.2.2 线程运行诊断

cpu占用过多

  • 用top定位哪个进程堆cpu占用高
  • ps H -ef pid,tid,%cpu | grep 进程id(用ps命令进一步定位哪个线程引起cpu占用过高)
    
  • jstack 进程id 可以根据线程id找到有问题的线程,进一步定位问题代码的源码行号

程序运行很长时间没有结果

1.3 本地方法栈

Native Method Stacks本地方法栈

1.4 堆

1.4.1 Heap堆

通过new关键字,创建对象都会使用堆内存
特点

  • 线程共享,堆中对象都需要考虑线程安全问题
  • 有垃圾回收机制

1.4.2 堆内存溢出

1.4.3 堆内存诊断

  1. jps工具
    • 查看当前系统中有哪些java进程
  2. jmap工具
    • 查看堆内存占用情况
  3. jconsole工具
    • 图形界面的,多功能的检测工具,可以连续监测
  4. jvisualvm工具

1.5 方法区

1.5.1 Meathod Area方法区定义:

  • 线程共享
  • JVM启动时创建,逻辑上是堆的一部分
  • 存储类相关结构,如运行时常量池、成员变量、方法数据、成员&构造方法、特殊方法(类构造器)

1.5.2 方法区内存溢出

1.8以前的永久代内存溢出

演示永久代内存溢出 java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize-8m

1.8以后的元空间内存溢出

演示元空间内存溢出 java.lang.OutOfMemoryError:Metaspace space
-XX:MaxMetaspaceSize-m

1.5.3 运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是*.class文件中的,当类被加载,它的常量池信息就会被放入运行时常量,并把里面的符号地址变为真实地址。

1.5.4 StringTable

// javap -v xxx.class查看字节码
// StringTable["a","b","ab"]
// 常量池中的信息会被加载到运行时常量池中,这时a b ab都是常量池中的符号,还没有编程java字符串对象
// idc #2会把a符号变为"a"对象
// idc #3会把b符号变为"b"对象
// idc #4会把ab符号变为"ab"对象

public static void main(String[] args){
    String s1 = "a"; // 懒惰的
    String s2 = "b";
    // 字符串常量拼接,编译器优化为"ab",入池
    String s3 = "ab";
    // 变量拼接,运行期间StringBuilder创建,堆中
    String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString(); 
    String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期间确定为ab
    String s6 = s4.intern();
    
    System.out.println(s3 == s4); // false
    System.out.println(s3 == s5); // true
    System.out.println(s3 == s6); // true
    
    String x2 = new String("c") + new String("d");
    String x1 = "cd";
    String x3 = x2.intern();
    // 如果调换【最后两行代码】的位置,调换后为true
    System.out.println(x1 == x2);// false
    System.out.println(x1 == x3);// true
}

特性

  • 常量池中字符串仅是符号,第一次用到时才变为对象
  • 利用串池机制,来避免重复创建字符串对象
  • 字符串变量拼接原理时StringBuilder(1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串放入串池
    • 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会放入串池,会把串池中的对象返回
    • 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会复制一份放入串池,会把串池中的对象返回

1.5.5 StringTable位置

6fa83329d0cb733d

1.5.6 StringTable垃圾回收

1.5.7 StringTable性能调优

  • 调整-XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池

1.6 直接内存

748a9fc080a51514

0d6aebb94d3491be

Direct Memory

  • 常见于NIO操作,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

2. 垃圾回收

2.1 如何判断对象可以回收

2.1.1 引用计数法

2.2.2 可达性分析算法

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为GC Root?

2.2.3 四种引用

  1. 强引用
    • 只有所有GC Root对象都不通过【强引用】该对象,该对象才能被垃圾回收
  2. 软引用
    • 仅有软引用该对象时,在垃圾回收后,内存仍不足时会再次发出垃圾回收,回收软引用对象
    • 可以配合引用队列来释放引用自身
    private static final int _4MB = 4*1024*1024;
    public static void main(String[] args){
        List<SoftReference<byte[]>> list = new ArrayList<>();
        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        for(int i=0;i<5;i++){
            // 关联了引用队列,当软引用所关联的byte[]被回收,时,软引用会自己加入引用队列
            SoftReference<byte[]> ref = new SoftRerence<>(new byte[_4MB],queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        Reference<? extends byte[]> poll = queue.poll();
        while(poll != null){
            list.remove(poll);
            poll = queue.poll();
        }
        for(SoftReference<byte[] reference:list){
            System.out.println(reference.get());
        }
    }
    
  3. 弱引用
    • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
    • 可以配合引用队列来释放弱引用本身
  4. 虚引用
    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
  5. 终结器引用
    • 无需手动编码,但内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC才能回收被引用对象。

2.2 垃圾回收算法

2.2.1 标记清除Mark Sweep

  • 速度快
  • 会造成内存碎片

2.2.2 标记整理Mark Compact

  • 速度慢
  • 没有内存碎片

2.2.3 复制Copy

  • 不会有内存碎片
  • 可用内存空间减半

2.3 分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的年龄加1,并且交换from to
  • minor gc会引发stop the world,暂停其他用户的线程,等待垃圾回收线程结束
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命15次(4bit)
  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW时间更长。

相关VM参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx或-XX:MaxHeapSize=size
新生代大小 -Xmm或(-XX:NewSize=size+-XX:MaxNewSize=size)
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC前MinorGC -XX:+ScavengeBeforeFullGC

2.4 垃圾回收器

  1. 串行

    -XX:UseSerialGC=Serial+SeralOld
    
    • 单线程
    • 堆内存较小,适合个人电脑
  2. 吞吐量优先

    -XX:+UserParallelGC~-XX:+UseParallelOldGC
    -XX:+UseAdaptiveSizePolicy
    -XX:GCTimeRatio=ratio
    -XX:MaxGCPauseMills=ms
    -XX:ParallelGCThreads=n
    
    • 多线程
    • 堆内存较大,多核cpu
    • 单位时间内STW时间最短
  3. 响应时间优先

    -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
    -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
    -XX:CMSInitiatingOccupancyFraction=percent
    -XX:+CMSScavengeBeforeRemark
    
    • 多线程
    • 堆内存较大,多核cpu
    • 尽可能让单次STW时间最短
  4. G1

2.5 垃圾回收调优

3. 类加载与字节码计数

3.1 类文件结构

魔数 0-3字节,表示它是否是【class】类型的文件 ca fe ba be
版本 4-7字节,表示版本00 34(52),表示java8
常量池 8-9字节,表示常量池长度,00 35(35)表示常量池有#1~#34项。#0项不计入,也没有值
第#1项0a表示一个Method信息,00 06和00 15(21)表示引用常量池中#6和#21项来获得这个方法的【所属类】和【方法名】
ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

3.2 字节码指令

javap工具反编译class文件

3.2.1 图解方法执行流程

  1. 原始java代码
  2. 编译后的字节文件
  3. 常量池载入运行时常量池
  4. 方法字节码载入方法区
  5. main线程开始运行,分配栈帧内存
  6. 执行引擎开始执行字节码

多态的原理HSDB

3.3 编译期处理

3.4 类加载阶段

  1. 加载
    • 将类的字节码载入方法区,内部采用C++的instanceKlass描述java类,它的重要field有:
      • _java_mirror 即java类镜像,例如对String来说,就算String.class,作用是把Klass暴露给java使用
      • _super 即父类
      • _fields 即成员变量
      • _methods 即方法
      • _constants 即常量池
      • _class_loader 即类加载器
      • _vtable 虚方法表
      • _itable 接口方法表
    • 如果这个类还有父类没有加载,先加载父类
    • 加载和链接可能是交替运行的
  2. 链接
    • 验证:验证类是否符合JVM规范,安全性检查
    • 准备:为static变量分配空间,准备默认值
      • static变量在JDK7之前存储与instanceKlass末尾,JDK7后存储于_java_mirror末尾。
      • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
      • 如果static变量是final的基本类型以及字符串常量,你们编译阶段值就确定了,赋值在准备阶段完成
      • 如果static变量是final的,但属于引用类型,你们赋值也会在初始化阶段完成
    • 解析:将常量池中的符号引用解析为直接引用
  3. 初始化
    初始化即()v方法,虚拟机会保证整个类的构造方法的线程安全。类初始化是懒惰的,发生时机:
    • main方法所在的类,总会被首先初始化
    • 首次访问这个类的静态变量或静态方法
    • 子类初始化,如果父类还没初始化,会引发父类初始化
    • Class.forName()
    • new会导致初始化
      不会导致类初始化情况:
    • 访问类的static final静态常量(基本类型和字符串)不会触发初始化
    • 类对象.class不会触发初始化
    • 拆给年间该类的数组
    • 类加载器的loadClass方法
    • Class.forName()的参数2为法拉瑟时

3.5 类加载器

名称 加载类路径 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
Application ClassLoader classpath 上级为Extension
自定义类加载器 自定义 上级为Application
  1. 启动类加载器
  2. 扩展类加载器
  3. 双亲委派机制
    所谓双亲委派,就是指调用类加载器的loadClass方法,查找类的规则。优先委派上级类加载器进行加载,如果上级加载器:
    • 能找到这个类,由上级加载,加载后该类也对夏季加载器可见
    • 找不到这个类,则下级类加载器才有资格执行加载
  4. 线程上下文类加载器
  5. 自定义加载器

3.6 运行期优化

  1. 即时编译

    1. 分层编译
      JVM将执行状态分成了5个层次
      • 0层,解释执行(Interpreter)
      • 1层,使用C1即时编译器编译执行(不带profiling)
      • 2层,使用C1即时编译器编译执行(带基本的profiling)
      • 3层,使用C1即时编译器编译执行(带完全的profiling)
      • 4层,使用C2即时编译器编译执行
      • profiling是指在运行过程中收集一些程序执行状态的数据,如【方法的调用次数】、【循环的回边次数】等
        JIT和解释器的区别:
      • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
      • JIT是将一些字节码编译为针对字节码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
      • 解释器是将字节码解释为针对所有平台都通用的机器码
      • JIT会根据平台类型,生产平台特定的机器码
        对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采用解释执行的方式运行。对于小部分发的热点代码,我们可以将其编译成机器码。执行效率Interpreter<C1<C2,目标是发现热点代码(hotspot名称由来)
        逃逸分析 ,C2编译器中会发现新建的对象是否逃逸,若无逃逸则优化字节码。使用-XX:-DoEscapeAnalysis关闭逃逸分析。
    2. 方法内联(Inlining)
      如果发现热点方法,并且长度不长时,会进行内联。即把方法内部代码拷贝粘贴到调用者位置。还能进行常量折叠(constant folding)的优化。
    3. 字段优化
      尽可能使用局部变量,而不是成员变量和静态变量。
  2. 反射优化

4. java内存模型JMM

JMM(java memory model)定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。

4.1 原子性

4.2 可见性

4.3 有序性

详见并发编程

皖ICP备2023003517号-1