【JVM】

JVM

内存区域划分

JVM作为应用程序,启动的时候要从操作系统申请内存,会自动根据需要把申请到的内存划分成几个部分,每个部分有各自不同的功能

  • 具体划分

在这里插入图片描述

    1. 在JVM中,每个线程都有一个私有的Java虚拟机栈(Java Virtual Machine Stack)。每个方法在执行时都会创建一个栈帧(Stack Frame),栈帧包含了方法的局部变量、操作数栈、动态链接、方法出口等信息。
    2. Java虚拟机栈的主要作用是存储方法的调用和返回信息。每当一个方法被调用时,JVM就会在栈中创建一个新的栈帧,用于存储该方法的局部变量和中间结果。栈帧的大小在编译期间就确定了,包括了方法参数、局部变量和临时变量等。
    3. 栈的大小是有限的,并且在JVM启动时就已经确定了。当线程的方法调用层次过深时,栈可能会溢出,导致StackOverflowError错误。另外,栈还可以进行动态扩展,如果栈空间不足时,JVM会自动扩展栈的大小。
    4. 栈的生命周期与线程一致,线程创建时会分配一个新的栈,并且在线程结束时栈会被销毁。因此,栈上的数据是线程私有的,不同线程之间的栈数据是相互隔离的。
    5. 需要注意的是,Java虚拟机栈与堆(Heap)是两个不同的概念。栈用于存储方法调用和返回信息,而堆用于存储对象实例和数组等动态分配的内存。
  1. 在JVM中,堆(Heap)是用于存储对象实例和数组等动态分配的内存区域。所有通过new关键字创建的对象都会被分配在堆上。
  2. 堆是JVM管理的最大一块内存区域,其大小在JVM启动时就被确定,并且可以通过启动参数进行调整。堆内存的分配和释放由Java虚拟机自动进行,程序员无需手动管理。
  3. 堆内存被划分为多个线程共享的部分和线程私有的部分。线程共享的部分称为"共享堆",用于存储类信息、静态变量、常量等。线程私有的部分称为"线程堆栈",用于存储线程创建的对象实例和数组等。
  4. 堆内存的分配方式是动态的,采用垃圾回收机制进行内存的自动回收。当对象不再被引用时,垃圾回收器会自动识别并回收该对象占用的内存,以便后续的对象分配使用。
  5. 需要注意的是,堆内存的使用情况会对程序的性能产生影响。如果堆内存不足,可能会触发垃圾回收频繁进行,导致程序的执行速度变慢。因此,在设计和编写程序时,需要合理管理对象的生命周期,避免不必要的内存占用。同时,可以通过调整堆的大小和优化垃圾回收策略等方式来优化程序的性能。
  • 元数据区(方法区)

    1.在JVM中,元数据区(Metadata Area)是一块特殊的内存区域,用于存储类的元数据信息。

    2.元数据指的是描述类的结构信息,包括类的名称、访问修饰符、字段信息、方法信息、父类、接口、注解等。这些元数据信息在程序运行期间是需要被动态加载和访问的。

    3.元数据区是JVM中的一个逻辑概念,实际上并没有单独的物理内存空间。它是在JVM内部数据结构中保存的,通常是存储在方法区(Method Area)中。

    4.元数据区的主要作用是支持Java程序的反射机制和动态代理等高级特性。通过元数据信息,Java程序可以在运行时获取类的结构信息,并对类进行操作和调用。例如,可以通过反射获取类的字段和方法,并进行动态的创建对象、调用方法等操作。

    5.在JDK 8及之前的版本,元数据区一般位于方法区中。而在JDK 8之后的版本,方法区被替换为元空间(Metaspace),元数据区的元数据信息存储在元空间中。

    6.需要注意的是,元数据区的大小是有限制的,如果加载的类和元数据信息过多,可能会导致元数据区溢出或内存不足的问题。因此,在进行类的动态加载和使用反射等操作时,需要注意合理管理和控制元数据的使用。

类加载

1.类加载的过程

