news 2026/3/25 23:57:42

JVM 学习小记(边学边充实)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM 学习小记(边学边充实)

🐱‍👓 一、JVM

1.1 JVM基本定义

定义:Java Virtual Machine-Java 程序的运行环境(Java二进制字节码的运行环境)

好处

  • 一次编写后,任意环境都可运行

  • 自动内存管理、垃圾回收功能

  • 数组下标越界,越界检查(其他语言无此功能会造成内存地址覆盖)

  • 多态

比较:JVM、JRE、JDK

学习路线

🐱‍🚀 二 、JVM内存结构

JVM内存结构:

线程私有的:程序计数器、虚拟机栈、本地方法栈

线程共享的:堆、方法区、直接内存 非运行时数据区的⼀部分)

1.8之前

1.8之后

2.1 程序计数器

(1) 定义:程序计数器是⼀块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

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

作用:

  • 记住下一条jvm要执行的指令地址

    • 字节码解释器通过改变程序计数器来依次读取指令,实现代码的流程控制,如:顺序执行、选择

    • Java源代码->二进制字节码->解释器(寻找程序计数器记录)->机器码->CPU

  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

特点:

  • 线程私有

  • 不会内存溢出

程序计数器是唯⼀⼀个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

2.2 虚拟机栈

(1)定义:每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。Java栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

Java方法有两种返回方式:return语句、抛出异常。不管哪种返回方式都会导致栈帧被弹出。

是线程运行时需要的内存空间,将每个栈帧执行按顺序压入栈内,每个栈帧的内存占用即为每个方法的参数、局部变量、返回地址。

(2) 问题辨析

  • 垃圾回收是否涉及栈内存(不涉及,每次出栈后即被释放)

  • 栈内存空间越大越好吗(不,物理内存大小固定,栈内存越大,只能进行更多次的方法调用,线程数会变小)

  • 方法内局部变量是否安全

    • 如果方法内局部变量,没有处于方法的作用外,它是线程安全的。

    • 如果局部变量引用了对象,并且对象处于方法的作用范围,需要考虑线程安全。

(3) 栈内存溢出(java.lang.StackOverFlowError)

1.当方法递归调用过多,导致栈内存溢出

2.栈内存过大,导致栈内存溢出

3.循环引用

两个类属性循环调用,在JSON数据转换时出现错误。

Java虚拟机栈会出现两种错误:StackOverFlowErrorOutOfMemoryError

  • StackOverFlowError若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

  • OutOfMemoryError若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。对于非固定大小的栈,在其扩展时(扩容),如果没有办法获取到足够大小的内存,报OutOfMemoryError

(4) 设置栈内存大小

在idea中edit configuration编辑

-Xss256k

(5)线程运行诊断

案例一:CPU占用过多

  • 用top定位哪个进程对cpu的占用过高

  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)

  • jstack 进程id

    • 根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号案例2:程序运行很长时间没有结果

  • 死锁

2.3 本地方法栈

(1)定义:和虚拟机栈所发挥的作用非常相似,区别是︰虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完后相应的栈帧也会出栈并释放内存空间,也会出现StackOverFlowErrorOutOfMemoryError两种错误。

本地方法栈给本地方法调用分配内存空间,调用的本地方法接口(其他语言编写)间接操作操作系统的方法。

// 被native关键字修饰的方法调用本地方法接口,例如: protected native Object clone() throws CloneNotSupportedException;

2.4 堆

(1)定义:Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

通过 new 关键字,创建对象都会使用堆内存它。是线程共享的,堆中对象都需要考虑线程安全的问题有垃圾回收机制。

Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap) 。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为︰新生代和老年代∶再细致一点有: Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

堆最容易出现的就是 OutOfMemoryError 错误。

设置堆内存大小

-Xmx10m

2.5 方法区 🎏

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

2.5.1 永久代、元空间

永久代和元空间就是方法区的实现,在JDK1.8之前方法区的实现是永久代,在JDK1.8之后是元空间。方法区就是一个定义规范,永久代和元空间就是它的实现。

