JVM

本文最后更新于:2023年11月16日 下午

JVM

JVM定义

  • JVM是运行在操作系统之上的,它与硬件没有直接的交互
  • 通过编译器将 Java 程序转换成该虚拟机所能识别的指令序列,也称 Java 字节码。Java虚拟机会将字节码,即class文件加载到JVM中。由JVM进行解释和执行

类加载器

  • 类加载器,即ClassLoader,它负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定

类加载器分类

虚拟机自带的类加载器

  1. 启动类加载器:主要负责加载jre中的最为基础、最为重要的类。如$JAVA_HOME/jre/lib/rt.jar等,以及由虚拟机参数 -Xbootclasspath 指定的类。由于它由C++代码实现,没有对应的java对象,因此在java中,尝试获取此类时,只能使用null来指代。
  2. 扩展类加载器:由Java代码实现,用于加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类,以及由系统变量 java.ext.dirs 指定的类。如$JAVA_HOME/jre/lib/ext/*.jar。
  3. 应用程序类加载器:由Java代码实现, 它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。

用户自定义的加载器

  • Java.lang.ClassLoader的子类,用户可以定制类的加载方式。例如可以对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。

双亲委派机制

  • 双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
  • 应用程序类加载器的父类是扩展类加载器,扩展类加载器的父类是启动类加载器
  • 优点:1.避免类的重复加载。2.防止核心API中定义的类型不会被用户恶意替换和篡改

JVM的内存模型

gZ2GDO.png

  • Java虚拟机将运行时内存区域划分为五个部分,分别为方法区,堆,PC寄存器,Java方法栈和本地方法栈
  • 执行Java代码首先需要使用类加载器将它编译成而成的class文件加载到Java虚拟机中。加载后的Java类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。
  • 在虚拟机中,方法区和堆为线程共享,也是垃圾回收的重点照顾区域。栈空间为线程私有,基本不会出现垃圾回收。
  • Java虚拟机将栈细分为面向Java方法的Java方法栈,面向本地方法(c++写的native方法)的本地方法栈,以及存放各个执行线程执行位置的PC寄存器(程序计数器)
  • 在运行过程中,每当调用进入一个Java方法,Java虚拟机会在当前线程的Java方法栈中生成一个栈帧(栈的一片区域),用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且Java虚拟机不要求栈帧在内存空间里连续分布。当推出当前执行的方法时,不管是正常返回还是异常返回,Java虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。
  • Execution Engine执行引擎负责解释命令,提交操作系统执行
  • Native Method Stack:定义了很多调用本地操作系统的方法,也称之为本地方法接口
  • 每个线程都有一个程序计数器,是线程私有的,就是一个指针
  • 方法区:所有定义的方法的信息都保存在该区域,此区属于共享区间。
    静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中。
  • JDK1.7之前通过永久代实现方法区,1.7之前字符串常量池放到方法区中
  • JDK1.8之后,通过元空间实现方法区,1.7之后字符串常量放到堆中

  • 栈我们也叫内存,是线程私有的,生命周期随线程的生命周期,线程结束栈内存释放
  • 栈:8种基本类型的变量+对象的引用变量+实例方法都是在栈内存中分配
  • 在栈区域规定了两种异常状态:如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  • 栈帧:一个线程的每个方法在调用时都会在栈上划分一块区域,用于存储方法所需要的变量等信息,这块区域称之为栈帧(stack frame)。栈由多个栈帧构成,好比一部电影由多个帧的画面构成。

  • 堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
  • 逻辑上分为三部分:1.新生区。2.养老区。3.永久区。(1.8后改为元空间)
  • 新生区进一步分为:
    • 伊甸园区
    • 幸存区
      • 幸存from区
      • 幸存to区
  • 在物理上划分,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor
  • 创建对象的过程
    • 新new的对象会放在伊甸园区(大对象直接进入老年代),伊甸园区的对象存活率非常低,当伊甸园区快满时会触发轻量级的垃圾回收机制(MinorGC)MinorGC会回收伊甸园区和幸存from区,会将伊甸园的幸存者标记复制到幸存to区,from区中的幸存者会根据它的年龄判断它的去向:默认情况下,如果年龄小于15则被标记到to区,如果年龄大于15则被标记复制到养老区,然后from区和to区交换角色(to区又为空了,下次又是回收from区的)
    • 当养老区内存不足时会触发重量级垃圾回收机制(MajorGC/fullGC),如果养老区无法回收内存则会出现OOM异常

JVM常见参数设置

  • -Xms:堆初始值(默认为物理内存的1/64)
  • -Xmx:堆最大可用值(默认为物理内存的1/4)
  • -Xss:每个线程的栈大小,默认为1M,此值不能设置过大,否则会减少线程并发数。

常见异常

  • 错误原因: java.lang.OutOfMemoryError: Java heap space 堆内存溢出
  • 解决办法:调大堆内存大小
  • 错误原因: java.lang.StackOverflowError表示为栈溢出,一般产生于递归调用。
  • 解决办法:设置线程最大调用深度,默认是1m

GC

  • GC:JVM中的Garbage Collection,简称GC,它会不定时去堆内存中清理不可达对象。
  • 分类:两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)
  •  新生代GC(minor GC):只针对新生代区域的GC。
  • 老年代GC(major GC or Full GC):针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC。
  • Minor GC触发机制:当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden区满,Survivor满不会引发GC。
  • Full GC触发机制:当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代,当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载
  • 工作特点:理论上GC过程中会频繁收集Young区,很少收集Old区,基本不动Perm区(元空间/方法区)

标记不可达对象

  • 引用计数法:引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到循环指向的存在。
  • 可达性分析(GC ROOTS算法)简单理解,可以理解为堆外指向堆内的引用

垃圾回收的三种方式

  1. 清除:第一种是清除(sweep),即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中
  2. 压缩:第二种是压缩(compact),即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间
  3. 复制第三种则是复制(copy),即把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内
    • 总结:回收死亡对象的内存共有三种方式,分别为:会造成内存碎片的清除、性能开销较大的压缩、以及堆使用效率较低的复制。

垃圾回收算法

  1. 标记复制算法 因此Minor GC使用的则是标记-复制算法,理性情况下:,Eden 区中的对象基本都死亡了,那么需要复制的数据将非常少,因此采用这种标记 - 复制算法的效果极好。
    • 优点:不会产生内存碎片
    • 缺点:需要双倍空间,浪费内存
  2. 标记清除算法 老年代一般是由标记清除或者是标记清除与标记压缩的混合实现
    • 优点:不需要双倍空间
    • 缺点:1.会产生内存碎片 2.需要停止整个应用程序 3.需要维护内存碎片的地址列表
  3. 标记压缩算法
    • 优点:1.不需要双倍空间,也不会产生内存碎片 2.不需要维护内存碎片的列表,只需要记录内存的起始地址即可
    • 缺点:开销大,需要更新对象的地址
  4. 标记清除压缩算法 标记清除压缩(Mark-Sweep-Compact)算法是标记清除算法和标记压缩算法的结合算法。其原理和标记清除算法一致,只不过会在多次GC后,进行一次Compact压缩操作!

常见问题

JVM内存分几个区,每个区的作用是什么?

  • 方法区
    • 有时候也称为永久代,该区很少发生垃圾回收,在这里进行GC主要是对方法区里的常量池和对类型的卸载。
    • 方法区用来存放已经被虚拟机加载的类信息,常量,静态变量和即时编译器编译后的代码等数据。
    • 该区域是被线程共享的。
    • 静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中
  • Java栈
    • 8种基本类型的变量+对象的引用变量+实例方法都是在栈内存中分配
    • 是线程私有的,它的生命周期与线程相同
    • 在栈区域规定了两种异常状态:如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  • 本地方法栈
    • 本地方法栈和Java栈类似,只不过本地方法栈为Native方法服务
    • 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。
    • 堆区被所有线程共享
  • 程序计数器
    • 内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令。该内存区域是唯一一个Java虚拟机规范没有规定任何OOM情况的区域。
    • 线程私有的

Java类加载过程

  • 加载
  • 验证
  • 解析
  • 初始化

如何判断一个对象是否存活

  • 引用计数法
    • 引用计数法就是如果一个对象没有被任何引用指向,则视之为垃圾。这种方法的缺点就是不能检测到循环指向的存在。
  • 可达性算法(引用链法)
  • 根据搜索算法的基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链时,则证明此对象时不可用的。

Java中的垃圾收集的方法有哪些

  • 在上面

什么是类加载器,类加载器有哪些

  • 类加载器,即ClassLoader,它负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定

  • 在上面

简述Java内存分配以及垃圾回收策略Minor GC,Major GC(Full GC)

  • 在上面

JUC

  • 线程和进程

    • 程序(program)是为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码。
    • 进程(process)是程序的一次执行过程,或是正在运行的一个程序。进程是一个动态的过程,即有它自身的产生,存在和消亡的过程。每个Java程序都有一个隐含的主程序,即main 方法
    • 线程(thread)是进程内部的一条具体的执行路径。若一个程序可同一时间执行多个线程,就是支持多线程的。
    • 总结:程序是静态的,程序运行后变为一个进程,一个进程内部可以有多个线程同时执行。进程是所有线程的集合,每一个线程是进程中的一条路径。线程是直接去竞争cpu资源的。
  • 线程安全

    • 当多个线程同时共享同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。
  • 线程安全的解决方式

    • 使用多线程之间同步或使用锁(lock)可以解决线程安全问题。

    • 使用同步代码块

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      /*
      synchronized(同一个对象){
      可能会发生线程冲突问题
      }
      */


      class Ticket
      {
      private int number = 30;


      public synchronized void sale() {

      if (number > 0) {
      System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
      }
      }

      }


      public class SaleTicket
      {
      public static void main(String[] args)
      {
      //创建资源对象
      Ticket tc = new Ticket();
      //创建AA线程、
      new Thread(new Runnable() {

      @Override
      public void run() {
      for (int i = 0; i < 40; i++) {
      //卖票
      tc.sale();
      }
      }
      }, "AA").start();
      //创建BB线程、
      new Thread(new Runnable() {

      @Override
      public void run() {
      for (int i = 0; i < 40; i++) {
      //卖票
      tc.sale();
      }
      }
      }, "BB").start();
      //创建CC线程、
      new Thread(new Runnable() {

      @Override
      public void run() {
      for (int i = 0; i < 40; i++) {
      //卖票
      tc.sale();
      }
      }
      }, "CC").start();
      }
      }

  • 使用Lock解决线程安全

    • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。Lock实现提供更广泛的锁定操作可以比使用 synchronized获得方法和声明更好。

    • 相比于synchronized的有系统获取锁和释放锁,Lock需要自己动手实现加锁和释放锁,因此会更加灵活。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      class Ticket
      {
      private int number = 30;

      //创建锁
      Lock lock = new ReentrantLock();

      public void sale() {
      //上锁
      lock.lock();
      try {
      if (number > 0) {
      System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
      }
      } finally {
      //解锁
      lock.unlock();
      }

      }
      }

      public class SaleTicket
      {
      public static void main(String[] args)//main所有程序的入口
      {
      //创建资源对象
      Ticket tc = new Ticket();
      //创建AA线程、
      new Thread(new Runnable() {

      @Override
      public void run() {
      for (int i = 0; i < 40; i++) {
      //卖票
      tc.sale();
      }
      }
      }, "AA").start();
      //创建BB线程、
      new Thread(new Runnable() {

      @Override
      public void run() {
      for (int i = 0; i < 40; i++) {
      //卖票
      tc.sale();
      }
      }
      }, "BB").start();
      //创建CC线程、
      new Thread(new Runnable() {

      @Override
      public void run() {
      for (int i = 0; i < 40; i++) {
      //卖票
      tc.sale();
      }
      }
      }, "CC").start();
      }
      }

多线程的创建

  • 继承Thread类
    • 定义子类继承Thread类
    • 子类重写Thread类中的run方法
    • 创建Thread子类对象,即创建了线程对象
    • 调用线程对象start方法启动线程,默认调用run方法
      • 注意:如果只是调用run方法,则此时会在调用该方法的线程中来执行,而不是另启动一个线程
  • 实现Runnable接口
    • 定义子类,实现Runnable接口
    • 子类中重写Runnable接口中的run 方法
    • 通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
    • 调用Thread类的start方法启动线程,其最终调用Runnable子类接口的run方法。
  • 实现Runnable接口避免了单继承的局限性,多个线程可以共享同一个接口子类的对象,非常适合多个相同线程来处理同一份资源。
  • 使用Callable 接口
  • 与Runnable比较:
    • 相比于run方法可以有返回值
    • 可以抛出异常
    • 支持泛型的返回值
    • 落地方法是call方法
  • 分布式锁
    • 当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。分布式锁可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存,如 Redis,通过set (key,value,nx,px,timeout)方法添加分布式锁。
  • 分布式事务
    • 分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!