更多相关内容可查看
内存泄露
原因:对象引用未清理,导致垃圾回收器无法回收,也就是大家常说的OOM。
示例:
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakExample {
private List<String> list = new ArrayList<>();
public void addString(String value) {
list.add(value); // 持续添加字符串,导致内存无法释放
}
public static void main(String[] args) {
MemoryLeakExample example = new MemoryLeakExample();
while (true) {
example.addString("Leak " + System.nanoTime());
}
}
}
-Xmx:是JVM的一个参数,用于设置Java程序的最大堆内存大小。例如 -Xmx512m 表示最大堆内存为512MB。合理配置这个参数有助于避免OOM(OutOfMemoryError)。详细的JVM调优可查看JVM调优原理、思路、真正意义上解决性能瓶颈(附实际调优案例)
栈内存溢出
原因:递归调用未设定终止条件。
示例:
public class StackOverflowExample {
public static void recursiveMethod() {
recursiveMethod(); // 无限递归调用
}
public static void main(String[] args) {
try {
recursiveMethod();
} catch (StackOverflowError e) {
System.out.println("Stack overflow occurred!");
}
}
}
无限递归导致栈空间耗尽。是因为递归反复调用方法,调用方法的时候会创建栈帧,每个栈帧存储该方法的局部变量和调用信息。当递归没有终止条件时,这些栈帧不断积累,最终导致栈空间耗尽,从而引发栈内存溢出。
-Xss :是JVM的一个参数,用于设置每个线程的栈大小。例如,-Xss512k表示每个线程的栈大小为512KB,所以当栈内存溢出的时候,可以通过调节Xss参数来解决
强引用、软引用、弱引用、虚引用的区别?
强引用:最常见的引用类型,如 Object obj = new Object();
。只要强引用存在,垃圾回收器不会回收被引用的对象。
软引用:可以用来描述一些还有用但不一定需要的对象。在内存不足时,垃圾回收器会回收软引用指向的对象,适用于缓存场景。
Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
}else{
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}
弱引用:比软引用更弱,当垃圾回收器工作时,如果一个对象只被弱引用引用,就会被回收。适合用于不需要强保持的对象。
虚引用:几乎没有使用场景,主要用于跟踪对象的生命周期。当一个对象只被虚引用引用时,它仍然可以被回收。虚引用与引用队列结合使用,允许在对象被回收时收到通知。
判断对象是否可以被回收
- 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0
时就可以被回收。它有一个缺点不能解决循环引用的问题; - 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots
没有任何引用链相连时,则证明此对象是可以被回收的
JVM 运行时堆内存分代方式
Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年代。
- 新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数
–XX:NewRatio
来指定 ) - Eden: from : to = 8 :1 : 1 ( 可以通过参数
–XX:SurvivorRatio
来设定 )
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。
新生代
- Eden 区 :Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
- Servivor from 区: 上一次 GC 的幸存者,作为这一次 GC 的被扫描者,扫描完还存活的就放到Servivor to 区中了。
- Servivor to 区: 保留了Servivor from后 MinorGC 过程中的幸存者
MinorGC 原理:
- 把 Eden 和 ServivorFrom区域中存活的对象复制到 ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo不够位置了就放到老年区);
- 清空 Eden 和 ServicorFrom 中的对象;
- ServicorTo 和 ServicorFrom 互换,原ServicorTo 成为下一次 GC 时的 ServicorFrom区。
老年代
-
对象存活时间:老年代主要存放那些在多次垃圾回收(GC)中仍然存活的对象。通常,经过几轮的Minor GC后,仍然存活的对象会被转移到老年代。
-
内存管理:老年代的垃圾回收通常较少且耗时,因为老年代的对象通常较大,且需要全堆扫描。相较于新生代,老年代的垃圾回收更为复杂,常见的方式有Full GC。
-
GC策略:老年代的垃圾回收策略与新生代不同。老年代使用的GC策略通常是标记-清除或标记-整理,而新生代多使用复制算法。
-
堆内存分配:在堆内存中,老年代的空间通常比新生代更大,适合存放长期存在的对象。通过分代收集,JVM能更高效地管理内存,减少频繁的内存回收。
永久代
- 永久代是方法区的一部分,但它的大小通常是固定的,并且内存不够时可能引发 OutOfMemoryError。
- 垃圾回收主要发生在Full GC期间,因此永久代的GC频率相对较低。
Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区
JVM中一次完整的GC流程
- Java堆 = 老年代 + 新生代
- 新生代 = Eden + S0 + S1
- 当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到
Survivor区。 - 大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态;
- 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor
GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。 - 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
- Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。
垃圾回收算法
垃圾回收(Garbage Collection,GC)是自动内存管理的一部分,旨在释放不再被使用的内存。不同的垃圾回收算法有不同的实现和用途
1. 标记-清除算法(Mark-and-Sweep)
原理:
- 标记阶段:从根对象开始,遍历所有可达的对象,并将其标记为"活着"。
- 清除阶段:遍历整个堆,清除未被标记的对象,释放其占用的内存。
优点:
- 简单易实现,能够有效回收不再使用的对象。
缺点:
- 可能导致内存碎片。
- 清除过程可能导致应用程序暂停(停顿时间长)。
2. 标记-整理算法(Mark-and-Compact)
原理:
- 标记阶段:同标记-清除,标记所有可达对象。
- 整理阶段:将所有标记的对象移动到堆的一端,释放出一块连续的内存空间。
优点:
- 解决了内存碎片的问题,得到了连续的内存区域。
缺点:
- 实现复杂,相比标记-清除算法,需要更多的内存复制和调整指针。
3. 复制算法(Copying)
原理:
- 将堆分为两个区域(from空间和to空间),只使用其中一个空间进行分配。
- 当其中一个空间用满时,复制所有活着的对象到另一个空间,并清空当前使用的空间。
优点:
- 内存分配简单,无需碎片整理,所有对象都在连续空间。
- 每次回收后都可一次性释放整块空内存。
缺点:
- 需要额外的内存空间(至少一倍于活对象的大小)。
- 只适合新生代。
4. 分代收集算法
原理:
- 将堆分为新生代和老年代,采用不同的回收策略。
- 新生代采用复制算法,老年代采用标记-清除或标记-整理算法。
优点:
- 考虑到对象的生命周期,通过分代收集提升了回收效率。
- 可以快速回收新生代中的短命对象,减少停顿时间。
缺点:
- 设计和实现较为复杂,对老年代的配置和回收策略需要合理调优。
5. 清除-修改算法
原理:
- 通过维护一个记录对象引用的表,直接标记和清理不再使用的对象。
- 可通过引用计数技术实现,对象引用计数为零时自动释放。
优点:
- 允许对象之间的引用关系保持实时更新。
缺点:
- 不适合循环引用的情况,可能导致内存泄漏。
- 维护引用计数的开销较大。
6. 增量垃圾回收
原理:
- 在程序的执行过程中,将垃圾回收操作与应用程序的执行交替进行,分段进行。
优点:
- 减少了程序的停顿时间,保持了系统的响应性。
缺点:
- 实现复杂,可能需要在多个线程间进行协调。
7. 并发垃圾回收
原理:
- 在应用程序运行的同时进行垃圾回收,使用多个线程并行执行,以提高回收效率。
优点:
- 改善了停顿时间,适用于需要高响应性的大型应用。
缺点:
- 实现复杂,可能对应用程序的性能造成影响,且需要处理多线程中的数据一致性。
jvm中的垃圾回收器的种类
1. 串行垃圾回收器(Serial Garbage Collector)
原理:
- 使用单线程进行所有的垃圾回收操作。
- 在垃圾回收期间,所有应用线程都会暂停。
算法:
- 采用标记-清除(Mark-and-Sweep)和复制(Copying)算法。
适用场景:
- 小型应用或单线程环境。
- 堆内存较小(例如小于100MB)。
换用情境:
- 当CPU资源有限或程序对响应时间和性能要求不高时,可以选择该垃圾回收器。
2. 并行垃圾回收器(Parallel Garbage Collector)
原理:
- 采用多线程并行处理的方式来执行垃圾回收,适当利用多核处理器。
- 在回收过程中,所有用户线程都会被暂停。
算法:
- 使用标记-清除和复制算法,结合并行执行。
适用场景:
- 需要高吞吐量的大型应用程序。
- 合适在处理大数据量的场景中。
换用情境:
- 若应用对吞吐量敏感且可以接受较长的停顿时间,适合使用并行垃圾回收器。
3. CMS(Concurrent Mark-Sweep)垃圾回收器
原理:
- 将垃圾回收过程分为多个阶段,包括初始标记(单线程)、并行标记、预清理、重新标记和清理。
- 大部分的回收工作是在应用线程同时运行时进行,减少了停顿时间。
算法:
- 基于标记-清除算法,采用并发标记和清扫。
适用场景:
- 对延迟要求较高的应用,如Web应用、金融应用等。
- 对停顿时间有较高要求且运行在多核处理器上。
换用情境:
- 当出现较大的停顿时间时,可以考虑切换到CMS,以降低停顿时间。
4. G1(Garbage First)垃圾回收器
原理:
G1首先进行初始标记,标记所有活跃的根对象,在标记完成后,G1会识别哪些区域包含大量的垃圾对象,优先收集这些区域。这是“垃圾优先”的核心思想,G1采用复制算法,存活的对象会从回收区域复制到其他区域,减少内存碎片
算法:
- 结合了标记-整理和复制算法。
适用场景:
- 大规模应用和需要可预测停顿时间的应用。
- 堆大小较大,且需要动态调整内存利用率的情况。
换用情境:
- 如果在使用CMS时发现内存碎片问题,可以考虑换成G1以更好地管理内存。
5. ZGC(Z Garbage Collector)
原理:
- 采用分代收集和并发标记,在处理大堆时能够实现低延迟。
- 整个回收过程尽可能使应用线程保持运行状态。
算法:
- 采用并发标记集(Concurrent Marking Set)和并发清理算法。
适用场景:
- 大内存环境下,需要极低或可控的停顿时间的应用,如云计算和实时服务。
换用情境:
- 如果应用对低延迟要求极高,而传统的垃圾回收器无法满足需求时,可以考虑使用ZGC。
6. Shenandoah垃圾回收器
原理:
- 采用并发标记和并发整理,旨在实现在大堆内存中快速回收。
算法:
- 使用分区标记和并发清理算法。
适用场景:
- 需要低停顿时间的应用,特别是在处理大量内存和高吞吐量的情况下。
换用情境:
- 如果需要实时响应并且其他GC策略无法提供足够低的停顿时间,可以选择Shenandoah。
上述这些是广泛使用或更新的垃圾回收器
新生代垃圾回收器
- ParNew(Serial+多线程):ParNew是一个并行的年轻代垃圾回收器,它与其他的并行回收器(如CMS)配合使用。写入数据时,会使用多线程来加速垃圾回收过程。
- Serial:这是一个单线程的Young GC,适用于小型应用和资源受限的环境。
- Parallel:也称为Parallel Scavenge,它是一个多线程的Young GC,设计目标是优化吞吐量。
老年代垃圾回收器
- Concurrent Mark-Sweep(CMS):CMS是一个以速度为主的低停滞时间垃圾回收器,它并行地进行标记和清除操作。适用于需要较快响应时间的应用,比如Web应用。
- G1 (Garbage-First) GC:G1GC是现代的垃圾回收器,旨在实现可预测的停顿时间和高吞吐量。G1将堆划分为多个小的区域(Region),同时进行标记和回收,通过预测最优的回收区域来提高效率。
- Serial Old:这是Serial GC的老年代实现,适用于小型应用或资源有限的环境。
双亲委派模型
原理:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
为啥要有这个东西:在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码