为什么要将永久代(PermGen)替换为元空间(MetaSpace)

1.整个永久代有一个JVM本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

当元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace

  • -XX: MaxMetaSpaceSize标志设置最大元空间大小,默认值为unlimited,这意味着它只受系统内存的限制。

  • -XX: MetaSpaceSize调整标志定义元空间的初始大小如果未指定此标志,则Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

2.元空间里面存放的是类的元数据,这样加载多少类的元数据就不由-XX: MaxPermSize(永久代内存大小)控制了,而由系统的实际可用空间来控制,这样能加载的类就更多了。

3.在JDK8,合并HotSpot和JRockit 的代码时, JRockit 从来没有一个叫永久代的东西,合并之后就没有必要额外的设置这么一个永久代的地方了。

2.5.2 常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息运行时常量池。常量池在方法区中,运行时常量池在堆中。

常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)。

2.5.2 StringTable

String table又称为String pool,字符串常量池,当字符串常量未被引用时,都只是一个符号,当被引用时才会将它创建为对象放在StringTable中,是一个懒惰的,可以使用intern方法主动放入,StringTable数据结构是hash表数组+链表

1.字符串变量拼接

public class Test { public static void main(String[] args) throws InterruptedException { // 基本类型在常量池中 StringTable["a","b","ab"] String a = "a"; // 存放在常量池中 String b = "b"; String ab = "ab"; //实际调用new StringBuilder().append(a).append(b).toString,返回的是封装的String对象 new String("ab"); String ab1 = a+b; //变量拼接 //javac 在编译期的优化,因为是"a","b"是常量,在编译期已经确定为"ab",所以不用StirngBuilder方式拼接 String ab2 = "a"+"b"; //常量拼接 System.out.println(ab1 == ab); //false } }

字节码文件反编译 String ab1 = a+b; 后指令:

字节码反编译指令

javap -v Test.class

2.字符串延迟加载

intern()方法主动将常量放入常量池

public class Test1 { public static void main(String[] args) { // 常量池 ["a","b","ab"] 堆 new String("a") 、new String("b") 、new String("ab") String ab = new String("a")+new String("b"); // intern() 方法尝试将字符串对象放入常量池,有则不放无则放入,返回池中对象 String intern = ab.intern(); System.out.println(intern=="ab"); //true System.out.println(ab=="ab"); //就是将ab放入的池,所以也是true ,1.8之前是false } }

3.StringTable特性:

  • 1、stringTable数据结构为一个hash表(数组+链表),不可扩容,存字符串常量,唯一不重复。 2、常量池中的字符串仅是符号,第一次用到才变为对象 3、其创建方式为懒创建,用到时才创建

  • 常量池中的字符串仅是符号,第一次用到时才变为对象。

  • 利用串池的机制,来避免重复创建字符串对象。

  • 字符串变量拼接的原理是stringBuilder (1.8)。

  • 字符串常量拼接的原理是编译期优化。

  • ·可以使用intern方法,主动将串池中还没有的字符串对象放入串池。

4.StringTable位置

JDK1.6存放在永久代时,只会被FullGC即是在老年代满后进行回收,导致回收效率过低,所以1.8后将方法区放到堆里,此时在MinorGC时就会进行回收。

5.StringTable垃圾回收

  • 打印字符串实例的个数,占用的大小信息

-XX:+PrintStringTableStatistics