JVM的类加载过程通常包括以下几个步骤:

  1. 加载(Loading):将类的字节码文件加载到内存中。类的字节码可以来自本地文件系统、网络等各种途径。加载后的字节码将存储在方法区(或者在JDK 8及之后的版本中的元空间)。

  2. 验证(Verification):验证被加载的字节码文件的正确性和安全性。验证过程包括对字节码的结构检查、语义检查等。通过验证可以防止在运行时出现安全问题和错误。

  3. 准备(Preparation):为被加载的类的静态变量分配内存空间,并设置默认初始值(0)。这些静态变量将存储在方法区(或者在JDK 8及之后的版本中的元空间)。

  4. 解析(Resolution):初始化字符串常量,将被加载的类中的符号引用替换为直接引用。符号引用指的是用于在运行时定位、访问方法、字段等的符号名称。解析过程将符号引用转换为直接引用,以便在后续的执行过程中快速定位和访问相关的方法、字段等。

  5. 初始化(Initialization):为类的静态变量赋予真正的初始值,并执行类的静态初始化代码块。初始化过程是类加载过程的最后一步,也是类被首次使用的时机。在初始化阶段,可以执行一些静态初始化操作,如初始化静态变量、执行静态代码块等。

  6. 使用(Usage):在类加载完成并初始化后,就可以被其他程序使用了。可以通过创建对象、调用方法、访问字段等方式使用该类。

  7. 卸载(Unloading):如果某个类不再被程序使用,并且不再有任何引用指向它,JVM会将该类从内存中卸载,释放相关资源。

需要注意的是,类加载过程并不是一次性完成的,而是按需加载和按需初始化的。在程序运行过程中,如果遇到新的类被使用或者已加载的类被使用,JVM会进行相应的类加载和初始化操作。

在这里插入图片描述

解析:针对于字符串常量,

在类加载之前,是在.class文件中的,此时的“引用”记录的不是字符串常量真正的地址,而是偏移量(或者是个占位符)

在类加载之后,才真正把字符串常量放到内存中,此时才有“内存地址”,这个引用才会被赋值为真正的“内存地址”

2…class文件的规范

.class文件是Java字节码文件的标准格式,遵循Java虚拟机规范(Java Virtual Machine Specification)。

下面是.class文件的规范要点:

  1. 魔数(Magic Number):.class文件的前四个字节是一个固定的魔数,用于标识该文件是否为有效的.class文件。魔数为0xCAFEBABE。

  2. 版本号(Version Number):紧随魔数之后的两个字节表示.class文件的版本号。其中第一个字节表示主版本号,第二个字节表示次版本号。Java版本与.class文件版本的对应关系可以在Java虚拟机规范中找到。

  3. 常量池(Constant Pool):紧随版本号之后的两个字节表示常量池的大小,紧随其后的是常量池表。常量池用于存储类、接口、字段、方法等的符号引用、字面量常量等信息。

  4. 访问标志(Access Flags):紧随常量池之后的两个字节表示类或接口的访问标志,用于描述类的访问级别、是否为接口、是否为抽象类等。

  5. 类索引、父类索引和接口索引集合:紧随访问标志之后的两个字节表示类的索引,紧随其后的两个字节表示父类的索引,然后是接口索引集合。

  6. 字段表(Field Table):紧随接口索引集合之后的两个字节表示字段表的数量,紧随其后的是字段表。字段表用于描述类中声明的字段信息,如字段的访问标志、名称、类型等。

  7. 方法表(Method Table):紧随字段表之后的两个字节表示方法表的数量,紧随其后的是方法表。方法表用于描述类中声明的方法信息,如方法的访问标志、名称、参数列表、返回类型等。

  8. 属性表(Attribute Table):紧随方法表之后的两个字节表示属性表的数量,紧随其后的是属性表。属性表用于描述类、字段、方法等的附加信息,如源文件名、行号表、注解等。

这些是.class文件的主要结构和规范要点。.class文件通过这些信息来描述和存储Java类的结构、方法、字段等信息,供Java虚拟机解析和执行。具体的规范细节可以参考Java虚拟机规范中的相关章节。

