Skip to content

JVM与内存管理类

本章内容都由网上整合,以及个人思考,仅用于学习分享,如有侵权部分或者思考错误地方,欢迎大家及时提出,本人一定及时纠正。

堆和栈的区别(内存管理层的区别)

  • 栈区(Stack): 由编译器自动分配释放,主要用于存放函数的参数值、局部变量等。其操作方式类似于数据结构中的栈。栈内存的分配运算内置于CPU的指令集,效率很高,但分配的内存量有限。在Java中,栈是线程私有的

  • 堆区(Heap): 一般由程序员手动分配和释放(如通过mallocnew等函数)。如果程序员不释放,程序结束时可能由操作系统回收。堆内存的分配方式类似于链表,适用于需要动态分配内存的场景。在Java中堆是线程共享的,且由 JVM 管理

JVM的主要组成部分

  1. 组成部分

    • 类加载器

    • 运行时数据区

    • 执行引擎

      1. 解释器
      2. 即时编译器
      3. 垃圾回收器
    • 本地方法接口

  2. 执行顺序

    1. 类加载器:通过类加载器,将 Java 代码转为字节码
    2. 运行时数据区:再把字节码加载到内存中,由于字节码不能直接被底层操作系统识别并执行
    3. 执行引擎:将字节码翻译成底层系统指令,再交由 CPU 执行
    4. 本地方法接口:这个过程中需要用到其它语言的本地接口来实现整个程序的功能

JVM的内存区域是怎样的

内存区域:

  • 线程私有:

    1. 虚拟机栈

      • 每个Java线程都会有一个Java虚拟机栈,与线程同时创建。
      • 每个方法在执行的时候都会创建一个栈帧。调用方法时入栈,方法返回时出栈
    2. 程序计数器

      • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
      • 多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了
      • 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
    3. 本地方法栈

      • 与虚拟机栈基本类似,但它为本地方法服务,支持本地(Native)方法执行的内存区域。

      • **本地方法(Native Method)**是指用其他编程语言(通常是C或C++)编写的方法,这些方法通过Java本地接口(JNI)与Java代码进行交互。

        java
        public class Example {
            // 声明本地方法
            public native void nativeMethod();
        
            static {
                // 加载包含本地方法实现的动态链接库
                System.loadLibrary("exampleLibrary");
            }
        }
  • 线程共享

      • 占据 JVM 中最大的一块内存,用于存储对象实例。所有对象实例和数组都在堆上分配内存。堆是被所有线程共享的一块内存区域。在 JVM 启动时创建

      • 由于存放所有对象实例和数组,所以也是垃圾回收的主要区域

      • 堆的内存管理通常由一下部分组成:

        1. 年轻代(Young Generation): 用于存放新创建的对象。年轻代又分为:

          • Eden区: 新对象首先被分配到Eden区。

          • Survivor区(S0和S1): 经过一次垃圾回收后,仍然存活的对象会被移动到Survivor区。两个Survivor区交替使用,以提高内存利用率。

        2. 老年代(Old Generation): 用于存放生命周期较长的对象。经过多次垃圾回收后,仍然存活的对象会被晋升到老年代。

        3. 永久代(Permanent Generation)/元空间(Metaspace): 用于存放类的元数据,如类的结构信息、常量池等。需要注意的是,在JDK 8及之后的版本中,永久代被元空间取代,元空间不再位于堆内存中,而是使用本地内存。

      • 堆内还有一块内存叫 字符串常量池

    1. 方法区

      **方法区(Method Area)**是一个用于存储类级别信息的内存区域。它主要包含以下内容:

      • 已加载的类型信息: 包括类、接口、枚举和注解的结构信息,如类名、父类名、访问修饰符等。

      • 常量池: 存储编译期生成的各种字面量和符号引用,例如字符串常量、数字常量和类引用等。

      • 静态变量: 类的静态成员变量,这些变量在方法区中有唯一的存储位置,所有实例共享。

      • 即时编译器编译后的代码缓存: 存储JIT编译器将字节码编译为本地机器码后的代码,以提高执行效率。

      运行时常量池:属于方法区的一部分,用于存放编译期产生的各种字面量和符号引用。JDK1.8之前都是存放在方法区中,但从JDK8开始,存放在本地内存中的元空间内。

  • 本地内存

    本地内存 与上述的 运行时数据区域是分开来的,JDK1.8前只有直接内存,在JDK1.8开始,元空间也存放此内存中,并携带着运行时常量池一起。

    直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。

    直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

  • 总结:

    1. 程序计数器
    2. Java虚拟机栈
    3. 本地方法区
    4. Java堆
    5. 方法区
    6. 运行时常量池
    7. 字符串常量池
    8. 直接内存