  • 打印垃圾回收的详细信息,回收次数、时间...

-XX:+PrintGCDetails -verbose:gc

6.StringTable性能调优

(1) 设置 ,如字符串常量较多(如读取大文件,减少串池hash冲突

-XXStringTableSize=桶个数

(2) 考虑将字符串对象是否入池,未入池的字符串对象每个都要再堆中分配内存,而在串池中重复的字符串引用同一个字符串对象。

  • intern()方法入池,list.add( new String("a").intern() ) 放入池中,list存入返回的池中信息,提高性能。

2.6 直接内存

普通调用,需要走系统缓存区和Java缓冲区两次空间,使用直接内存系统本地和Java都可使用。

🐱‍💻 三、垃圾回收

3.1 判断垃圾可回收

3.1.1 引用计数法

若A、B循环引用,虽然A、B都没被引用,但是计数已经+1,故不会被回收。

3.1.2 可达性分析、GCRoots

(TODO:三色标记法)

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收

GCRoots包括以下几类元素:

  • 虚拟机栈中引用的对象

    • 比如:各个线程被调用的方法中使用到的参数、局部变量等。

  • 本地方法栈内JNI(通常说的本地方法)引用的对象

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

    • 比如: Java类的引用类型静态变量

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

    • 比如:字符串常量池(string Table)里的引用

  • 所有被同步锁synchronized持有的对象

  • Java虚拟机内部的引用。

    • 基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。

  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

3.1.3 五种引用

1.强引用

  • 只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。

2.软引用(SoftReference)

  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象。

  • 可以配合引用队列来释放软引用自身。

3.弱引用(WeakReference)

  • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。可以配合引用队列来释放弱引用自身。

4.虚引用(PhantomReference)

  • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由referenceHandler线程调用虚引用相关方法释放直接内存。

5.终结器引用(FinalReference)

  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程(优先级较低,执行概率小)通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象

  • 软引用和弱引用对象被垃圾回收时:

  • 虚引用对象被垃圾回收:

  • 终结器引用被垃圾回收时:

3.1.4 实例

1.软引用

在设置堆内存-Xmax40m后,创建5个Byte[4 * 1024 * 1024]对象,会内存溢出。

public class Test3 { static int _4MB = 2*1024 * 1024; public static void main(String[] args) { ArrayList<Byte[]> bytes = new ArrayList<>(); for (int i = 0;i<5;i++){ bytes.add(new Byte[_4MB]); System.out.println(i); } System.out.println(bytes); } } java.lang.OutOfMemoryError: Java heap space

使用软引用对Byte数组引用,在垃圾回收后若内存不足,将释放软引用的对象

public class Test3 { static int _4MB = 2*1024 * 1024; public static void main(String[] args) { ArrayList<SoftReference<Byte[]>> bytes = new ArrayList<>(); // 引用队列 ReferenceQueue<Byte[]> referenceQueue = new ReferenceQueue<>(); for (int i = 0;i<5;i++){ // 将byte数组进行软引用 将软引用对象放入引用队列 SoftReference<Byte[]> softReference = new SoftReference<>(new Byte[_4MB],referenceQueue); bytes.add(softReference); System.out.println(i); } // 清除队列中的软引用 Reference<? extends Byte[]> poll = referenceQueue.poll(); while (poll != null) { bytes.remove(poll); poll = referenceQueue.poll(); } for (SoftReference<Byte[]> item : bytes) { System.out.println(item.get()); } } }

2.弱引用

static void weakReference(){ ArrayList<WeakReference<Byte[]>> bytes = new ArrayList<>(); // 引用队列 ReferenceQueue<Byte[]> referenceQueue = new ReferenceQueue<>(); for (int i = 0;i<7;i++){ // 将byte数组进行软引用 将软引用对象放入引用队列 WeakReference<Byte[]> softReference = new WeakReference<>(new Byte[_4MB],referenceQueue); bytes.add(softReference); System.out.println(i); } // 清除队列中的软引用 Reference<? extends Byte[]> poll = referenceQueue.poll(); while (poll != null) { bytes.remove(poll); poll = referenceQueue.poll(); } for (WeakReference<Byte[]> item : bytes) { System.out.println(item.get()); } }

3.2 垃圾回收算法 🎏

3.2.1 标记清除

标记清除回收算法分为两步:1、第一次扫描记录可被回收的地址 2、第二次根据地址进行清除

优点:速度快

缺点:回收内存后不会进行合并,会造成内存碎片

3.2.2 标记整理算法

标记整理分为两步:1、标记存活的内存地址 2、整理存活的内存到一起

优点:不会造成内存碎片的问题。

缺点:因为在整理过程中会移动内存地址,效率会降低。

3.2.3 复制算法

复制算法将内存分为from区和to区,将from区中未引用的内存标记,将被引用的内存转到to区,将from区全部释放,然后from区和to区对换。

优点:不会产生内存碎片。

缺点:会占用两倍内存空间。

3.2.4 分代回收

  • 对象首先分配在伊甸园区

  • 新生代空间不够时,触发MinorGC清理整个新生代空间,采用复制算法,将伊甸园和from区的对象复制到to区中,对象存活时间+1,然后from区和to区交换,保证to区为空交换数据内存

  • MinorGC会引发stop the world,暂停其他线程,等垃圾回收结束后用户线程才恢复运行

  • 当幸存区对象寿命超过阈值,会晋升到老年代,最大寿命是15(4bit)

    • 当老年代空间不足,会先尝试触发MinorGC,如果空间仍不足,触发FullGCSTW时间更长


  • 在Eden区内存够的情况下,创建的对象会优先选择放到Eden区。

  • 如果在存入对象时,Eden区的内存不够存的,那就会进行一次minor gc垃圾回收(整个新生代),然后将Eden区survivor from中,然后再清空Eden区,将新对象存入Eden区,若Eden区存不下将直接放入老年代。若survivor区存不下,则将部分对象存入老年区。

  • 之后再进来的对象还是会选择放到Eden区,如果Eden区又存放不下了,这时就会将Eden区和survivor from中存活的对象都复制到survivor to中,然后清空Eden区和survivor from,并且将survivor from和survivor to进行位置交换,目的就是为了保证survivor to中不存放对象。如果这个时候新生代还是放不下这个对象,那该对象就会被放到老年代中。若survivor区存不下,则将部分对象存入老年区。

  • 原文链接

3.3 相关VM参数 🎏

  • 堆初始大小:-Xms

  • 堆最大大小:-Xmx或-XX:MaxHeapSize=size

  • 新生代大小:-Xmn或(XX:NewSize=size +-XX:MaxNewSize=size )

  • 幸存区比例(动态):-Xx:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy

  • 幸存区比例:-Xx:SurvivorRatio=ratio

  • 晋升阈值:-XX:MaxTenuringThreshold=threshold

  • 晋升详情:-XX:+PrintTenuringDistribution

  • Gc详情:-XX:+PrintGCDetails -verbose:gc

  • FullGC前MinorGC:-XX:+ScavengeBeforeFullGC

3.4 垃圾回收器

3.4.1 串行

  • 单线程

  • 堆内存小,适合个人电脑

-XX:-UserSerialGC = Serial(新生代,复制算法)+SerialOld(老年代,标记整理算法)

3.4.2 吞吐量优先

  • 多线程

  • 堆内存较大,多核CPU

  • 让单位时间内,STW时间最短

-XX:+UseParallelGC~ -XX :+UseParalle10ldGc (新生代,复制算法)+(老年代,标记整理算法) -XX:+UseAdaptivesizePolicy 采用自适应调整新生代大小策略 -XX:GCTimeRatio=ratio 垃圾回收时间与总时间的占比 -XX:MaxGCPauseMillis=ms 垃圾回收暂停时间 默认200ms -XX:Paralle1GCThreads=n GC时线程数

3.4.3 响应时间优先 CMS

  • 多线程

  • 堆内存较大,多核CPU

  • 让单次STW时间最短

-XX :+UseConcMarkSweepGC~老年代GC,并发出现问题时,由于标记清除算法造成的内存碎片问题,老年代GC会退回到SerialOld进行一次标记整理

-XX :+UseConcMarkSweepGC~ -XX:+UseParNewGC ~ SerialOld (复制算法)+(标记清除算法)

-XX: ParallelGCThreads=n ~ -XX: ConcGCThreads=threads 并行垃圾回收线程数+并发垃圾回收线程数

-xX:CMSInitiatingOccupancyFraction=percent 当内存占比到percent的时候进行并发标记

-XX :+CNSScavengeBeforeRemark 1,0 重新标记会标记所有引用对象,在并发高时可能新生代对象较多且是垃圾,导致在重新标记花费长时间,在重写标记发生之前对新生代垃圾进行清理,减少重新标记的时间。

当老年代发生内存不足,其中一个线程进行初始标记,仅标记GCRoot对象,到阈值后就可进行并发标记标记所有:

初始标记:在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。

并发标记:这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

并发预清理:并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。

重新标记:这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。

并发清理:清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

3.4.4 G1

博客

  • 注重吞吐量和低延迟,默认暂停目标是200ms

  • 超大堆内存,会将堆划分为多个大小相等的Region

  • 整体上是标记+整理算法,两个区域之间是复制算法

-XX: +UserG1GC JDK9以后默认使用

-XX:G1HeapRegionSize=size 设置region区域大小

-XX:MaxGCPauseMillis=ms

1)G1垃圾回收阶段

年轻代GC->年轻代GC+并发标记->混合回收

2)Young Collection

  • 会STW

3)Young Collection + CM

  • 在Young GC是会进行GC Root的初始标记

  • 老年代占用堆空间比例达到阈值时,进行并发标记(STW)

-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

4)Mixed Collection

会对E、S、O进行全面垃圾回收