3.类加载的时机

一个类在以下情况下会被加载:

  1. 类被实例化:当程序创建类的实例对象时,需要先加载类的字节码文件,然后创建对象。例如,通过new关键字创建对象时会触发类加载过程。

  2. 访问类的静态成员:当程序访问类的静态成员(静态变量或静态方法)时,需要先加载类的字节码文件。静态成员属于类级别,与对象无关。

  3. 调用类的静态方法:当程序调用类的静态方法时,需要先加载类的字节码文件。静态方法是属于类的,可以通过类名直接调用。

  4. 使用类的静态变量:当程序使用类的静态变量时,需要先加载类的字节码文件。静态变量也是属于类的,可以通过类名直接访问。

  5. 使用反射机制:当程序使用Java的反射机制来动态创建类的实例、调用方法或访问字段时,会触发类的加载。

  6. 继承关系:当一个类继承自另一个类时,子类在被实例化或访问其静态成员时,会触发父类的加载。父类会在子类加载之前被加载。

需要注意的是,类加载是按需进行的,即在需要使用某个类时才会触发该类的加载过程。JVM会使用一种叫做"Lazy Loading"(懒汉模式类似)的策略,即在首次使用类时才会进行加载,以节省资源和提高性能。

此外,类加载过程中,类加载器(ClassLoader)负责加载类的字节码文件,并将其转换为JVM内部的数据结构。类加载器根据一定的规则(例如双亲委派模型)来搜索和加载类,保证类的唯一性和安全性。

4.双亲委派模型

更合适叫“单亲委派模型”或“父亲委派模型”

双亲委派模型是Java类加载机制中的一种重要策略,它的主要目的是保证Java类的安全性和避免类的重复加载。该模型采用了层次化的类加载器结构,分为三个主要的类加载器:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。加载类时,会按照以下过程进行双亲委派:

  1. 检查类是否已被加载:当应用程序需要加载一个类时,首先会委托应用程序类加载器(Application ClassLoader)去搜索类路径(Classpath)下是否已经加载了该类。如果已经加载,则直接返回已加载的类,不再重复加载。

  2. 委托给扩展类加载器:如果应用程序类加载器没有找到需要加载的类,那么会将加载请求委托给扩展类加载器(Extension ClassLoader)。扩展类加载器会在JRE的扩展目录(jre/lib/ext)下搜索类文件,如果找到并加载了该类,就返回它。

  3. 委托给启动类加载器:如果扩展类加载器也没有找到需要加载的类,那么会将加载请求进一步委托给启动类加载器(Bootstrap ClassLoader)。启动类加载器是最顶层的类加载器,它负责加载Java核心类库,如java.lang等。如果启动类加载器找到了并加载了该类,就返回它。

  4. 查找并加载类:如果以上所有的类加载器都没有找到需要加载的类,那么最后会由当前类加载器(可能是应用程序类加载器或其他自定义类加载器)来尝试加载类。如果当前类加载器找到了并加载了该类,就返回它。

通过这种双亲委派模型,可以保证类的唯一性和安全性。如果一个类已经由更上层的类加载器加载,那么就不会被下层的类加载器再次加载,从而避免了类的重复加载。同时,由于核心类库是由启动类加载器加载的,因此可以保证核心类库的安全性,防止恶意代码替换核心类。这种模型也允许开发人员自定义类加载器,从而实现一些特定的类加载需求,如加载网络资源或加密的类文件等。

在这里插入图片描述

垃圾回收机制(GC)

垃圾回收,就是把不用的内存回收

GC的回收单位是对象

  • GC实际工作过程