四种引用

1.强引用(StrongReference)

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

java
public class StrongReferenceExample {
    public static void main(String[] args) {
        Object strongRef = new Object(); // 创建一个强引用
        System.out.println(strongRef); // 输出对象信息
    }
}

2.软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

java
import java.lang.ref.SoftReference;

public class SoftReferenceExample {
    public static void main(String[] args) {
        Object obj = new Object();
        SoftReference<Object> softRef = new SoftReference<>(obj);
        obj = null; // 解除强引用
        System.gc(); // 请求垃圾回收
        Object retrievedObj = softRef.get(); // 获取软引用指向的对象
        System.out.println(retrievedObj); // 输出对象信息
    }
}

3.弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

java
import java.lang.ref.WeakReference;

public class WeakReferenceExample {
    public static void main(String[] args) {
        Object obj = new Object();
        WeakReference<Object> weakRef = new WeakReference<>(obj);
        obj = null; // 解除强引用
        System.gc(); // 请求垃圾回收
        Object retrievedObj = weakRef.get(); // 获取弱引用指向的对象
        System.out.println(retrievedObj); // 输出null,因为对象可能已被回收
    }
}

4.虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动

java
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceExample {
    public static void main(String[] args) {
        Object obj = new Object();
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
        obj = null; // 解除强引用
        System.gc(); // 请求垃圾回收
        Object retrievedObj = phantomRef.get(); // 获取虚引用指向的对象
        System.out.println(retrievedObj); // 输出null,因为虚引用不能通过get()方法访问对象
    }
}

虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

怎么判断对象是否可以被回收

  1. 引用计数法

    给对象中添加一个引用计数器:

    • 每当有一个地方引用它,计数器就加 1;
    • 当引用失效,计数器就减 1;
    • 任何时候计数器为 0 的对象就是不可能再被使用的。

    这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。

    java
    public class ReferenceCountingGc {
        Object instance = null;
        public static void main(String[] args) {
            ReferenceCountingGc objA = new ReferenceCountingGc();
            ReferenceCountingGc objB = new ReferenceCountingGc();
            objA.instance = objB;
            objB.instance = objA;
            objA = null;
            objB = null;
        }
    }
  2. 可达性分析算法

    基本思想为:通过一些列称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,也就是不可达时,就证明此对象不可能再被使用了。具体可以去看《深入理解Java虚拟机》中的解释。

垃圾回收算法、垃圾回收机制

垃圾回收算法:

  • 标记清除法

    标记存活的对象,统一收回所有被标记的对象(也可以反一下)

    缺点:执行效率不稳定(堆中包含大量对象且大部分需被回收,导致标记和清除的执行效率降低)、内存空间碎片化(会产生大量不连续的内存碎片)

  • 标记复制法

    将可用内存按容量等分成两块,每次只使用其中一块,等这块使用完成,就将还存活的对象复制到另一块,然后把当前使用过的这块内存空间一次清理

    缺点:可用内存缩小为原来的一半,空间浪费多

  • 标记整理法

    标记过程同标记清除法,后续步骤不是直接清楚,而是让所有存活的对象向内存空间一端移动,然后清理掉边界以外的内存

  • 分代收集法

    根据对象的存活时间,将内存划分为不同的区域(如新生代和老年代),对不同区域采用不同的回收策略。新生代采用复制算法,老年代采用标记-清除或标记-整理算法。

垃圾回收机制:

年轻代分为 Eden 区和 survivor 区 (fromto),且 Eden : from : to == 8 : 1 : 1

  1. 先产生的对象优先分配在 Eden 区(除非配置了 -XX:PretenureSizeThreshold,大于该值的对象会直接进入老年区)

  2. Eden 区满了,这时会把该区域中存活的对象复制到 from 区。如果存活的对象 from 区放满了也放不下,则这些存活下来的对象全部进入老年代。之后将 Eden 区的内存全部回收

  3. 之后将产生的对象继续分配在 Eden 区,如果当 Eden 区满了后,这时会把 Edenfrom 区中存活下来的对象复制到 to 区(同2,如果 to 区也放不下,存活下来的对象全部放入老年代),之后回收 Eden 区和 from 区的所有内存

  4. 反复操作,对象每被复制一次,对象的年龄就+1,默认情况下,当对象被复制了15次(可以通过 -XX:MaxTenuringThreshold 来配置),就会进入老年代

  5. 当老年代满了,或者存放不下的时候,就会发生一次 Full GC(该操作的耗时很严重,需要尽量避免)

  6. 补充:

    虽然对象的年龄可以通过参数 -XX:MaxTenuringThreshold 来设置,但是设置的值应该在 0 - 15,否则会爆出下列错误

    java
    MaxTenuringThreshold of 20 is invalid; must be between 0 and 15

    为什么年龄只能在 0 - 15,因为记录年龄的区域在对象头中,这个区域的大小通常是 4 位。这 4 位可以表示的最大二进制数字是 1111,即十进制的 15。因此,对象的年龄被限制为 0 到 15。

    提到对象头,我们就来简单刨析一下对象头:

    首先在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其中,对象头包括两部分:标记字段(Mark Word)和类型指针(Klass Word)。这个年龄信息就是在标记字段中存放的(标记字段还存放了对象自身的其他信息比如哈希码、锁状态信息等等)。还记得上面提到锁的时候吗,问题4的第七点,偏向锁和轻量级锁吗,锁的信息也是存放在对象头中的标记字段。