  • 最终标记(Remark)会STW

  • 拷贝存活(Evacuation)会STW

-XX:MaxGCPauseMillis=ms

在回收老年代Region时,会根据最大暂停时间,选择部分回收价值最高的即能回收的垃圾最多的Region进行回收。

5)Full GC

SerialGC

  • 新生代内存不足发生的垃圾收集 - minor gc

  • 老年代内存不足发生的垃圾收集 - full gc

ParallelGC

  • 新生代内存不足发生的垃圾收集 - minor gc

  • 老年代内存不足发生的垃圾收集 - full gc

CMS

  • 新生代内存不足发生的垃圾收集 - minor gc

  • 老年代内存不足

G1

  • 新生代内存不足发生的垃圾收集 - minor gc

  • 老年代内存不足

6)Young Collection跨代引用

新生代垃圾回收的过程:

  1. 首先要找到根对象

  2. 然后对根对象进行可达性分析,找到存活对象

  3. 对存活对象进行复制,复制到幸存区

产生问题:

  • 找到新生代对象的根对象,根对象有一部分是来自老年代的,而老年代存活的对象一般都特别多,如果去遍历整个老年代,效率非常低。

采用了cart table的方式,对老年代进行细分,分成了许多个card,每个card大约是512K。如果老年代某个对象,引用了新生代的对象,我们把这个老年代的对象标记为脏card。这样,找老年代的根对象时,就不用遍历整个老年代了,只需要关注脏card,减小搜索范围,提高效率。

如下图,粉色为脏card,绿色为伊甸园区,蓝色为幸存者区,橙色为老年代。

