深入理解 Java虚拟机

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

​ Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

JDK(Java Development Kit) 是 Java 语言的软件开发工具包(SDK)。 (JDK=JRE+SDK)

JRE(Java Runtime Environment ) 是Java运行环境。(JRE=JVM+Java核心类库和支持文件 )

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

先看一下 JVM 模型图

在这里插入图片描述

运行时数据区(Runtime Data Area)

  • 线程共享
    • 方法区
  • 线程私有
    • 虚拟机栈 针对native方法
    • 本地方法栈
    • 程序计数器 内存占用最小 这里不会抛出内存不够的异常

直接内存

  • 不是虚拟机运行时的一部分,可以直接访问堆外的内存;所以当内存空间无法动态扩展的时候就会出现OutOfMemoryError异常;
  • 堆内存与直接内存使用的比较
    • 堆内存: 分配快 读写慢
    • 直接内存: 分配慢 读写快
    • 原因:
      • 非直接内存作用链:本地IO –>直接内存–>非直接内存–>直接内存–>本地IO
      • 直接内存作用链:本地IO–>直接内存–>本地IO

首先我们先看一下运行时数据区的各个模块都是干什么的:

  • 线程计数器
    • 是一块较小的内存空间,用来指定当前线程执行字节码的行数,每个线程计数器都是私有的,因为每个线程都需要记录执行的行数;
    • 这里解释一下为什么每个线程都需要一个线程计数器:JVM的多线程是通过线程轮流切换分配执行时间来实现的,在任何时刻,每个处理器都只会执行一个线程中的指令,当线程进行切换的时,为了线程能恢复当正确的位置,所以每个线程必须有个独立的线程计数器,这样才能保证线程之间不互相影响。
    • 这里注意下:如果线程执行是一个Java方法的时候,计数器记录的是虚拟机字节码指令的地址;当执行的是Native的方法的时候,计数器指令为空;该内存区域是Java虚拟机唯一没有规定任何OutOfMemoryError的区域。
  • Java虚拟栈
    • 这个也是一个线程私有的,生命周期与线程是同步的,每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出入口等信息,每个方法的调用到执行完成的过程就是一个栈帧入栈到出栈的过程;
    • 这里解释一下局部变量表:局部变量表存储方法相关的局部变量,包括基本数据,对象引用和返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。这部分东西我还想等下一篇博客的时候我想仔细说一下字节码的执行过程;
    • 虚拟机栈规定了2种异常情况,一种是线程请求栈的深度大于虚拟机栈所允许的深度,这时候将会抛出StackOverflowError异常,如果当Java虚拟机允许动态扩展虚拟机栈的时候,当扩展的时候没办法分配到内存的时候就会报OutOfMemoryError异常;
  • 本地方法栈
    • 与虚拟机栈执行的基本相同,唯一的区别就是虚拟机栈是执行Java方法的,本地方法栈是执行native方法的;
  • Java堆
    • 堆区是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的内存区域,主要存储对象的实例。
  • 方法区
    • 这个也是线程共享的内存区域,存储被虚拟机加载的类信息、常量、静态变量、即时编译的代码数据等;
    • 方法区在物理上也是不需要连续的,可以选择固定大小或者扩展的大小,还可以选择不实现垃圾收集,方法区的垃圾回收是比较少的,这就是方法区为什么被称为永久区的原因,但是方法区也是可以执行回收的,该区域主要是针对常量池和类型的卸载;在方法区也规定当方法区无法满足内存分布的时候,将会抛出OutOfMemoryError异常;
    • 运行时常量是方法区的一部分,常量池主要用于存放编译生成的各种字面量和符合引用,由于常量池属于方法区的一部分,所以当常量池没有内存空间的时候就抛出OutOfMemoryError异常;

类文件加载的具体过程:

  • 加载: 本地系统(classpath) 网络 jar包 等

  • 链接:

    • 验证: 验证加载的.class是否正确
    • 准备:
    • 解析:
  • 初始化: 静态变量 等 赋初始值 分配静态变量的空间

  • 使用: Class Method Field等

  • 卸载:


垃圾回收

如何确定某个对象是垃圾:

  • 引用计数法:
    • 对象中添加一个引用计数器,每当有一个地方引用计数器就增加1,引用失效就减少1,计数器为0就不可用;缺点就在于无法处理对象直接相互引用的问题,因为相互引用以后无法使计数器为0,所以无法回收;
    • 特点: 简单 容易实现
  • 可达性分析:
    • GC roots和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
      • 显式地将对象的唯一强引用指向新的对象。
      • 显式地将对象的唯一强引用赋值为Null。
      • 局部引用所指向的对象(如,方法内对象)。

典型的垃圾回收算法:

  • 标记-清除算法(Mark-Sweep)
    • 首先标记所有需要回收的对象,标记完成后统一回收
    • 特点:简单
    • 缺点:内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
  • 复制算法(Copying)
    • 内存容量将内存划分为等大小的两块 标记-整理算法(Mark-Compact)
    • 缺点:可用内存被压缩到了原本的一半
  • 分代收集算法(Generational Collection)
    • 标记后不是清理对象,而是将存活对象移向内存的一端,直接清除另一端

典型的垃圾收集器:

  • Serial/Serial Old
    • 最古老的收集器,是一个单线程收集器,用它进行垃圾回收时,必须暂停所有用户线程。
    • Serial是针对新生代的收集器,采用Copying算法;
    • 而Serial Old是针对老生代的收集器,采用Mark-Compact算法。
    • 优点是简单高效,缺点是需要暂停用户线程。
  • ParNew
    • Seral/Serial Old的多线程版本,使用多个线程进行垃圾收集。
  • Parallel Scavenge
    • 新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。

关于引用类型

  • 强引用 StrongReference
    • 如果一个对象具有强引用,那么垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止。(out of memory)
  • 软引用 SoftReference
    • 如果一个对象只具有软引用,那么垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收这些对象。软引用对象被回收后,Java 虚拟机会把这个软引用加入到与之关联的引用队列中。
    • 应用:构建敏感数据的缓存
  • 弱引用 WeakReference
    • 如果一个对象只具有弱引用,那么垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。与软引用相同,弱引用对象被回收后,Java 虚拟机会把这个弱引用加入到与之关联的引用队列中。
    • 应用:解决内存泄露问题
  • 虚引用 PhantomReference
    • 虚引用并不决定对象生命周期,如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。与软引用和弱引用不同的是,虚引用必须关联一个引用队列。

猜你喜欢

转载自blog.csdn.net/weixin_43650254/article/details/88776300