Java虚拟机

Java虚拟机

1.运行时数据区域

java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域。这些区域有不同的用途。

程序计数器

程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器

字节码解释器工作时,通过改变程序计数器的值选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖程序计数器完成。

为了线程切换后能恢复到正确的执行位置,每个线程都需要有独立的程序计数器由于每个线程的程序计数器是独立存储的,因此各线程之间的程序计数器互不影响,这类内存区域被称为线程私有的内存区域。

程序计数器是唯一不会出现 OutOfMemoryError 的内存区域。

Java 虚拟机栈

虚拟机栈描述的是Java 方法执行的内存模型是线程私有的。

和程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。每个方法被执行的时候会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。

一个方法被调用直至执行完成的过程对应一个栈帧在虚拟机中从入栈到出栈的过程。

局部变量表存放编译器可知的各种基本数据类型、对象引用类型和返回地址类型。

Java 虚拟机栈会出现两种异常。

  • 如果虚拟机栈不可以动态扩展,当线程请求的栈深度大于虚拟机所允许的深度时,将抛出 StackOverflowError 异常;
  • 如果虚拟机栈可以动态扩展,当无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈和虚拟机栈的作用相似。

区别在于:

  • 虚拟机栈为虚拟机执行 Java 方法服务,

  • 本地方法栈为虚拟机使用到的本地方法服务。

有的虚拟机(如 HotSpot 虚拟机)把本地方法栈和虚拟机栈合二为一。

Java 堆

Java 堆是被所有线程共享的内存区域,其目的是存放对象实例,几乎所有的对象实例都在堆中分配内存。

对于大多数应用而言,Java 堆是 Java 虚拟机管理的内存中最大的一块。

Java 堆是垃圾回收器管理的主要内存,因此也称为 GC 堆(Garbage Collected Heap)。

从垃圾回收的角度,由于现代编译器基本都采用分代垃圾回收算法,所以 Java 堆还可以分成新生代和老年代,新生代又可以细分成 Eden 区、From Survivor 区、To Survivor 区等。细分成多个空间的目的是更好地回收内存或者更快地分配内存。

方法区

方法区用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。被所有线程共享的内存区域。

和 Java 堆一样,方法区也是被所有线程共享的内存区域。

当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

JDK 1.8 将方法区彻底移除,取而代之的是元空间,元空间使用的是直接内存。

运行时常量池

运行时常量池是方法区的一部分,用于存放编译器生成的字面量和符号引用

Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息,用于存放编译器生成的字面量和符号引用,这些信息将在类加载后存放到方法区的运行时常量池中。

运行时常量池也受到方法区内存的限制,当常量池无法再申请到内存时将抛出 OutOfMemoryError 异常。

直接内存

直接内存不是虚拟机运行时数据区域的一部分,也不是虚拟机规范中定义的内存区域。

但是这部分也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。

本机直接内存的分配不受到 Java 堆大小的限制,但是直接内存仍然受到本机总内存地大小及处理器寻址空间的限制。如果各个内存区域的总和大于物理内存限制,就会导致动态扩展时出现 OutOfMemoryError 异常。


2.垃圾回收

垃圾回收,顾名思义就是释放垃圾占用的空间,从而提升程序性能,防止内存泄露。当一个对象不再被需要时,该对象就需要被回收并释放空间。

Java 内存运行时数据区域包括程序计数器、虚拟机栈、本地方法栈、堆等区域。

其中,程序计数器、虚拟机栈和本地方法栈都是线程私有的,当线程结束时,这些区域的生命周期也结束了,因此不需要过多考虑回收的问题。

而堆是虚拟机管理的内存中最大的一块,堆中的内存的分配和回收是动态的,垃圾回收主要关注的是堆空间

调用垃圾回收器的方法

调用垃圾回收器的方法是 gc,该方法在 System 类和 Runtime 类中都存在。

  • Runtime 类中,方法 gc 是实例方法,方法 System.gc 是调用该方法的一种传统而便捷的方法。
  • System 类中,方法 gc 是静态方法,该方法会调用 Runtime 类中的 gc 方法。
  • 其实,java.lang.System.gc 等价于 java.lang.Runtime.getRuntime.gc 的简写,都是调用垃圾回收器。

方法 gc 的作用是提示 Java 虚拟机进行垃圾回收,该方法由系统自动调用,不需要人为调用。

该方法被调用之后,由 Java 虚拟机决定是立即回收还是延迟回收。

finalize 方法

该方法在 Object 类中被定义,在释放对象占用的内存之前会调用该方法。

与垃圾回收有关的另一个方法是 finalize 方法。该方法的默认实现不做任何事,如果必要,子类应该重写该方法,一般建议在该方法中释放对象持有的资源。

判断对象是否可回收

垃圾回收器在对堆进行回收之前,首先需要确定哪些对象是可回收的。常用的算法有两种,引用计数算法根搜索算法

引用计数算法

引用计数算法给每个对象添加引用计数器,用于记录对象被引用的计数,引用计数为 0 的对象即为可回收的对象。

虽然引用计数算法的实现简单,判定效率也很高,但是引用计数算法无法解决对象之间循环引用的情况。