  • 老年代有脏卡标记,而新生代则有remembered Set记录外部对它的引用,记录都有哪些脏卡。将来对新生代进行垃圾回收时,先通过remembered Set 知道有哪些脏卡,然后通过脏卡区域遍历GC Root。

  • 在引用变更时通过post-write barrier + dirty card queue,在每次的引用变更时,都要更新标记脏卡(异步操作,把更新的指令放到一个队列(dirty card queue)之中,将来由一个线程,执行更新操作)。

7)remark

在并发标记阶段当对象引用改变时,JVM会加入一个写屏障,引用改变,写屏障指令就会被执行,会将该对象加入一个队列中,将对象变为为处理状态,等到整个并发标记结束,进入重新标记阶段会STW, 对象出队列,进行标记。

8)字符串去重

9)JDK 8u40并发标记类卸载所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 -XX:+ClassUnloadingWithConcurrentMark 默认启用

10)回收巨型对象

一个对象大于region的一半时,称之为巨型对象.G1不会对巨型对象进行拷贝,回收时被优先考虑。 G1会跟踪老年代所有incoming 引用,这样老年代 incoming 引用为0的巨型对象可以在新生代垃圾回收。

11)JDK9并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为FullGC

  • JDK9之前需要使用-Xx:InitiatingHeapOccupancyPercent- JDK 9可以动态调整

    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值

    • 进行数据采样并动态调整

    • 总会添加一个安全的空档空间