垃圾回收的两种类型:

  1. Minor GC

    对新生代进行回收,不会影响到老年代。因为新生代的 Java 对象大多存活时间较短,所以 Minor GC 非常频繁

  2. Full GC

    也称为 Major GC,对整个堆进行回收,包括新生代和老年代。导致 Full GC 的原因有:老年代被写满,永生代被写满, System.gc() 被显示调用等

垃圾回收器

经典垃圾收集器

新生代垃圾收集器

  1. Serial——(新生代采取复制算法,老年代采取标记整理算法)

    串行收集器,当它在进行垃圾收集的时候,必须暂停其他所有工作线程,直到它收集结束。读者可以试想自己的电脑每运行一小时就会强制暂停响应五分钟,那感受肯定是不好受的。

    虽然Serial收集器听起来比较鸡肋,但迄今为止,,它依旧是HotSpot虚拟机在客户端模式下默认新生代收集器,比起其它任何收集器来说,它时最简单而高效的,且额外内存消耗最小。

  2. ParNew——(同Serial,新生代采取复制算法,老年代采取标记整理算法)

    实质上就是Serial收集器的多线程并行版,连控制参数、收集算法、对象分配、回收策略都与Serial收集器一致。但它有一个特性,目前只有ParNew收集器能与CMS收集器配合工作

  3. Parallel Scavenge

    Parallel Scavenge收集器也是一款新生代收集器,同样基于标记——复制算法实现。但它的关注点与其它收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge收集器地目标是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

老年代垃圾收集器

  1. Serial Old

    Serial Old 是 Serial收集器的老年代版本,同样也是单线程收集器,使用标记整理算法。主要的用处:

    1. 给客户端模式下的虚拟机使用
    2. 在Server模式下主要有两个用途:
      • 在JDK5版本以及之前的版本中与Parallel Scavenge收集器搭配使用
      • 作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure 时使用
  2. Parallel Old

    是 Paraller Scavenge 收集器的老年代版本,基于标记-整理算法。这个收集器是直到JDK6时才开始提供的。

  3. CMS (Concurrent Mark Sweep)

    CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

    目前很大一部分的Java应用集中在互联网网站或者基于浏览器的 B/S系统的服务端上,这类应用通常都会比较关注服务的响应速度,希望系统停顿时间尽可能短。

    CMS收集器基于标记-清楚算法,运作过程如下:

    1. 初始标记
    2. 并发标记
    3. 重新标记
    4. 并发清除

    其中初始标记重新标记两个步骤仍需要 “Stop The World”。

    • 初始标记仅仅只是标记以下 GC Roots能直接关联到的对象,速度很快

    • 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行

    • 重新标记阶段则是为了修正并发标记期间,因用户线程继续运作而导致标记产生变动的那部分对象的标记记录。这个阶段的停顿时间通常会比初始标记阶段长一点,但也远比并发标记阶段的时间短

    • 并发清楚阶段会清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段可以与用户线程同时并发。

    CMS收集器的主要优点:

    • 并发收集
    • 低停顿

    CMS收集器三个明显缺点:

    • CMS收集器对处理器资源非常敏感。CMS默认启动的回收线程数是(处理器核心数量+3)/ 4,也就是说处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不少于25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。为了缓解这个情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器。
    • CMS收集器无法处理 “浮动垃圾”,有可能出现“Concurrent Mode Failure”失败,进而导致另一次完全 “Stop The World” 的 Full GC。在CMS并发标记和并发清理阶段,用户线程还是在继续运行的,程序在运行,自然就会产生新的垃圾对象,但是这一部分垃圾是出现在标记过程结束以后,CMS无法在这次收集中处理掉它们,只能留下一次垃圾收集时处理。这一部分垃圾就成为浮动垃圾。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活。到了JDK1.6后,CMS收集器的启动阈值默认提升至92%,这又会面临一个新的问题,当CMS运行期间预留的空间无法满足程序分配新对象的需要,就会出现一次 “并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集。
    • CMS是一款基于 标记-清楚算法实现的收集器,这意味着收集结束后,会留下大量的空间碎片。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,导致不得不提前触发一次 Full GC