垃圾回收(Garbage Collection)的实际工作过程可以分为以下几个步骤:

  1. 标记(Mark): 垃圾回收器首先会从根对象(如栈中的引用、静态变量等)开始,递归遍历对象图,标记所有被引用的对象。被标记的对象表示它们是活动对象,不会被回收。

  2. 可达性分析(Reachability Analysis): 在标记阶段完成后,垃圾回收器会进行可达性分析。通过从根对象出发,判断哪些对象是不可达的,即没有被标记的对象。这些不可达对象将被认定为垃圾对象,可以被回收。

  3. 回收(Sweep): 在确定了垃圾对象后,垃圾回收器会回收这些对象的内存空间。回收的方式可以有多种策略,如标记-清除(Mark and Sweep)、复制(Copying)、标记-整理(Mark and Compact)等。

  4. 内存回收(Memory Reclamation): 在回收了垃圾对象的内存空间后,垃圾回收器会将这些空闲的内存空间进行整理或重新分配,以供将来的对象使用。

需要注意的是,垃圾回收的具体实现可能因不同的垃圾回收算法和垃圾回收器而有所差异。一些垃圾回收器可能会使用分代回收策略,将堆内存划分为不同的代(如新生代和老年代),并采用不同的回收算法和阶段来处理不同代的对象。

此外,垃圾回收的触发时机也是由具体的垃圾回收器决定的。常见的触发条件包括堆内存的占用达到一定阈值、分配新对象时无法满足内存需求等。

总的来说,垃圾回收的实际工作过程包括标记、可达性分析、回收和内存回收等阶段。这些步骤的具体实现可能因不同的垃圾回收算法和垃圾回收器而有所差异。垃圾回收的目标是释放不再使用的内存空间,以提高系统的性能和资源利用率。

  • 垃圾回收中如何判断对象是否有引用指向

在垃圾回收过程中,判定对象是否为垃圾主要通过以下两种方式:

  1. 引用计数算法(Reference Counting):这种算法维护了一个对象的引用计数器,每当有一个引用指向该对象时,计数器加1;当引用失效或对象被释放时,计数器减1。当计数器的值为0时,表示该对象没有被引用,即可以判定为垃圾对象。然而,引用计数算法无法解决循环引用的问题,即若存在一组对象彼此之间互相引用但与其他对象没有引用关系,它们的引用计数永远不会达到0,导致无法被回收。

    • ★★★引用计数的缺点

      • 引用计数是一种垃圾回收算法,它的主要思想是通过在对象上维护一个引用计数器,记录当前有多少个引用指向该对象。当引用计数器变为零时,即没有任何引用指向该对象时,就可以将该对象回收。

        尽管引用计数算法有一些优点,如实时性和简单性,但它也存在一些缺点,包括:

        1. 循环引用问题:引用计数无法解决循环引用的问题。如果存在循环引用,即一组对象互相引用形成一个环路,那么它们的引用计数器永远不会变为零,导致这些对象无法被回收,造成内存泄漏。

        2. 计数器维护开销:引用计数需要在每次引用发生变化时更新计数器的值,包括引用增加和引用减少。这增加了额外的开销,并且在多线程环境下需要使用同步机制来确保计数器的正确性,进一步增加了开销。

        3. 无法处理循环引用导致的内存泄漏:由于引用计数无法解决循环引用的问题,因此在存在循环引用的情况下,引用计数算法会导致内存泄漏,即无法回收被循环引用的对象。

        4. 对象间的相互影响:引用计数算法要求在每次引用变化时都要更新计数器,这会导致对象之间相互影响。当对象的引用发生变化时,需要更新相关对象的计数器,这可能导致频繁的引用计数操作,影响性能。

        综上所述,引用计数算法由于无法解决循环引用问题和存在计数器维护开销等缺点,通常不被作为主要的垃圾回收算法使用。现代的垃圾回收器通常采用其他算法,如标记-清除算法、复制算法、标记-整理算法等来解决这些问题。

  2. 可达性分析算法(Reachability Analysis):这种算法基于"GC Roots"的概念,通过从一组称为GC Roots的根对象出发,递归遍历所有的引用链,标记所有可以从根对象访问到的对象为活动对象,未被标记的对象则被判定为垃圾对象。GC Roots包括:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(Java Native Interface)引用的对象

可达性分析算法能够处理循环引用的情况,只有无法从GC Roots访问到的对象才会被判定为垃圾对象,即使存在循环引用,只要与GC Roots没有关联,对象也会被回收。