3.5 垃圾回收调优 🎏

查看虚拟机运行参数:java -XX:+PrintFlagsFinal -version | findstr "GC"

3.5.1 调优领域

内存、锁竞争、CPU占用、IO、GC

3.5.2 确定目标

  • 【低延迟】还是【高吞吐量】

  • 选择合适的回收器

  • CMS,G1,ZGC (低延迟,响应时间优先)

  • ParallelGC

  • Zing

3.5.3 最快的 GC

最快的GC是不发生GC,查看Full GC前后的内存占用,考虑以下几个问题:

  • 数据是不是太多?

    • resultSet = statement.executeQuery("select * from 大表")

  • 数据表示是否太臃肿? 对象图 对象大小

  • 是否存在内存泄漏

3.5.4 新生代调优

  • 新生代的特点

    • 所有的new操作分配内存都是非常廉价的

    • TLAB thread-local allocation buffer(可防止多个线程创建对象时的干扰)

    • 死亡对象回收零代价

    • 大部分对象用过即死(朝生夕死)

    • Minor GC 所用时间远小于Full GC

  • 新生代内存越大越好么?不是

    • 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降

    • 新生代内存太大:老年代内存占比有所降低,会频繁地触发Full GC。而且触发Minor GC时,清理新生代花费的时间更长

    • 新生代内存设置为能容纳[并发量*(请求-响应)]的数据为宜

    • 幸存区大到能保留【当前活跃对象+需要晋升对象】

    • 晋升阈值配置得当,让长时间存活对象尽快晋升

3.5.5 老年代调优

以 CMS 为例 :

  • CMS 的老年代内存越大越好

  • 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代

  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

    • -XX:CMSInitiatingOccupancyFraction=percent

🐱‍🐉 四、类加载

4.1 类加载过程

4.1.1加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用

    • _super 即父类

    • _fields 即成员变量

    • _methods 即方法

    • _constants 即常量池

    • _class_loader 即类加载器

    • _vtable 虚方法表

    • _itable 接口方法表

  • 如果这个类还有父类没有加载,先加载父类

  • 加载和链接可能是交替运行的

结果:加载完成之后会在堆中实例化一个Java类的原型模板—–类模板对象(Class对象),该对象用来访问方法区中的类信息,方法信息,域(filed)信息,使用new关键字创建对象的时候,首先会去这个类对应的Class对象获取到该类的信息,然后再创建对象。因此将Class对象看做是类的模板,类创建的对象可以有很多,但是模板只有一份,也就是说每个类对应的Class对象只有一个。

注意:

instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内), _java_mirror是存储在堆中。

可以通过前面介绍的 HSDB 工具查看。

4.1.2链接

1)验证

验证类是否符合 JVM规范,安全性检查。这一步骤是确保Class文件的字节流中包含的信息要符合虚拟机规范中的要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。

2) 准备

这个阶段做的事情就是为静态变量分配内存,然后赋值(普通静态变量赋默认值,加上final的静态变量直接赋值)。

  • 为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

3)解析

将常量池中的符号引用解析为直接引用

public class Load2 { public static void main(String[] args) throws ClassNotFoundException,IOException { ClassLoader classloader = Load2.class.getClassLoader(); // loadClass 方法不会导致类的解析和初始化 Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C"); // new C(); System.in.read(); } } ​ class C { D d = new D(); } ​ class D { }

4.1.3 初始化

4.3初始化

<clinit>()方法,是由编译器自动收集类中的所有类变量的赋值动作静态语句块中的语句合并产生的

初始化即调用 <clinit>() ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化

