虚拟机概述

  • 虚拟机是java程序运行环境,使得java可以一次编译,处处运行
  • 常见的术语
    • JVM:java运行最基本的环境
    • JRE: Java Runtime Environment = JVM + 基础类库
    • JDK:Java Development Kit = JVM+ 基础类库 + 编译工具
    • JavaSE: Java Standard Edition = JDK+基础开发
    • JavaEE: Java Enterprise Edition = JDK+基础开发+应用服务器
  • 基本结构
    • 内存结构
    • 垃圾回收器
    • Java程序加载机制
  • 学习的目标
    • 了解java程序运行的原理
    • 掌握常用虚拟机的调试 和 配置的相关工具
    • 具备可以线上调优的能力
  • java程序的生命周期源码编写 -> 代码编译 -> 虚拟机加载 -> 虚拟机存储 -> 虚拟机运行 -> 垃圾回收
  • 内存的整体结构image-20220520095049892.png

    Java代码的运行机制

    代码编译机制

  • 将.java文件编译成.class文件 :::info javac xxx.java :::

  • 对.class的翻译方式有两种

    • 解释执行,即逐条将字节码翻译成机器码并执行;
    • 第二种是即时编译(Just-In-Time,JIT),即将一个方法中包含的所有的字节码编译成机器码后再执行
  • Hotspot默认使用是混合编译(mixed mode),那些编译,那些优化,则是由监视器(Profile Monitor)决定
  • 可以使用javap命令文件.class文件 :::info javap -c xxx.class > xxx.txt :::

    代码加载机制

  • 代码加载过程

    • Loading:使用类加载器加载类
    • Linking:是指将创建成的类合并至Java虚拟机中,使之能够执行的过程。
    • Initializing:则是为标记为常量值的字段赋值。利用这个机制可以使用的单例模式的代码可以延时加载
  • 类加载器

    • 多个类加载器
      • Bootstrap ClassLoader:加载$JAVA_HOME中jre/lib/rt.jar里面所有的class或Xbootclassoatch选项指定的jar包
      • Extension ClassLoader:加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
      • App ClassLoader:加载classpath中指定的jar包及Djava.class.path所指定目录下的类和jar
      • Custom ClassLoader:通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身 需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
    • 双亲委派原则
      • 由于出现了多个类加载,那么就可能会出现类对重复加载的问题
      • 当一个类加载器收到类加载指令的时候,自己不会去加载,而且是去让父类去加载,如果父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的),然后自己加载
      • 如此就可以保证重复被类加载器加载类只有一个

        代码运行机制

  • 内存可以分为两个部分

    • 代码的存储区域
      • 方法区
    • 代码的运行区域
      • 计数器
      • 本地方法区(主要运行native的方法)
  • 代码被加载之后,都存在方法区;代码执行过程中创建的对象存储在堆中
  • 代码主要是在栈里面运行

    • 每个线程都会在栈里面被分配一块独立的空间
    • 每个线程中执行的方法,会当前栈中,再次被分配一个空间,即栈帧

      JVM的内存结构

      程序计数器

  • 程序计数器(Program Counter Register):用于多线程在切换了记录当前运行的位置,以保证切换回来的时候可以继续

  • 是唯一一个没有内存溢出的空间

    虚拟机栈

  • Java Virtual Machine Stacks(Java 虚拟机栈) 是每个线程运行时所需要的内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 栈内存可能出现的异常的情况-(1)当固定大小情况下,线程请求分配的栈容量大于Java虚拟机栈最大容量时,抛出异常:StackOverFlowError。- (2)当可拓展时,如果在拓展过程中,无法申请到足够的内存时,抛出异常:OutOfMemoryError(比如:JVM运行内存被占满,此时已经无处可以申请内存了)。
  • 栈内存的大小

    • 可以自定义设置栈的大小,通过java运行的执行设置栈的大小

      java -Xss1024k Test001
      java -XX:ThreadStackSize=1024 Test002

    • 在idea中设置vm的参数image-20220520113219827.png

    • 在eclipse中的设置image-20220520113313661.png
    • 查看默认的栈的大小

      java -XX:+PrintFlagsFinal -version | findstr /i “ThreadStackSize”

    • 不同操作系统的栈的默认大小不一样

      Linux / ARM(32位):320 KB
      Linux / i386(32位):320 KB
      Linux / x64(64位):1024 KB
      OS X(64位):1024 KB
      Oracle Solaris / i386(32位):320 KB
      Oracle Solaris / x64(64位):1024 KB

  • 一台服务器可以创建线程的计算公式,根据栈的大小计算可以创建线程的数量

    (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads

    • MaxProcessMemory 指的是一个进程的最大内存
    • JVMMemory JVM内存
    • ReservedOsMemory 保留的操作系统内存
    • ThreadStackSize 线程栈的大小

本地方法栈

1,本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。本地方法栈服务的对象是JVM执行的native方法,其就是一个java调用非java代码的接口,作用是与操作系统和外部环境交互
2,某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。
3,能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈。
4,描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。

heap定义

  • 堆内存被所有的线程共享
  • 通过new出来的对象都是在堆里面
  • 堆会出现内存溢出的问题

    堆的大小

  • 有初始的大小

  • 有最大值的大小

    1. //字节大小 K -> M -> G -> T<br /> double initialMemory= Runtime.getRuntime().totalMemory()/1024/1024;<br /> double maxMemory = Runtime.getRuntime().maxMemory()/1024/1024;<br /> System.out.println("堆内存的初始总量Xms:"+initialMemory);<br /> System.out.println("堆内存的最大总量Xmx:"+maxMemory);
  • 默认情况下,初始内存大小=物理电脑内存大小/64 ;最大内存大小=物理电脑内存/4

  • 修改初始值和最大值的堆内存的大小 :::info -Xms10m -Xmx10m :::

    -Xms10m 初始值的大小 -XX:InitialHeapSize=10m -Xmx10m 最大值的大小 -XX:MaxHeapSize=10m

  • 查看正在运行的java程序的状态

    • jps : 查看系统上运行的java程序进程
    • jstat:查看java的运行程序状态
    • jvisualvm:可以调用jdk提供的可视化工具进行查看

      堆内存的结构

  • 堆内存要分为一下结构

    • 新生代 + 老年代 + 永生代(1.7)
    • 新生代 + 老年代 + 元空间(1.8)
  • 新生代:新创建的对象就是放在的新生代
    • eden区:所有的对象最先出生的地方
    • Servivor区:又分为s0和s1也就是from和to
    • 新生代 和 老年代的比例是1:2
    • eden区 和 Servivor区的比例是8:1:1
    • 如果eden满了会出发MinorGC
      • 在eden区内存满了之后,会触发MinorGC,将eden区存活下来的对象放入到servivor区的s1,然后清空eden中的内存
      • 等eden的区的内存在在满了之后,又会触发MinorGC,会将S1中所有存活下来的对象和eden中存活下来的对象都放入到s0的区域,然后将s1和eden区的对象全部清空
      • 如果在minorGC中,Servivor区放不下了,就会放入到老年代中
    • Servivor区如果满了,会将一部分对象放入到老年代
  • 老年代
    • 生命周期长的内存对象
    • 一般而言新生代的对象经过15次GC没有被释放就会被放入到老年代
    • 如果老年代满了就会进行Major GC
    • 如果Major GC并释放老年代的对象,那么就会OOM
  • 永生代

    • 在1.7的时候大小是固定的
    • 在1.8之后就改成了matespace元空间,大小不受限制,和物理内存一样大

      内存活动的流程

  • 程序运行会先加载程序的元数据放入到meta区

  • 在程序的过程中逐步的创建对象放入到Eden区
  • 当Eden区中的内存满了以后,会进行Young GC/minGC,其中大多数的对象被回收,没有被回收的拷贝到(Copying)s0区
  • 再次YGC,将eden区和S0区的没有被回收的对象拷贝到S1区
  • 再次YGC,eden+S1 -> S0,然后循环4,5
  • 年龄足够拷贝到老年代(一般是15次,CMS6次)
  • 如果新创建的对象占用内存很大,则直接分配到老年代,当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

    关于GC

  • 在内存GC时候所有线程都会暂停SWT

  • minGC触发条件,就是eden区内存满了
  • oldGC触发条件,就是老年代内存满了
  • fullGC = oldGC + minGC +mateGC
    • 调用System.gc()时,系统建议执行Full GC,但是不必然执行。- 老年代的空间不足。- 方法区的空间不足。- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。- 年轻代放不下,放到老年代中,若该对象大小大于老年代,也会触发Full GC。
  • 尽量减少内存的碎片

    方法区

  • 和堆内存差不多,其实在逻辑上也是堆的一部分

  • 主要存放的内容
    • 方法区需要存储每个加载的类(类,接口,枚举,注解)
    • 域(属性)信息
    • JVM需要保存所有方法的信息及其声明的顺序
    • non-final的类变量(static)
  • 方法区大小的设置

    • 1.7的设置 :::info -XX:Permsize 设置永久代初始分配空间
      -XX:MaxPermsize 设定永久代最大可分配空间
      OutOfMemoryError:PermGen space OOM错误 :::

    • 1.8的设置 :::info -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 元数据区大小,其默认值依赖于平台。windows下,-XX:MetaspaceSize=21M -XX:MaxMetaspaceSize=-1 即没有限制。 :::

  • 方法区大小的测试

    1. //-XX:MaxMetaspaceSize=100m
    2. public class TestMethod extends ClassLoader {
    3. public static void main(String[] args) {
    4. int j=0;
    5. TestMethod test = new TestMethod();
    6. try{
    7. for(int i=0;i<300000;i++){
    8. //ClassWriter作用是生成类的二进制字节码
    9. ClassWriter cw = new ClassWriter(0);
    10. //版本号,public,类名,包名,父类,接口
    11. cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
    12. //返回byte[]
    13. byte[] code = cw.toByteArray();
    14. //执行了类的加载
    15. test.defineClass("Class"+i,code,0,code.length);
    16. j++;
    17. }
    18. }finally {
    19. System.out.println(j);
    20. }
    21. }
    22. }

    垃圾回收

  • 如何定义垃圾

    • 引用计数法(reference count):如果指向当前对象引用是0那么这个对象就会被回收,虽然简单,但是不能解决循环引用的问题
    • 根可达性分析(RootSearch):为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
  • 垃圾如何清除
    • 标记清除(Mark-Sweep):最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题
    • 复制算法(copying):为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。但是最大的问题是可用内存被压缩到了原本的一半**。且存活对象增多的话,Copying 算法的效率会大大降低。
    • 标记压缩算法(Mark-Compact**)**:结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象
    • 分带收集算法:分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
  • 垃圾回收算法

    • 在1.8默认情况下使用的是UseParallelGC
    • 查看默认的垃圾回收器的算法 :::info java -XX:+PrintCommandLineFlags -version :::

    • 如何切换垃圾回收器 :::info -XX:+UseG1GC :::

    • 可以通过jinfo 查看 垃圾回收器的配置

      小结

  • JVM的基本构成

    • 代码运行机制
    • 内存的结构
    • 垃圾回收算法
  • 代码运行机制
    • 解释翻译 + JIT即使翻译
    • 双亲委派机制
  • 内存结构
    • 程序计数器
      • 唯一没有内存泄漏的问题
    • 虚拟机栈
      • 每个线程是独立拥有
      • 设置栈的大小
    • 本地方法栈
      • 执行C方法的内存空间
    • 堆空间
      • 新生代
        • eden
        • suvivior区
      • 老年代
      • 元空间
      • 垃圾回收方式
        • minGC
        • oldGC
        • fullGC =minGC+oldGC
      • 设置堆大小的指令 和 查看内存变化的工具
    • 方法区
      • 用于存储代码,静态变量的区域
  • 垃圾回收
    • 如何定义垃圾
    • 回收的基本算法
    • 各种垃圾回收期
      • 在1.8设置使用G1垃圾回收期
  • 目标
    • 了解JVM的基本概念
    • 掌握JVM的内存的结构
    • 会设置JVM的一些参数和调试工具