需要注意的是,判定对象是否为垃圾是垃圾收集器的职责,具体的判定策略和算法会因垃圾收集器的不同而有所差异。Java虚拟机提供了不同的垃圾收集器(如Serial GC、Parallel GC、CMS GC、G1 GC等),它们在判定对象是否为垃圾以及回收对象的时机上可能采用不同的策略和算法。

  • ★★★可达性分析的流程

    可达性分析是一种常用的垃圾回收算法,用于确定对象是否可被访问,从而判断是否为垃圾对象。以下是可达性分析的基本流程:

    1. 根节点标记:可达性分析从一组称为"根节点"的起始对象开始。在Java中,根节点通常包括活动线程的栈帧中的本地变量、静态变量和常量等。这些根节点对象被标记为活动对象。

    2. 标记阶段:从根节点对象开始,通过遍历对象的引用关系,将可达对象标记为活动对象。具体的标记方式可以是深度优先搜索或广度优先搜索等。在标记过程中,被访问到的对象都会被标记为活动对象,而未被访问到的对象则被标记为垃圾对象。

    3. 清除阶段:在标记阶段完成后,所有未被标记为活动对象的对象被认为是垃圾对象,将被回收。清除阶段会释放这些垃圾对象所占用的内存空间,以便后续的对象分配。

    4. 压缩/整理阶段(可选):在清除阶段之后,如果使用的是压缩或整理算法的垃圾回收器,可能会对内存进行整理,以减少内存碎片并提高内存利用率。在整理阶段,活动对象会被移动到一起,以便空闲内存空间形成连续的块,方便后续对象的分配。

    可达性分析的基本思想是从一组根节点出发,通过对象之间的引用关系,逐步遍历和标记可达的对象,最终确定哪些对象是活动对象,哪些对象是垃圾对象。这种方式能够有效地找到不再被引用的对象,并进行相应的回收处理,从而释放内存资源。