  • 首次访问这个类的静态变量或静态方法时

  • 子类初始化,如果父类还没初始化,会引发

  • 子类访问父类的静态变量,只会触发父类的初始化

  • Class.forName

  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化

  • 类对象.class 不会触发初始化

  • 创建该类的数组不会触发初始化

实验

class A { static int a = 0; static { System.out.println("a init"); } } class B extends A { final static double b = 5.0; static boolean c = false; static { System.out.println("b init"); } }

验证(实验时请先全部注释,每次只执行其中一个)

public class Load3 { static { System.out.println("main init"); } public static void main(String[] args) throws ClassNotFoundException { // 1. 静态常量(基本类型和字符串)不会触发初始化 System.out.println(B.b); // 2. 类对象.class 不会触发初始化 System.out.println(B.class); // 3. 创建该类的数组不会触发初始化 System.out.println(new B[0]); // 4. 不会初始化类 B,但会加载 B、A ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl.loadClass("cn.itcast.jvm.t3.B"); // 5. 不会初始化类 B,但会加载 B、A ClassLoader c2 = Thread.currentThread().getContextClassLoader(); Class.forName("cn.itcast.jvm.t3.B", false, c2); // 1. 首次访问这个类的静态变量或静态方法时 System.out.println(A.a); // 2. 子类初始化,如果父类还没初始化,会引发 System.out.println(B.c); // 3. 子类访问父类静态变量,只触发父类初始化 System.out.println(B.a); // 4. 会初始化类 B,并先初始化类 A Class.forName("cn.itcast.jvm.t3.B"); } }

4.2类加载器

1.引导类加载器(BootstrapClassLoader)

  • c/c++语言实现,嵌套在jvm内部

  • 用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar或者sum.boot.class.path)

  • 不继承java.lang.ClassLoader,没有父加载器

  • 出于安全考虑,只加载包名为java、javax、sum等开头的类

  • 加载扩展类加载器和系统类加载器,并指定为他们的父加载器。

  • 无法被获取

2.拓展类加载器(ExtensionClassLoader):

  • Java语言编写,由sum.misc.Launcher$ExtClassLoader实现

  • 继承于ClassLoader类

  • 父类加载器为引导类加载器

  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户常见的JAR放在此目录下,也会有拓展类加载器加载

3.系统类加载器(AppClassLoader):

  • Java语言编写,由sum.misc.Launcher$AppClassLoader实现

  • 继承于ClassLoader类

  • 父类加载器为引导类加载器

  • 负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库

  • 应用程序中的类加载器默认是系统类加载器

  • 它是用户自定义类加载器的默认父加载器

  • 通过ClassLoader的getSystemClassLoader()方法可以获取到系统类加载器

4.用户自定义类加载器:

  • 通过自定义类加载器可以实现插件机制

  • 通过自定义类加载器能够实现应用隔离

  • 自定义类加载器要继承与ClassLoader

类加载分类

  1. 显示加载指的是在代码中通过调用ClassLoader加载class对象,例如直接使用Class.forName(name)或者this.getClass().getClassLoader().loadClass(name)加载对象

