深入理解 Java 虚拟机(JVM)从入门到精通
目录
- 一、JVM内存结构
- 1、堆(Heap)
- (1)特点
- (2)堆内存分配
- (3)晋升到老年代的方式
- (4)堆内存检验方式
- 2、虚拟机栈(VM Stack)
- (1)特点
- (2)局部变量表
- (3)操作栈
- (4)动态连接
- (5)方法返回地址
- (6)栈内存溢出
- 3、程序计数器
- (1)特点
- (2)举例
- 4、本地方法栈
- 5、方法区
- 6、方法栈和本地方法栈的区别
- 7、运行时常量池和字符串常量池的区别
- 8、总结
- 二、垃圾回收(Garbage Collection)
- 1、垃圾判定
- (1)引用计数法(Reference Counting)
- (2)可达性分析(Reachability Analysis)
- 2、垃圾回收算法
- (1)标记-清除
- (2)标记-整理
- (3)复制
- 3、Minor GC 和 Full GC 的区别
- 4、空间担保策略
- 5、垃圾回收器
- 三、类的加载过程
- 四、双亲委派机制
- 1、双亲委派机制特点
- 2、如何打破双亲委派机制
- 五、直接内存
- 六、JAVA中的四种引用
- 七、JVM常用调优参数
- 八、JVM调优
- 1、JVM调优方法
- 2、OOM发生区域
- 3、常见分析工具
- 4、模拟OOM和分析示例
本文将按照以下思维导图的结构,深入讲解Java虚拟机(JVM)的核心概念
一、JVM内存结构
在网上借鉴几张图片,可以很形象看出jvm的内存结构
1、堆(Heap)
堆是JVM内存中最大的一块,用来存储对象和数组,它被所有线程共享
。
(1)特点
- 通过 new 关键字,创建的对象都会使用堆内存,数组和字符串常量池(StringTable)也存储在堆中
- 它是线程共享的
- 堆中对象都需要考虑线程安全的问题,有垃圾回收机制
(2)堆内存分配
在 Java 的堆内存中,可以分配为新生代
和老年代
的主要依据是对象的生命周期。这个分配是为了更好地进行垃圾回收和提高内存利用率。默认分配比例如下:
- 新生代(Young Generation):
新生代由伊甸园(Eden Space)
和 两个幸存者区(Survivor Space)
组成。
伊甸园(Eden Space):伊甸园是新生代中的一部分,用于存放新创建的对象
。大部分对象在伊甸园中被创建。当内存需要分配给新对象时,大部分对象都会首先被放入伊甸园中。
幸存者区(Survivor Space):幸存者区包括两个区域,分别为From区
和To区
。幸存者区的数据是在 From 区和 To 区之间进行交换的。
例如:当from区和to区都是null的时候,第一次从新生代eden进行垃圾回收,会把存活下来的对象放入from区,下次垃圾回收会把存活下来的数据放入to区,然后from区清空。再下次垃圾回收会把存活下来的数据放入from区,然后to区清空。直到达到一定的年龄后,这些对象会被晋升到老年代。
老年代(Old Generation):用于存放新生代中经过多次gc依然存活的对象,或者新生代中放不下的大对象。
(3)晋升到老年代的方式
- 年龄阈值:当对象在 survivor 区存活了 15 次(默认)之后,会被移到老年代区。可以通过JVM参数
-XX:MaxTenuringThreshold
修改。 - 大对象:当对象大于survivor区空间一半(默认)的时候,会被移到老年代区。可以通过JVM参数
-XX:PretenureSizeThreshold
参数设置 - survivor空间不足:当存活下来的对象大于survivor区容量的时候,会被移到老年代区。
假设新生代由100MB的Eden空间和两个50MB的Survivor空间组成,老年代有500MB的空间。
初始情况下,所有新创建的对象都分配在Eden空间。
进行第一次GC,此时Eden空间有80MB的对象,被GC后只有30MB的对象存活。这些存活的对象被移动到Survivor1,Eden被清空。
再次分配对象,Eden空间再次填满到80MB,此时Survivor1中还有30MB的存活对象。
进行第二次GC,Eden区的80MB对象中,60MB存活,加上Survivor1中的30MB存活对象,一共有90MB需要被移动到Survivor2,但Survivor2只有50MB的容量。
此时,JVM会检查Survivor1中对象的年龄,并将年龄大的对象提前晋升到老年代
,假设10MB的对象被晋升,这样剩下20MB的对象与Eden区的60MB存活对象能够被移动到Survivor2。
如果Survivor空间依旧不足以处理这60MB的对象,那么无论年龄如何,都会将多出来的部分提前晋升到老年代
。
GC的这些细节实际上取决于使用的垃圾收集器以及JVM的配置参数,不同的垃圾收集器(如Serial, Parallel, CMS, G1,
ZGC等)会以不同的方式管理这些区域。
(4)堆内存检验方式
1、jmap
- 首先使用
jps
查看有哪些进程 - 然后根据
jmap -heap [进程ID]
查看进程的堆内存
实例:
new一个10M的字节对象,来占用堆内存,在输出分别在输出 1 2 3后打出 jmap命令对比堆内存变化
public class Test {public static void main(String[] args) throws InterruptedException {System.out.println("1111111111111111111111111");Thread.sleep(20000);byte[] array = new byte[1024 * 1024 * 10]; // 10M内存System.out.println("2222222222222222222222222");Thread.sleep(20000);System.gc(); // 垃圾回收System.out.println("3333333333333333333333333");Thread.sleep(10000);}
}
打出 111111 后先根据 jps
命令查看到进程id
23968 Test
3312
24196 Jps
22764 Launcher
4828 RemoteMavenServer36
可以看出启动类Test进程ID是23968,然后输入命令:jmap -heap 23968
Heap Usage:
PS Young Generation
Eden Space:capacity = 66584576 (63.5MB)used = 8754440 (8.348884582519531MB) // 这里只展示部分打印信息,可以看见这里最初占用了8M13.14784973625123% used
From Space:capacity = 11010048 (10.5MB)used = 0 (0.0MB)free = 11010048 (10.5MB)0.0% used
然后控制台打印22222222后,继续输入命令:jmap -heap 23968
Heap Usage:
PS Young Generation
Eden Space:capacity = 66584576 (63.5MB)used = 19240216 (18.348899841308594MB) // 可以看见这里占用内存变成了18M28.895905261903298% used
From Space:capacity = 11010048 (10.5MB)used = 0 (0.0MB)free = 11010048 (10.5MB)0.0% used
然后控制台打印333333333后,继续输入命令:jmap -heap 23968
Heap Usage:
PS Young Generation
Eden Space:capacity = 66584576 (63.5MB)used = 1331712 (1.27001953125MB) // 可以看见这里占用内存变成了1Mfree = 65252864 (62.22998046875MB)2.0000307578740157% used
From Space:capacity = 11010048 (10.5MB)used = 0 (0.0MB)free = 11010048 (10.5MB)0.0% used
2、jconsole
还是运行刚才代码,然后执行jconsole
命令,选择’本地连接’->'对应进程’用图形查看该进程的堆内存变化
3、jvisualvm
如下代码可以使堆内存在1万秒内增加200M内存占用空间。以便模拟我们排查问题
public class Test {public static void main(String[] args) throws InterruptedException {List<Student> students = new ArrayList<>();for (int i =0; i < 200 ;i++){students.add(new Student());}Thread.sleep(10000000); // 10000秒}
}
class Student {private byte[] big = new byte[1024*1024];
}
首先我们还是jconsole 查看,并点击了垃圾回收,但是毫无作用,说明这个类一直被占用。
然后我们输入 jvisualvm
,根据图片进行操作
到了这里,我们可以很清楚看见,是Test这个类下面,一个Student的数组引起的,即可找到代码解决问题
2、虚拟机栈(VM Stack)
每个线程都有自己的虚拟机栈,这个栈用于存储栈帧。每当一个线程调用一个方法时,JVM就会为这个方法创建一个栈帧,并且将它压入虚拟机栈中。栈帧是用来存储局部变量、执行运算过程中的操作栈、动态链接信息以及方法返回地址等数据。
(1)特点
线程私有,每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
(2)局部变量表
用来存储方法的参数
和方法内部定义的局部变量
。这些数据包括各种基本数据类型(int、float、long、double等)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。
举例:
int sum(int a, int b) {int result = a + b;return result;
}
在调用sum方法时,它的局部变量表将会包含以下内容:
a 的值
b 的值
result 变量
(3)操作栈
每个栈帧内部含有一个操作数栈,通常也叫做操作栈。这是一个后进先出(LIFO)的栈,用于执行方法中的字节码指令。操作数栈的主要作用是作为计算过程中的临时存储空间,用于存储操作指令的输入和输出参数
。
举例:
public int addNumbers(int a, int b) {int result = a + b;return result;
}
当这个方法被调用时,JVM会使用操作栈来执行计算过程。以下是一个简化的操作栈示例:
操作 | 操作数栈
---------------------
// 初始状态: 操作数栈为空
iload_1 // 将第一个参数a压入操作数栈
iload_2 // 将第二个参数b压入操作数栈
iadd // 将栈顶两个元素相加
istore_3 // 将结果存储到局部变量表的索引3(即result)
在这个例子中,iload_1 和 iload_2 将参数 a 和 b 压入操作数栈,iadd 从栈中弹出这两个参数相加,然后 istore_3 将结果存储到局部变量表中的 result 变量中。
(4)动态连接
每个栈帧内部含有一个指向运行时常量池中该栈帧所属方法的引用,这使得当前方法能够动态链接到其它方法和变量。简而言之,动态连接是指方法在运行时实际引用的地址可以被替换成其他的方法或变量地址,这为Java的多态和方法重载提供了基础。
举例:
class A {void foo() {System.out.println("A's foo()");}
}class B extends A {void foo() {System.out.println("B's foo()");}
}public class Test {public static void main(String[] args) {A obj = new B();obj.foo(); // 动态链接到B类的foo()方法}
}
在上述代码中,虽然变量 obj 的类型是 A,但在运行时,obj.foo() 动态链接到了 B 类的 foo 方法。
(5)方法返回地址
当一个方法开始执行后,它需要知道在完成执行后返回到哪里。方法返回地址就是保存这个信息的地方,它指向调用该方法的位置的下一条指令地址。
举例:
void caller() {callee();int a = 10; // 当callee方法完成之后,返回到这里继续执行
}void callee() {// do somethingreturn; // 在这里,方法返回地址指向caller方法中callee调用之后的指令
}
在 caller 方法中调用 callee 方法后,JVM 会在 callee 方法的栈帧中存储返回地址,当 callee 方法执行完毕后,控制权将会返回到 caller 方法中 callee 调用后的位置。
(6)栈内存溢出
- 栈帧过多导致栈内存溢出(如方法递归调用没有设置下线)
- 栈帧过大导致栈内存溢出
用debug方式演示:
每个线程会创建一个虚拟机栈,每个方法会创建一个栈帧,放入虚拟机栈。当走到方法b时就会创建三个栈帧(main,a,b),每个方法里面的参数(如变量x e)会被放入到这个栈帧里面。当调用方法b完成回到方法a时,就会释放方法b栈帧
(注:栈内存会自己释放,因此不需要垃圾回收)
栈是不是越大越好?
不是,如内存为500M,每个栈为1M,那么最多可以有500个线程并发。所以栈越大,线程越少。
3、程序计数器
记住下一条jvm指令的执行地址
(1)特点
- 是线程私有的(每个线程都有自己的程序计数器,因为每个线程执行地址不一样)
- 不会存在内存溢出(由jvm规定的)
(2)举例
public class Example {public static void main(String[] args) { // 1int a = 5; // 2int b = 10; // 3int c = addNumbers(a, b); // 4 System.out.println(c); // 7}public static int addNumbers(int a, int b) {int result = a + b; // 5return result; // 6}
}
在上面的方法中,程序计算器指向的地址分别是1到7,代码执行的每一步操作都会被记录
4、本地方法栈
本地方法栈的结构与虚拟机栈类似,也是由栈帧(Stack Frame)组成的,栈帧中保存了Native方法的局部变量、操作数栈、方法出口等信息。与虚拟机栈不同的是,本地方法栈中的方法不是用Java语言编写的,而是用其它语言编写的,比如C、C++等。
因此,本地方法栈的结构与虚拟机栈类似,但是用于调用本地方法。
这里有一个简单的示例,演示了一个Java程序如何调用一个使用C语言编写的Native方法:
public class NativeExample {static {System.loadLibrary("NativeLibrary");}public native void nativeMethod();public static void main(String[] args) {NativeExample example = new NativeExample();example.nativeMethod();}
}
在这个示例中,NativeExample类中的nativeMethod方法是一个本地方法,它用native关键字修饰,表示这个方法是用其它语言实现的。在main方法中,通过example.nativeMethod()调用了这个本地方法。在执行时,虚拟机会使用本地方法栈来执行native Method方法的相关操作。
5、方法区
方法区实现方式:永久代、元空间
在早期的 Java 版本中,方法区与永久代有着密切的关系。方法区是一块用于存储类的相关信息、常量、静态变量、即时编译器优化后的代码等数据的内存区域
。而永久代是 HotSpot 虚拟机中的概念,它实际上就是方法区的一种实现
。
在 Java 7 及之前的版本中,永久代用于存储类和方法相关的信息,包括类的字节码、运行时常量池、字段、方法、构造函数等。由于永久代的大小在JVM启动时固定,并且随着应用的运行可能会出现永久代内存溢出的错误(OutOfMemoryError),在Java 8中被元空间所替代。
因此,从 Java 8 开始,永久代逐渐被元空间(Metaspace)所取代
。它使用本地内存(即非JVM堆内存)来存储类元数据。这样的设计减少了内存溢出的可能性,因为元空间的大小仅受到系统可用内存的限制。当然,元空间中还是有一个初始大小,并且可以设置上限,一旦超过这个上限,仍然会抛出OutOfMemoryError异常。因此,方法区与永久代之间的关系在 Java 8 及以后的版本中已经不再存在。
元空间主要包括以下内容:
- 类的元数据信息:包括类的名称、方法名、访问修饰符、字段描述符等。
- 静态变量:类的静态变量存放在元空间中。
- 常量池:其中存放着字符串常量、字面量和符号引用。
方法区的对象不会被Java堆中的垃圾回收器以相同的方式回收,它有自己的内存管理系统(在使用元空间的情况下,内存可以从操作系统直接获取)。
让我们通过一段简单的Java代码,说明方法区中某些部分是如何被使用的:
public class ExampleClass {// 常量池中的内容private final static String CONSTANT_STRING = "Hello, World!";// 方法区中静态变量private static int counter = 0;// 类型信息和方法代码public static void increment() {counter++;}public static void main(String[] args) {ExampleClass.increment();System.out.println(CONSTANT_STRING);}
}
在上述代码中:
字符串"Hello, World!"会被存储在常量池中。
静态变量counter会被存储在方法区。
类ExampleClass的类型信息(比如它的方法和字段)也会存储在方法区。
increment方法和main方法的代码,在被即时编译器编译之后,编译后的机器码也会存储在方法区。
当Java程序运行时,JVM会加载ExampleClass,这个过程中会将ExampleClass的类型信息、常量池中的常量、increment和main方法的字节码等数据存储在方法区,静态变量counter同样存储在方法区内,但具体是在永久代还是元空间则取决于JVM的版本及配置。在Java 8及之后版本,这部分数据会存储在操作系统的本地内存中,称作元空间。
6、方法栈和本地方法栈的区别
方法栈(Method stack):
- 方法栈存储的是 Java 方法的调用信息。每当一个方法被调用时,JVM都会在方法栈中分配一个栈帧(Stack Frame),用于存储该方法的调用信息。
- 方法栈中的栈帧会随着方法的调用和返回而动态地被创建和销毁,方法栈的栈帧也包括了方法的参数、局部变量以及用于返回的指令地址等信息。
本地方法栈(Native method stack):
- 本地方法栈则是用于执行本地(Native)方法的栈,即使用本地语言(如 C 或 C++)编写的方法。它与方法栈类似,但是用于执行本地方法。
- 本地方法栈也会为每个本地方法分配一个栈帧,用于存储本地方法的调用信息。
7、运行时常量池和字符串常量池的区别
- 运行时常量池:存在于元空间中,用于存储字符串常量、字面量和符号引用。
- 字符串常量池(String Table): 存在于堆中(jdk8), 存储的是程序中创建的字符串对象,包括字符串常量和通过 intern 操作成为字符串常量的字符串对象。
8、总结
- 程序计数器:存储jvm指令的执行地址,不会内存溢出
- 虚拟机栈:每个线程运行时所需要的内存,每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存, 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 本地方法栈:存储非java代码编写的本地方法
- 堆:通过 new 关键字,创建对象都会使用堆内存。同时包含字符串常量池和数组。
- 方法区:它存储每个类的结构,如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法
共同特点:
- 程序计数器、栈是线程私有;方法区、堆是线程共享。
- 程序计数器不会内存溢出,其他都会。
二、垃圾回收(Garbage Collection)
1、垃圾判定
垃圾判定是指在编程中确定哪些内存中的对象是“垃圾”,即不再被应用程序使用的对象,因此可以被垃圾回收器回收的过程。
在Java中,垃圾回收(Garbage Collection, GC)主要采用两种基本方法:引用计数法和可达性分析。下面分别对这两种方法进行说明:
(1)引用计数法(Reference Counting)
引用计数算法是一种最直观的垃圾收集技术。其基本思想是给每个对象分配一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。
任何时刻计数器为0的对象就是不可能再被使用的,因此可以回收其占用的内存。
不过,Java并不采用引用计数法来进行垃圾回收,因为它存在循环引用的问题。在循环引用中,两个或多个对象相互引用,但它们可能都已经不再被其他活动部分的应用程序所引用。由于它们相云引用,因此它们的引用计数永远不会达到0,导致内存泄漏。
public class ReferenceCounting {Object instance = null;public static void main(String[] args) {ReferenceCounting objA = new ReferenceCounting();ReferenceCounting objB = new ReferenceCounting();// 创建循环引用objA.instance = objB;objB.instance = objA;// 尝试手动置空以断开引用objA = null;objB = null;// 希望GC能回收objA和objB,但如果是采用引用计数法,则无法回收System.gc();}
}
(2)可达性分析(Reachability Analysis)
Java采用的是可达性分析算法来进行垃圾回收。在这种方法中,通过一系列的称为“GC Roots”的对象作为起点,然后向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达)
时,则证明此对象是不可用的。
在Java中,可作为GC Roots的常见对象包括:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象:例如,正在执行的方法中的局部变量或参数。
- 方法区中类静态属性引用的对象:这些静态变量所引用的对象也被称为 GC Roots。
- 方法区中常量引用的对象:例如,字符串常量池(String Table)的引用
- 本地方法栈中JNI(Java Native Interface)引用的对象:在使用 JNI 调用本地方法的过程中,会涉及到本地方法栈,其中引用的对象也是 GC Roots。
举一个简单的例子来描述可达性分析:
public class ReachabilityAnalysis {public static void main(String[] args) {ReachabilityAnalysis obj = new ReachabilityAnalysis(); // 对象obj是可达的,因为它被栈上的引用变量所引用// 现在让我们断开这个引用obj = null; // 此时对象不再可达// 垃圾回收可以执行了,它将使用可达性分析来确定obj的内存是否可以被释放System.gc();}
}
在JVM模型中,垃圾回收主要发生在堆内存(Heap)中,因为这里是存放对象实例的地方。当前主流的JVM使用分代垃圾收集算法,将堆内存分为年轻代(Young Generation),老年代(Old Generation),以及永久代(Permanent Generation,但在Java 8及之后被MetaSpace所替代)。不同代的对象会根据其生命周期的不同被相应的垃圾回收器回收,以提高回收效率。
垃圾回收算法、垃圾回收器的选择以及垃圾回收的时机,通常是由JVM自动管理的,但是开发者可以通过JVM参数来对其进行调优。
2、垃圾回收算法
(1)标记-清除
标记-清除算法分为两个阶段:标记阶段和清除阶段。
- 标记阶段:从根对象(如活动线程的堆栈指针、静态对象等)开始,递归遍历所有可达的对象,并将它们标记为活动的。
- 清除阶段:遍历堆内存中所有对象,对于没有被标记为活动的对象,释放其占用的内存空间。
缺点:
- 整个过程中需要停止应用程序,导致停顿时间(STW,Stop-The-World)。
- 会产生内存碎片。
(2)标记-整理
标记-整理算法是标记-清除的改进版。在标记活动对象之后,它会将所有存活的对象移到内存的一端,然后清理掉端边界外的内存空间。
优点:
- 解决了内存碎片问题,不需要复制活动对象。
缺点:
- 需要移动存活对象,可能会造成较大的内存迁移开销。
- 需要较多的停顿时间,不适合对响应时间要求较高的应用。
(3)复制
复制算法将堆内存分为两半:一半用于分配内存,另一半处于空闲状态。在垃圾收集期间,它将所有活动对象从当前的内存区域复制到另一半,接着清除原有的内存区域中的所有对象。
优点:
- 解决了内存碎片问题,适合存活对象较少场景。
缺点:
- 不适用于处理存活较多对象的场景
- 会占用双倍内存空间
3、Minor GC 和 Full GC 的区别
Minor GC:对新生代的垃圾回收
Full GC :对堆(新生代、老年代)和方法区(永久代/元空间)的垃圾回收
推荐参考:JVM中 Minor GC 和 Full GC 的区别
4、空间担保策略
空间担保策略是指当触发 minor gc 时,会判断老年代剩余最大连续空间大于历次Minor GC晋升的平均大小 或者 大于新生代所有对象的大小总和 , 大于任意一个,就允许触发MinorGC,反之触发 Full GC
推荐参考:深入理解JVM内存空间的担保策略
5、垃圾回收器
JDK 8 中默认的垃圾回收器组合为Parallel Scavenge(用于Young Generation)加上Parallel Old(用于Old Generation)。
推荐参考:Java中常用的垃圾回收器
三、类的加载过程
Java类加载主要分为三个阶段:加载、链接、初始化
推荐参考:深入理解Java类加载过程
四、双亲委派机制
双亲委派是 Java 类加载器的一种机制。当一个类加载器收到加载类的请求时,它会首先将这个请求委派给父类加载器去完成。只有当父类加载器无法完成这个加载请求时,子类加载器才会尝试加载。
1、双亲委派机制特点
- 避免重复加载:由于双亲委派机制,如果一个类已经被某个类加载器加载过了,那么其他的类加载器就没有必要再加载一次,可以直接复用已经加载的类。这样可以避免类的重复加载,节省内存。
- 安全性:通过双亲委派机制,核心类库会被由启动类加载器加载,因此可以防止核心类库被恶意篡改。另外,由于类加载器可以通过双亲委派机制追溯到启动类加载器,所以可以确保核心类库不会被自定义的类所替代,从而保证了系统安全性。
2、如何打破双亲委派机制
要打破双亲委派机制,可以自定义类加载器,并重写 ClassLoader 类中的 loadClass(String name, boolean resolve)
方法(或者是 findClass(String name)
方法,根据具体需求)。自定义的类加载器可以先尝试加载类,而不是直接委派给父加载器。
下面是一个简化的示例,说明如何自定义类加载器以打破双亲委派模型:
public class CustomClassLoader extends ClassLoader {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {// 首先, 检查请求的类是否已经被加载Class<?> c = findLoadedClass(name);if (c == null) {try {// 尝试自己加载类,而不是委派给父类加载器c = findClass(name);} catch (ClassNotFoundException e) {// 如果自己无法加载类,那么调用父类加载器尝试加载c = super.loadClass(name);}}return c;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 在这里加入具体的类加载逻辑,比如从文件系统中读取.class文件的字节流// byte[] classBytes = ...;// return defineClass(name, classBytes, 0, classBytes.length);// 示例中没有具体实现,因为它通常需要读取文件或其他数据源中的类数据throw new ClassNotFoundException();}
}
在这个例子中,findClass(String name) 方法被重写用于尝试加载类。如果在 findClass 中没有找到类,则会抛出 ClassNotFoundException 异常,然后调用父类加载器尝试加载。
注意,直接破坏双亲委派机制可能会导致各种问题,如类冲突、安全问题等。因此,在实际开发中,只有在真正需要时才应该打破双亲委派模型,并且必须非常小心地实现。
自定义类加载器可以用在很多场景中,例如热部署(hot deploy)一个正在运行的应用程序,这通常需要动态地加载和卸载类。在框架开发中,比如OSGI、JSP的servlet容器等,这样的需求也是很常见的。
五、直接内存
直接内存是操作系统中分配的一块内存,不受JVM管理,Java代码可以直接获取直接内存中的数据。
推荐参考:直接内存(Direct Memory)
六、JAVA中的四种引用
Java 提供了四种不同的引用类型:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。
推荐参考:Java 中的四种引用类型和它们的使用场景
七、JVM常用调优参数
JVM提供了一些常用调优参数:-Xms、-Xmx、-Xmn等
推荐参考:JVM常用调优参数
八、JVM调优
正常情况下,JVM 是不需要额外调优的。默认的配置通常适用于许多应用程序,因为 JVM 实现考虑了大量的使用情况,并经过了在不同场景下的测试和优化。
除非是系统有特殊的性能需求或者存在特定的瓶颈,一般来说,在生产环境中使用默认参数是合适的。然而,在一些特殊场景下,可以对 JVM 进行一些微调,以获取更好的性能或者更好的资源利用率。这通常需要仔细评估和测试,以确保调整后的参数能够有效地改善系统的性能。
1、JVM调优方法
JVM调优通常涉及到调整内存设置、选择合适的垃圾回收器以及优化JVM参数等方面。
- 堆内存设置:通过调整堆(heap)大小,你可以控制Java应用可用的内存数量。堆内存过小可能导致频繁的垃圾回收,降低应用性能;过大则可能导致垃圾回收停顿时间过长。比如:设置-Xms和-Xmx来定义堆的初始大小和最大大小。
- 选择垃圾回收器:根据应用的需求选择合适的垃圾回收器(GC)。不同的垃圾回收器,比如Parallel GC、CMS、G1 GC,有着不同的特点和适用场景。
2、OOM发生区域
JVM中出现OOM的区域通常有:
- 堆内存(Heap Memory):如果堆内存太小,或者应用程序中有内存泄漏,都可能导致堆内存OOM。
- 永久代/元空间(PermGen/Metaspace):存储Java类元数据的地方。如果加载了大量的类或者大量的动态生成类的情形,可能导致这部分内存溢出。
- 方法栈:比如方法递归调用,可能会导致这个区域内存溢出。
3、常见分析工具
- JConsole:Java监控和管理控制台,是Java Development Kit (JDK)的一部分,可以用来监控JAVA应用运行时的资源消耗。
- JVisualVM:集成了多个JDK命令行工具的可视化工具,提供了内存和CPU分析功能。
- Memory Analyzer Tool (MAT):用于分析堆转储,可以帮助你找出内存泄漏和查看内存消耗的对象。
- jmap:命令行工具,可以用来生成堆转储文件,分析内存使用情况。
4、模拟OOM和分析示例
下面是一个简单的Java代码片段,用于模拟堆内存溢出。
import java.util.ArrayList;
import java.util.List;public class GenerateOOM {static final int SIZE = 2 * 1024 * 1024;public static void main(String[] args) {List<Object> list = new ArrayList<>();while (true) {list.add(new Object[SIZE]);}}
}
运行这个程序,很快就会因为堆内存溢出而出现 OutOfMemoryError
。
首先使用 jps
查看进程ID,然后使用jmap来生成堆转储文件:
jmap -dump:live,format=b,file=heapdump.hprof <PID>
使用MAT打开堆转储文件(heapdump.hprof),MAT将会对文件进行分析,并提供内存使用的概览。
相关文章:

深入理解 Java 虚拟机(JVM)从入门到精通
目录 一、JVM内存结构1、堆(Heap)(1)特点(2)堆内存分配(3)晋升到老年代的方式(4)堆内存检验方式2、虚拟机栈(VM Stack)(1&…...

哔哩哔哩自动评论软件,其成果展示与开发流程和代码分享
先来看实操成果,↑↑需要的同学可看我名字↖↖↖↖↖,或评论888无偿分享 一、背景介绍 随着互联网的发展,哔哩哔哩作为国内最大的弹幕视频网站之一,吸引了越来越多的用户。为了更好地推广自己的作品,许多UP主希望能够通…...

Qt OpenCV 学习(一):环境搭建
对应版本 Qt 5.15.2OpenCV 3.4.9MinGW 8.1.0 32-bit 1. OpenCV 下载 确保安装 Qt 时勾选了 MinGW 编译器 本文使用 MinGW 编译好的 OpenCV 库,无需自行编译 确保下载的 MinGW 和上述安装 Qt 时勾选的 MinGW 编译器位数一致,此处均为 x86/32-bit下载地址…...

Redis——某马点评day02——商铺缓存
什么是缓存 添加Redis缓存 添加商铺缓存 Controller层中 /*** 根据id查询商铺信息* param id 商铺id* return 商铺详情数据*/GetMapping("/{id}")public Result queryShopById(PathVariable("id") Long id) {return shopService.queryById(id);} Service…...

prometheus|云原生|轻型日志收集系统loki+promtail的部署说明
一, 日志聚合的概念说明 日志------ 每一个程序,服务都应该有保留日志,日志的作用第一是记录程序运行的情况,在出错的时候能够记录错误情况,简单来说就是审计工作,例如nginx服务的日志,kuber…...

MySQL 临时数据空间不足导致SQL被killed 的问题与扩展
开头还是介绍一下群,如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, Oceanbase, Sql Server等有问题,有需求都可以加群群内,可以解决你的问题。加群请联系 liuaustin3 ,(共1730人左右 1 2 3 4 5࿰…...

文心一言大模型应用开发入门
本文重点介绍百度智能云平台、文心一言、千帆大模型平台的基本使用与接入流程及其详细步骤。 注册文心一言 请登录文心一言官方网站 https://yiyan.baidu.com/welcome 点击登录;图示如下: 请注册文心一言账号并点击登录,图示如下࿱…...

C++新经典模板与泛型编程:SFINAE替换失败并不是一个错误
替换失败并不是一个错误(SFINAE) SFINAE是一个英文简称,全称为Substitution Failure is not an Error,翻译成中文就是“替换失败并不是一个错误”。 SFINAE可以看作C语言的一种特性或模板设计中要遵循的一个重要原则,…...

基于若依的ruoyi-nbcio流程管理系统支持支持定时边界事件和定时捕获事件
更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio 演示地址:RuoYi-Nbcio后台管理系统 1、定时边界事件 <template><div class"panel-tab__content"><!--目前只处理定…...

递归-极其优雅的问题解决方法(Java)
递归的定义 大名鼎鼎的递归,相信你即使没接触过也或多或少听过,例如汉诺塔问题就是运用了递归的思想,对于一些学过c语言的同学来说,它可能就是噩梦,因为我当时就是这么认为的(不接受反驳doge) …...

VSCode搭建STM32开发环境
1、下载安装文件 链接:https://pan.baidu.com/s/1WnpDTgYBobiZaXh80pn5FQ 2、安装VSCodeUserSetup-x64-1.78.2.exe软件 3、 在VSCode中安装必要的插件 3、配置Keil Assistant插件 4、在环境变量中部署mingw64编译环境...

解决CentOS下PHP system命令unoconv转PDF提示“Unable to connect or start own listener“
centos系统下,用php的system命令unoconv把word转pdf时提示Unable to connect or start own listene的解决办法 unoconv -o /foo/bar/public_html/upload/ -f pdf /foo/bar/public_html/upload/test.docx 2>&1 上面这个命令在shell 终端能执行成功,…...

软件测试外包干了2个月,技术进步2年。。。
先说一下自己的情况,本科生,18年通过校招进入北京某软件公司,干了接近2年的功能测试,今年国庆,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了2年的功能测试&…...
Linux-网络服务和端口
域名:便于人们记忆和使用的标识符 www.baidu.com域名解析:将域名转换为与之对应的 IP 地址的过程 nameserver 8.8.8.8ip地址:网络设备的唯一数字标识符 域名ip地址localhost127.0.0.1 网络服务和端口 网络服务端口ftp21ssh22http80https…...
Kubernetes权威指南:从Docker到Kubernetes实践全接触(第5版)读书笔记 目录
完结状态:未完结 文章目录 前言第1章 Kubernetes入门 11.1 了解Kubernetes 2 附录A Kubernetes核心服务配置详解 915总结 前言 提示:这里可以添加本文要记录的大概内容: Kubernetes权威指南:从Docker到Kubernetes实践全接触&…...

阿里云Arthas使用——通过watch命令查看类的返回值 捞数据出来
前言 Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类…...
Redis中持久化策略RDB与AOF优缺点对比
Redis持久化策略对比 RDBAOF持久化方式定时对整个内存做快照记录每一次执行的命令数据完整性不完整,两次备份之间存在丢失相对完整,取决于刷盘策略文件大小会有压缩,文件体积小记录命令,文件体积较大宕机恢复速度很快慢数据恢复优先级低,数据完整性不如AOF高,记录了执行命令数据…...

通用plantuml 时序图(Sequence Diagram)模板头
通用plantuml文件 startuml participant Admin order 0 #87CEFA // 参与者、顺序、颜色 participant Student order 1 #87CEFA participant Teacher order 2 #87CEFA participant TestPlayer order 3 #87CEFA participant Class order 4 #87CEFA participant Subject order …...

Domino多Web站点托管
大家好,才是真的好。 看到一篇文档,大概讲述的是他在家里架了一台Domino服务器,上面跑了好几个Internet的Web网站(使用Internet站点)。再租了一台云服务器,上面安装Nginx做了反向代理,代理访问…...

防火墙补充NAT
目录 1.iptables保存规则 2.自定义链 3.NAT NAT的实现分为下面类型: SNAT实验操作 DNAT实验操作 1.iptables保存规则 永久保存方法一: iptables -save > /data/iptables_rule //输出重定向备份 iptables -restore < /data/iptables_r…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
Mobile ALOHA全身模仿学习
一、题目 Mobile ALOHA:通过低成本全身远程操作学习双手移动操作 传统模仿学习(Imitation Learning)缺点:聚焦与桌面操作,缺乏通用任务所需的移动性和灵活性 本论文优点:(1)在ALOHA…...

云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...
动态 Web 开发技术入门篇
一、HTTP 协议核心 1.1 HTTP 基础 协议全称 :HyperText Transfer Protocol(超文本传输协议) 默认端口 :HTTP 使用 80 端口,HTTPS 使用 443 端口。 请求方法 : GET :用于获取资源,…...