JVM笔记

发布时间:2023年12月21日

JVM

运行时数据区

方法区

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

  • 它有个别命叫 Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛 OutOfMemoryError 异常。

  • java 堆是 java 虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。

  • 在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

  • java 堆是垃圾收集器管理的主要区域,因此也被成为“GC 堆”。

  • 从内存回收角度来看 java 堆可分为:新生代和老生代。

  • 从内存分配的角度看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区。

  • 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。

  • 根据 Java 虚拟机规范的规定,java 堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

程序计数器

  • 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号)

  • 由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定 OutOfMemoryError 情况的区域。

  • 线程是要执行指令的,基于时间片,如果被打断会被挂起,等其它线程结束后,根据程序计数器,再接着执行刚才的任务。

Java 虚拟机栈

  • 作用

    • java 虚拟机是线程私有的,它的生命周期和线程相同。

    • 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  • 线程的最小单位:栈帧

    • 局部变量表:是用来存储我们临时 8 个基本数据类型、对象引用地址、returnAddress 类型。(returnAddress 中保存的是 return 后要执行的字节码的指令地址。)

    • 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去

    • 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。

    • 出口:出口是什么呢,出口正常的话就是 return 不正常的话就是抛出异常喽

  • 思考题

    • 一个方法调用另一个方法,会创建很多栈帧吗?

      • 会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面
    • 栈指向堆是什么意思?

      • 栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址,堆中的数据等下讲
    • 递归的调用自己会创建很多栈帧吗?

      • 递归的话也会创建多个栈帧,就是一直排下去

Java 内存结构

直接内存

  • 介绍

    • 直接内存不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域。但是既然是内存,肯定还是受本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。

    • 在 JDK1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通脱一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native(本地)堆中来回复制数据。

  • 直接内存与堆内存差异

    • 直接内存申请空间耗费很高的性能,堆内存申请空间耗费比较低

    • 直接内存的 IO 读写的性能要优于堆内存,在多次读写操作的情况相差非常明显

JVM 字节码执行引擎

虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。

“虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要 JVM 字节码执行引擎编译成机器码后才可在物理机上执行。

垃圾回收机制

介绍

  • 程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java 虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。

    • 什么是引用

      • 引用是一个对象别名,与被引用的对象共享同一块内存区域。

        • 在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
  • GC 是不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个 Java 程序中的执行是自动的,不能强制执行清楚那个对象,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用 System.gc 方法来 “建议” 执行垃圾收集器,但是他是否执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

finalize 方法作用

  • finalize()方法是在每次执行 GC 操作之前时会调用的方法,可以用它做必要的清理工作。

  • 它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

新生代、老年代、永久代(方法区)

  • 区别

    • Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。

    • 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。

    • 老年代就一个区域。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

    • 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

    • 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1: 2 ( 该值可以通过参数 –XX: NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。

    • 其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 From Survivor 和 ToSurvivor ,以示区分。

    • 默认的,Edem : From Survivor : To Survivor = 8 : 1 : 1 ( 可以通过参数 –XX: SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,From Survivor = To Survivor = 1/10 的新生代空间大小。

    • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。

    • 因此,新生代实际可用的内存空间为 9/10 ( 即 90% )的新生代空间。

    • 永久代就是 JVM 的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。

  • 为什么要这样分代

    • 其实主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法:

    • 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

    • 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。

  • GC 触发条件

    • Minor

      • eden 区满时,触发 MinorGC。即申请一个对象时,发现 eden 区不够用,则触发一次 MinorGC。

      • 新创建的对象大小 > Eden 所剩空间

    • Major 和 full

      • 每次晋升到老年代的对象平均大小 > 老年代剩余空间

      • MinorGC 后存活的对象超过了老年代剩余空间

      • 永久代空间不足

      • 执行 System.gc()

      • CMS GC 异常

      • 堆内存分配很大的对象

  • 判断对象是否存活

    • 引用计数法:
      无法解决循环引用问题
      一般 java 中不使用

    • 可达性分析法

      • 该种方法是从 GC Roots 开始向下搜索,搜索所走过的路径为引用链。当一个对象到 GC Roots 没用任何引用链时,则证明此对象是不可用的,表示可以回收

      • 可以作为 GC Roots 的对象

        • 1、虚拟机栈(栈帧中的本地变量表)中引用的对象;

        • 2、方法区中类静态属于引用的对象;

        • 3、方法区中常量引用的对象;

        • 4、本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。

    • 标记–清除算法

      • 标记清除算法的优点:

        • 是可以解决循环引用的问题

        • 必要时才回收(内存不足时)

      • 标记清除算法的缺点:

        • 回收时,应用需要挂起,也就是 stop the world。

        • 标记和清除的效率不高,尤其是要扫描的对象比较多的时候

        • 会造成内存碎片(会导致明明有内存空间, 但是由于不连续, 申请稍微大一些的对象无法做到),

      • 标记清除算法的应用场景:

        • 该算法一般应用于老年代, 因为老年代的对象生命周期比较长。
      • 介绍

        • 为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。

        • 分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

    • 标记–整理(压缩)算法

      • 标记–整理算法优点:

        • 解决标记清除算法出现的内存碎片问题,
      • 标记–整理算法缺点:

        • 压缩阶段,由于移动了可用对象,需要去更新引用。
      • 标记–整理算法应用场景:

        • 该算法一般应用于老年代, 因为老年代的对象生命周期比较长。
      • 介绍

        • 标记清除算法和标记压缩算法非常相同,但是标记压缩算法在标记清除算法之上解决内存碎片化(有些人叫 “标记整理算法” 为 “标记压缩算法”)

        • 标记-整理法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

    • 复制算法

      • 复制算法的优点:

        • 在存活对象不多的情况下,性能高,能解决内存碎片和 java 垃圾回收算法之-标记清除 中导致的引用更新问题。
      • 复制算法的缺点::

        • 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差。
      • 复制算法的应用场景:

        • 复制算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。

        • jvm 将 Heap(堆)内存划分为新生代与老年代。又将新生代划分为 Eden 与 2 块 Survivor Space(幸存者区) ,然后在 Eden –> Survivor Space 与 To Survivor 之间实行复制算法。

        • 不过 jvm 在应用复制算法时,并不是把内存按照 1: 1 来划分的,这样太浪费内存空间了。一般的 jvm 都是 8: 1。也即是说, Eden 区: From 区: To 区域的比例是始终有 90%的空间是可以用来创建对象的, 而剩下的 10%用来存放回收后存活的对象。

      • 介绍

        • 该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。

        • 这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。

调优

在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,

这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。

初始堆值和最大堆内存内存越大,吞吐量就越高,

但是也要根据自己电脑(服务器)的实际内存来比较。

最好使用并行收集器, 因为并行收集器速度比串行吞吐量高,速度快。

当然,服务器一定要是多线程的

设置堆内存新生代的比例和老年代的比例最好为 1: 2 或者 1: 3。

默认的就是 1: 2

减少 GC 对老年代的回收。设置生代带垃圾对象最大年龄,进量不要有大量连续内存空间的 java 对象,因为会直接到老年代,内存不够就会执行 GC

类加载器

文章来源:https://blog.csdn.net/weixin_61281255/article/details/135137892
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。