  2. 隐式加载不在代码中调用ClassLoder的方法加载class对象,而是通过虚拟机自动加载到内存中,例如,在某个类的class文件中引用了另一个类的对象,额外引用的类会通过jvm自动加载到内存

4.2.1 启动类加载器

用 Bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load; public class F { static { System.out.println("bootstrap F init"); } }

执行

package cn.itcast.jvm.t3.load; public class Load5_1 { public static void main(String[] args) throws ClassNotFoundException { Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F"); System.out.println(aClass.getClassLoader()); } }

输出

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5 bootstrap F init null
  • -Xbootclasspath 表示设置 bootclasspath

  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后

  • 可以用这个办法替换核心类

    • java -Xbootclasspath:<new bootclasspath>

    • java -Xbootclasspath/a:<追加路径> 后追加

    • java -Xbootclasspath/p:<追加路径> 前追加

4.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 检查该类是否已经加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 2. 有上级的话,委派上级 loadClass c = parent.loadClass(name, false); } else { // 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { long t1 = System.nanoTime(); // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载 c = findClass(name); // 5. 记录耗时 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
public class Load5_3 { public static void main(String[] args) throws ClassNotFoundException { Class<?> aClass = Load5_3.class.getClassLoader() .loadClass("cn.itcast.jvm.t3.load.H"); System.out.println(aClass.getClassLoader()); } }

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级sun.misc.Launcher$ExtClassLoader.loadClass()

  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

  4. 委派上级BootstrapClassLoader

  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader的 // 2 处

  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了

4.4 自定义类加载器

什么时候需要自定义类加载器

1)想加载非 classpath 随意路径中的类文件

2)都是通过接口来使用实现,希望解耦时,常用在框架设计

3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类

  2. 要遵从双亲委派机制,重写 findClass 方法,注意不是重写 loadClass 方法,否则不会走双亲委派机制

  3. 读取类文件的字节码

  4. 调用父类的 defifineClass 方法来加载类

  5. 使用者调用该类加载器的 loadClass 方法

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/16 2:01:20

要让 SAP SD 销售订单行项目里的“重量”“毛重”等字段重新可编辑,99% 的情况都不是权限问题,而是系统标准逻辑

要让 SAP SD 销售订单行项目里的“重量”“毛重”等字段重新可编辑&#xff0c;99% 的情况都不是权限问题&#xff0c;而是系统标准逻辑&#xff1a;只要该行已经生成了交货单&#xff08;Delivery&#xff09;&#xff0c;这些属于「装运层」的字段就被自动锁掉&#xff0c;避…

作者头像 李华
网站建设 2026/3/25 20:17:33

k6负载测试实战:从架构解析到企业级应用部署

k6负载测试实战&#xff1a;从架构解析到企业级应用部署 【免费下载链接】k6 A modern load testing tool, using Go and JavaScript - https://k6.io 项目地址: https://gitcode.com/GitHub_Trending/k6/k6 k6作为现代化的性能测试工具&#xff0c;正在重新定义企业级负…

作者头像 李华
网站建设 2026/3/24 11:50:49

django基于Python员工管理系统

&#x1f345; 作者主页&#xff1a;Selina .a &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行交流合作。 主要内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据…

作者头像 李华
网站建设 2026/3/25 7:49:16

24、高级概念:Debian内核包构建与模块编译指南

高级概念:Debian内核包构建与模块编译指南 在Debian系统管理中,面对特殊需求时,有许多强大的工具可供使用。本文将重点介绍如何使用 make-kpkg 工具构建定制化的内核包、处理内核模块编译,以及相关的高级操作。 1. make-kpkg 工具概述 make-kpkg 是Debian的内核包工…

作者头像 李华
网站建设 2026/3/24 9:32:52

26、Debian系统安装与管理高级概念

Debian系统安装与管理高级概念 1. aptitude与多版本管理 1.1 多版本选择 当系统配置使用多个APT源时,APT可能会获取到同一软件包的多个版本。 apt-get 可通过在软件包名后加等号和版本号来安装特定版本。而 aptitude 的用户界面强大之处在于,它会在软件包详情页底部显…

作者头像 李华
网站建设 2026/3/22 1:03:03

29、Debian 包构建工具与 pbuilder 使用指南

Debian 包构建工具与 pbuilder 使用指南 在 Debian 系统中构建软件包是一项常见的任务,传统上使用 debian/rules 文件(通常是 Perl 或 make 脚本)来完成。不过,还有其他一些替代的构建工具,如 cdbs 和 yada ,它们各自有独特的优势。同时, pbuilder 作为一个个人…

作者头像 李华