新生代和老年代垃圾收集器

  1. Garbage First收集器

    简称G1收集器,JDK1.7后全新的回收器,用于取代CMS。它的优势如下:

    • 独特的分代垃圾回收器
    • 使用分区算法,把连续的Java堆划分为多个大小相等的独立区域,每个Region根据需要扮演新生代的Eden空间,Survivor空间或者老年代空间
    • 并行性
    • 空间整理
    • 可预见性

    G1收集器的运作过程大致可分为以下四步:

    1. 初始标记:仅仅只是标记以下GC Roots能直接关联到的对象
    2. 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出需要回收的对象,该阶段耗时很长,但可与用户程序并发执行。当对象图扫描完成后,还需要重新处理SATB(原始快照)记录下的在并发时有引用变动的对象
    3. 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录
    4. 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户期待的GC停顿时间指定回收计划,回收一部分Region

JVM类加载机制

类的生命周期:加载 ——连接——初始化——使用——卸载

其中连接过程中又可以拆分成验证——准备——解析

  1. 加载

    1. 类加载器通过全类名获取定义此类的二进制字节流。
    2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
    3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。

    补充:某个类具体是用哪个类加载器加载由 双亲委派模型 决定。

  2. 验证

    1. 文件格式验证(Class 文件格式检查)

      验证字节流是否符合Class文件格式的规范,如:

      • 是否以 0XCAFEBABE 开头
      • 主次版本号是否在当前虚拟机的处理范围之内
      • 常量池中的常量是否有不被支持的类型
      • 等等
    2. 元数据验证(字节码语义检查)

      对字节码描述的信息进行语义分析,保证描述的信息符合《Java语言规范》的要求,如:

      • 这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)
      • 这个类是否继承了不被允许继承的类(被final修饰的类)
      • 等等
    3. 字节码验证(程序语义检查)

      通过数据流分析和控制流分析,确认程序语义是合法的、符合逻辑的,如:

      • 函数的参数类型是否正确
      • 对象的类型转换是否合理
      • 等等
    4. 符号引用验证(类的正确性检查)

      验证该类的正确性,如:

      • 该类使用的其他类、方法、字段是否存在、是否拥有正确的访问权限
      • 符号引用中通过字符串描述的全限定名是否能找到对应的类

    除了文件格式验证这一阶段基于该类的二进制字节流进行的,其余都是基于方法区的存储结构上进行。

  3. 准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都在方法区中分配

    1. 这个阶段进行内存分配的仅包括类变量(静态变量),不包括实例变量。实例变量会在对象实例化时,跟随对象一起分配在Java堆中
    2. 每个类变量在准备阶段都有一个初始值(默认是零值),如 int 类型的零值是0,long 类型的零值是0L,boolean 类型的零值是false等等
  4. 解析

    解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用。如下例子:

    java
    public class Example implements Runnable {
        private int number;
        public static final String CONSTANT = "constant";
    
        public void setNumber(int number) {
            this.number = number;
        }
    
        public int getNumber() {
            return number;
        }
    
        @Override
        public void run() {
            System.out.println("Running...");
        }
    
        public static void main(String[] args) {
            Example example = new Example();
            example.setNumber(10);
            System.out.println(example.getNumber());
            example.run();
        }
    }

    在上述代码中:

    • 类或接口Example 类实现了 Runnable 接口,Example.classRunnable.class 都是类或接口的符号引用。
    • 字段number 是实例字段,CONSTANT 是静态字段,它们分别是字段符号引用。
    • 类方法main 方法是静态方法的符号引用。
    • 接口方法run 方法是 Runnable 接口的方法实现,其符号引用指向接口方法。
    • 方法类型(I)V 是方法类型的符号引用,表示接受一个整数参数并返回 void 的方法类型。
    • 方法句柄:在使用反射或动态代理时,方法句柄用于表示对方法的引用。例如,MethodHandles.lookup().findVirtual(Example.class, "run", MethodType.methodType(void.class)) 获取 run 方法的句柄。
    • 调用限定符:在动态调用中,调用限定符用于指定调用的目标方法,例如通过 invokedynamic 指令实现的调用点,其符号引用指向实际调用的方法。
  5. 初始化

    • 在类的初始化阶段,JVM 会执行类的静态初始化块(static {})和静态变量的赋值操作。

    • 如果类实现了 java.lang.Runnable 接口并包含 run() 方法,或者包含 main 方法,JVM 会在初始化过程中处理这些方法的准备工作。

    • 初始化过程确保类的静态成员在类首次使用之前被正确设置和初始化。

  6. 类卸载

    卸载类需要满足3个要求:

    1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
    2. 该类没有在其他任何地方被引用
    3. 该类的类加载器的实例已被 GC