垃圾清理的方法

  • 进行垃圾清理的做法

    1. 标记清理

      优点:

      1. 灵活性:标记清理算法可以处理任意形式的垃圾对象,包括循环引用(circular references)等复杂的情况。
      2. 内存利用率:标记清理算法可以在不进行内存整理的情况下回收垃圾对象,因此不需要额外的内存空间来进行对象的移动和整理,节省了内存使用。
      3. ★★★简单实现:相对于其他垃圾回收算法,标记清理算法的实现较为简单,容易理解和实现。

      缺点:

      1. ★★★内存碎片:标记清理算法会导致内存碎片的产生。当被标记的存活对象和被清理的垃圾对象交织在一起时,会产生一些不连续的内存空间,称为内存碎片。内存碎片可能会影响后续对象的分配和内存管理效率。
      2. 暂停时间:标记清理算法在执行垃圾回收过程时需要停止应用程序的执行,以进行标记和清理操作。这会导致暂停时间的增加,对实时性要求高的应用程序可能会受到影响。
      3. 效率低下:标记清理算法需要扫描整个堆内存来标记活动对象,这个过程需要耗费较长的时间,并且随着堆内存的增大,标记和清理的时间也会增加,对于大型应用程序的垃圾回收来说,效率可能较低。

      总的来说,标记清理算法是一种简单灵活的垃圾回收算法,适用于一些对实时性要求不高、内存碎片影响相对较小的场景。然而,随着应用程序的复杂性和内存需求的增加,标记清理算法的效率和内存管理效果可能会变得不够理想,更高效的垃圾回收算法和策略被提出和使用。

    2. 复制算法

      优点:

      1. ★★★内存整齐:复制算法会将存活对象复制到一个新的内存区域,同时清理掉原有的垃圾对象。这样可以保证内存的连续性,消除了内存碎片的问题。

      2. 快速回收:复制算法只需要对存活对象进行复制,而不需要对整个堆进行扫描和标记。这样可以提高垃圾回收的速度,减少回收时间。

      3. 简单实现:相对于其他垃圾回收算法,复制算法的实现较为简单,容易理解和实现。

        缺点:

      4. 内存开销:复制算法需要将存活对象复制到新的内存区域,这意味着需要额外的内存空间来存储复制的对象。对于大型对象或者存活对象比例较高的情况,可能需要较大的内存开销。

      5. ★★★内存利用率:复制算法的内存利用率相对较低,因为一半的内存空间被用于存储复制的对象,而另一半的空间被浪费。这在堆内存较小的情况下会更加明显。

      总的来说,复制算法适用于小型堆内存和存活对象比例较低的场景,因为它可以提供较高的回收效率和内存整齐性。然而,在大型堆内存和存活对象比例较高的情况下,复制算法的内存开销和内存利用率可能会成为问题。在实际应用中,复制算法通常会与其他垃圾回收算法结合使用,以达到更好的效果。

    3. 标记整理

      优点:

      1. 内存整齐:标记整理算法会将存活对象向一端移动,并清理掉垃圾对象。这样可以保证内存的连续性,消除了内存碎片的问题,提高了内存的利用率。
      2. ★★★垃圾回收效率较高:标记整理算法只需要对存活对象进行标记和移动,而不需要对整个堆进行复制。相对于复制算法,标记整理算法的回收效率更高。

      缺点:

      1. ★★★额外开销:标记整理算法需要进行标记和整理操作,这会带来一些额外的开销。标记阶段需要遍历对象图进行标记,整理阶段需要移动对象。这些操作会增加垃圾回收的时间开销。
      2. 内存移动:标记整理算法需要将存活对象进行移动,这涉及到对象的地址改变。如果应用程序中有大量的引用或指针指向对象,那么在移动对象的过程中需要相应地更新这些引用或指针,增加了额外的工作量。

      总的来说,标记整理算法适用于大型堆内存和存活对象比例较高的场景。它可以提供较高的内存整齐性和内存利用率,并且相对于复制算法而言,具有较低的内存开销。然而,标记整理算法的标记和整理过程会增加垃圾回收的时间开销,并且需要处理对象地址改变的情况。在实际应用中,选择合适的垃圾回收算法需要根据具体的应用场景和性能需求进行评估。

    4. 分代回收

      分代回收(Generational Garbage Collection)是一种常见的垃圾回收策略,它根据对象的存活周期将内存分为不同的代(Generation),并针对每个代采用不同的回收策略。

      一般来说,分代回收将内存分为新生代(Young Generation)和老年代(Old Generation)两个主要的代。

      1. 新生代:新生代是存放刚刚创建的对象的区域。通常情况下,大部分对象的生命周期较短,很快就会变成垃圾。新生代采用复制算法进行垃圾回收。它将内存分为两个等大小的区域:Eden空间和两个Survivor空间(通常是From空间和To空间)。对象首先在Eden空间进行分配,当Eden空间满时,仍然存活的对象会被复制到其中一个Survivor空间中,而非存活的对象将被回收。当Survivor空间满时,存活的对象会被复制到另一个Survivor空间,同时清空原来的Survivor空间。这样来回复制和清理,直到对象存活到一定次数后会被晋升到老年代。

      2. 老年代:老年代是存放长时间存活的对象的区域。老年代采用标记-清除(Mark-Sweep)或标记-整理(Mark-Compact)算法进行垃圾回收。由于老年代中的对象存活时间较长,不适合频繁地进行复制操作。因此,老年代的回收算法主要关注如何标记和清理或整理存活对象,以及如何处理碎片化的内存。

      分代回收的核心思想是根据对象的生命周期采用不同的回收策略,以提高垃圾回收的效率。新生代中的大部分对象很快就会成为垃圾,通过复制算法可以快速回收,而老年代中的对象生命周期较长,采用标记-清除或标记-整理算法进行回收。这样可以根据对象的特点进行针对性的回收,提高垃圾回收的效率和性能。

      给对象引入一个年龄的概念

      年龄不是岁数,而是熬过GC的轮次,年龄越大代表存在的时间就越长

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值