二、垃圾回收(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类模型)
-
创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
验证(链接)
-
(1)文件格式验证
-
(2)元数据验证
-
(3)字节码验证(1.2.3)格式检查,如:文件格式是否错误、语法是否错误、字节码是否合规
-
(4)符号引用验证 Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,检查它们是否存在
准备(链接)
为类变量分配内存并设置类变量初始值
- static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
- static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成
- static变量是final的引用类型,那么赋值也会在初始化阶段完成
解析(链接)
把类中的符号引用转换为直接引用
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
初始化
对类的静态变量,静态代码块执行初始化操作
- 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
使用
JVM 开始从入口方法开始执行用户的程序代码
- 调用静态类成员信息(比如:静态字段、静态方法)
- 使用new关键字为其创建对象实例
卸载
四、双亲委派机制
双亲委派是 Java 类加载器的一种机制。当一个类加载器收到加载类的请求时,它会首先将这个请求委派给父类加载器去完成。只有当父类加载器无法完成这个加载请求时,子类加载器才会尝试加载。
1、双亲委派机制特点
- 避免重复加载:由于双亲委派机制,如果一个类已经被某个类加载器加载过了,那么其他的类加载器就没有必要再加载一次,可以直接复用已经加载的类。这样可以避免类的重复加载,节省内存。
- 安全性:通过双亲委派机制,核心类库会被由启动类加载器加载,因此可以防止核心类库被恶意篡改。另外,由于类加载器可以通过双亲委派机制追溯到启动类加载器,所以可以确保核心类库不会被自定义的类所替代,从而保证了系统安全性。
2、如何打破双亲委派机制
要打破双亲委派机制,可以自定义类加载器,并重写 ClassLoader 类中的 loadClass(String name, boolean resolve) 方法(或者是 findClass(String name) 方法,根据具体需求)。自定义的类加载器可以先尝试加载类,而不是直接委派给父加载器。
下面是一个简化的示例,说明如何自定义类加载器以打破双亲委派模型:
public class CustomClassLoader extends ClassLoader {
@Override
public 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;
}
@Override
protected 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)。
七、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和分析示例
推荐参考: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,或者top查看内存较高进程ID,然后使用jmap来生成堆转储文件:
jmap -dump:live,format=b,file=heapdump.dat<PID>
1
使用MAT打开堆转储文件(heapdump.dat),MAT将会对文件进行分析,并提供内存使用的概览。
5、cpu飙高,用什么方式快速排查?
推荐参考:[linux系统cpu飙高如何排查]
- 使用
top
命令查看占用过高的进程ID(pid) - 使用
top -H -p <进程ID>
查看这个进程里面哪个线程导致的 - 使用
printf "%x\n" [tid]
将十进制的线程ID转换为十六进制的线程ID - 使用
jstack <进程ID> | grep <16进制线程ID> -A 20
命令打印线程日志
评论( 0 )