双亲委派机制是什么意思

站在Java虚拟机的角度,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机的一部分;另一种就是其它所有的类加载器,这些加载器都由Java语言实现,独立存在于虚拟机外部,并且全部继承于抽象类java.lang.ClassLoader。

在JDK9之前,绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载

  1. 启动类加载器
  2. 扩展类加载器
  3. 应用程序类加载器

类加载器双亲委派模型

双亲委派模型要求除了最顶层的启动类加载器外,其余的类加载器都需要有自己的父类加载器。不过类加载器之间的父子关系一般不是继承的关系来实现,而是通过使用组合关系来复用父加载器的代码。

双亲委派模型的工作流程:

  1. 当一个类加载器收到了类加载的请求,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中
  2. 当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需类时),子加载器才会尝试自己完成加载

优点:

  1. 确保核心类的安全性:通过将加载请求委托给父加载器,核心类库(如 rt.jar)由根加载器加载,防止被篡改或替换,确保了系统的安全性。
  2. 避免类的重复加载:由于采用了父子加载器的委派机制,同一个类只会被加载一次,避免了类的重复加载和相关问题。
  3. 增强系统的可维护性:父类加载器负责加载系统核心类,子类加载器负责加载应用程序特定类,职责清晰,有助于系统的维护和扩展。

缺点:

  1. 灵活性差:由于所有类加载请求都必须经过父加载器,子加载器无法自主加载自己需要的类,限制了类加载的灵活性。
  2. 扩展性差:在某些复杂应用场景中,可能需要自定义类加载器来加载特定的类,但双亲委派机制的严格父子关系可能限制了这种扩展。
  3. 调试复杂性增加:由于类加载过程受到父子加载器的影响,调试类加载问题可能变得更加复杂,需要深入理解类加载机制。

总体而言,双亲委派机制在确保类加载安全性和一致性方面发挥了重要作用,但在需要高度灵活性和扩展性的场景中,可能需要对其进行适当的调整或绕过。

JVM参数配置

1. 堆内存设置:

  • -Xms<size>:设置JVM初始堆内存大小。例如,-Xms512m将初始堆大小设置为512MB。
  • -Xmx<size>:设置JVM最大堆内存大小。例如,-Xmx1024m将最大堆大小设置为1GB。

2. 新生代和老年代内存比例设置:

  • -XX:NewRatio=<ratio>:设置新生代与老年代的比例。例如,-XX:NewRatio=2表示新生代大小是老年代的1/2。
  • -XX:NewSize=<size>:设置新生代初始大小。
  • -XX:MaxNewSize=<size>:设置新生代最大大小。

3. 垃圾回收器选择:

  • -XX:+UseSerialGC:使用串行垃圾回收器。适用于单核处理器或内存较小的系统。
  • -XX:+UseParallelGC:使用并行垃圾回收器。适用于多核处理器,能够利用多核优势进行垃圾回收。
  • -XX:+UseConcMarkSweepGC:使用并发标记清除垃圾回收器(CMS)。适用于需要低暂停时间的应用。
  • -XX:+UseG1GC:使用G1垃圾回收器。适用于需要大堆内存且要求可预测停顿时间的应用。

4. 垃圾回收日志记录:

  • -Xloggc:<file-path>:将垃圾回收日志输出到指定文件。例如,-Xloggc:/var/log/gc.log
  • -XX:+PrintGCDetails:打印垃圾回收的详细信息。
  • -XX:+PrintGCDateStamps:在垃圾回收日志中添加时间戳。

5. 方法区和元空间设置(适用于Java 8及以上版本):

  • -XX:MetaspaceSize=<size>:设置元空间的初始大小。例如,-XX:MetaspaceSize=128m
  • -XX:MaxMetaspaceSize=<size>:设置元空间的最大大小。例如,-XX:MaxMetaspaceSize=256m

6. 其他常用参数:

  • -XX:+PrintFlagsFinal:打印所有JVM参数及其最终值。
  • -XX:CICompilerCount=<number>:设置JIT编译器的线程数量。例如,-XX:CICompilerCount=2
  • -XX:+UseCompressedOops:启用指针压缩,以减少堆内存占用。