如果多个对象之间存在循环引用,则这些对象的引用计数永远不为 0,无法被回收。

因此 Java 语言没有使用引用计数算法。

根搜索算法

主流的商用程序语言都是使用根搜索算法判断对象是否可回收。

根搜索算法的思路是,从若干被称为 GC Roots 的对象开始进行搜索,不能到达的对象即为可回收的对象。

在 Java 中, GC Roots 一般包含下面几种对象:

  • 虚拟机栈中引用的对象;

  • 本地方法栈中的本地方法引用的对象;

  • 方法区中的类静态属性引用的对象;

  • 方法区中的常量引用的对象。

引用的分类

在 JDK 1.2 之后,Java 将引用分成四种,按照引用强度从高到低的顺序依次是:强引用、软引用、弱引用、虚引用

引用计数算法和根搜索算法都需要通过判断引用的方式判断对象是否可回收。

  • 强引用是指在程序代码中普遍存在的引用。

    • 垃圾回收器永远不会回收被强引用关联的对象。
  • 软引用描述还有用但并非必需的对象。

    • 只有在系统将要发生内存溢出异常时,被软引用关联的对象才会被回收。

    • 在 JDK 1.2 之后,提供了 SoftReference 类实现软引用。

  • 弱引用描述非必需的对象,其强度低于软引用。

    • 被弱引用关联的对象只能存活到下一次垃圾回收发生之前,当垃圾回收器工作时,被弱引用关联的对象一定会被回收。

    • 在 JDK 1.2 之后,提供了 WeakReference 类实现弱引用。

  • 虚引用是最弱的引用关系。

    • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。
    • 为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
    • 在 JDK 1.2 之后,提供了 PhantomReference 类实现虚引用。

垃圾回收算法

标记—清除算法

标记—清除算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记—清除算法是最基础的垃圾回收算法,后续的垃圾收集算法都是基于标记—清除算法进行改进而得到的。

标记—清除算法有两个主要缺点。

  1. 效率问题,标记和清除的效率都不高
  2. 空间问题,标记清除之后会产生大量不连
  3. 续的内存碎片,导致程序在之后的运行过程中无法为较大对象找到足够的连续内存

复制算法

复制算法是将可用内存分成大小相等的两块,每次只使用其中的一块,当用完一块内存时,将还存活着的对象复制到另外一块内存,然后把已使用过的内存空间一次清理掉

复制算法解决了效率问题。由于每次都是对整个半区进行内存回收,因此在内存分配时不需要考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。

复制算法的优点是实现简单,运行高效,缺点是将内存缩小为了原来的一半,以及在对象存活率较高时复制操作的次数较多,导致效率降低。

标记—整理算法

标记过程与标记—清除算法一样,但后续步骤不是直接回收被标记的对象,而是让所有存活的对象都向一端移动,然后清除边界以外的内存。

标记—整理算法是根据老年代的特点提出的。

分代收集算法

分代收集算法根据对象的存活周期不同将内存划分为多个区域,对每个区域选用不同的垃圾回收算法。

一般把 Java 堆分为新生代和老年代。

  • 在新生代中,大多数对象的生命周期都很短,因此选用复制算法
  • 在老年代中,对象存活率高,因此选用标记—清除算法标记—整理算法

分配内存与回收策略

Java 堆可以分成新生代和老年代,新生代又可以细分成 Eden 区、From Survivor 区、To Survivor 区等。

对于不同的对象,有相应的内存分配规则。

Minor GC 和 Full GC

Minor GC 指发生在新生代的垃圾回收操作。

  • 因为大多数对象的生命周期都很短,因此 Minor GC 会频繁执行,一般回收速度也比较快。

Full GC 也称 Major GC,指发生在老年代的垃圾回收操作。

Major GC 是对老年代进行GC,Full GC一般是对整个堆区和元空间(方法区)进行GC。

  • 出现了 Full GC,经常会伴随至少一次的 Minor GC。
  • 老年代对象的存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。

对象优先在 Eden 区分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

大对象对于虚拟机的内存分配而言是坏消息,经常出现 大对象会导致内存还有不少空间时就提前触发垃圾回收 以获取足够的连续空间分配给大对象。

将大对象直接在老年代中分配的目的是避免在 Eden 区和 Survivor 区之间出现大量内存复制。

长期存活的对象进入老年代

虚拟机采用分代收集的思想管理内存,因此需要识别每个对象应该放在新生代还是老年代。

虚拟机给每个对象定义了年龄计数器。

对象在 Eden 区出生之后,如果经过第一次 Minor GC 之后仍然存活,将进入 Survivor 区,同时对象年龄变为 1,对象在 Survivor 区每经过一次 Minor GC 且存活,年龄就增加 1,增加到一定阈值时则进入老年代(阈值默认为 15)。

动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到阈值才能进入老年代。

如果在 Survivor 区中相同年龄的所有对象的空间总和大于 Survivor 区空间的一半,则年龄大于或等于该年龄的对象直接进入老年代

空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间总和,如果这个条件成立,那么 Minor GC 可以确保是安全的。

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。

posted @ 2022-05-27 09:30  萝卜不会抛异常  阅读(39)  评论(0编辑  收藏  举报