ParNew(平行的) 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过 -XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。
ParNew 虽然是除了多线程外和 Serial 收集器几乎完全一样,但是 ParNew 垃圾收集器是很多 Java 虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
9.3 Parallel Scavenge 收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)), 高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
9.4 Serial Old收集器(单线程标记整理算法)
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法, 这个收集器也主要是运行在 Client 默认的 Java 虚拟机默认的年老代垃圾收集器。
在 Server 模式下,主要有两个用途:
-
在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
-
作为年老代中使用 CMS 收集器的后备垃圾收集方案。
9.5 Parallel Old收集器(多线程标记整理算法)
Parallel Old 收集器是 Parallel Scavenge 的年老代版本,使用多线程的标记-整理算法,在 JDK1.6 才开始提供。
在 JDK1.6 之前,新生代使用 Parallel Scavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略。
9.6 CMS收集器(多线程标记清除算法)
Concurrent Mark Sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
9.6.1 初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
9.6.2 并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
9.6.3 重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记 记录,仍然需要暂停所有的工作线程。
9.6.4 并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看 CMS 收集器的内存回收和用户线程是一起并发地执行。
9.7 G1收集器
Garbage First 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是:
-
基于标记-整理算法,不产生内存碎片。
-
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
在 Java 中,程序员是不需要显式的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
11、如何判断一个对象是否存活?(或者 GC 对象的判定方法)
其实第 7 点回答了哈,这里再详细说一下。
判断一个对象是否存活有两种方法:
11.1 引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收。
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对 象 B,对象 B 又引用者对象 A,那么此时 A、B 对象的引用计数器都不为零, 也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
11.2 可达性算法(引用链法)
该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。
在 Java 中可以作为 GC Roots 的对象有以下几种:
-
虚拟机栈中引用的对象
-
方法区类静态属性引用的对象
-
方法区常量池引用的对象
-
本地方法栈 JNI 引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象不一定会被回收。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记。
如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法或者已被虚拟机调用过,那么就认为是没必要的。 如果该对象有必要执行 finalize() 方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize() 线程去执行,此线程是低优先级的, 并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize() 执行缓慢或者发生了死锁,那么就会造成 F-Queue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除 ”即将回收” 集合,等待回收。
Java 语言中一个显著的特点就是引入了垃圾回收机制,使 C++ 程序员最头疼的内存管理的问题迎刃而解,它使得 Java 程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java 中的对象不再有“作用域”的概念,只有对象的引用才有"作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
13、垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当 GC 确定一些对象为“不可达”时,GC 就有责任回收这些内存空间。
可以。程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行。
所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java 中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象变成了孤儿的时候,对象将自动被垃圾回收器从内存中清除掉。由于 Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达的,那么 GC 也是可以回收它们的,例如下面的代码可以看到这种情况的内存回收:
public class GarbageTest {
public static void main(String[] args) throws IOException {
try {
gcTest();
} catch (IOException e) {
}
System.out.println(“has exited gcTest!”);
System.in.read();
System.in.read();
System.out.println(“out begin gc!”);
for (int i = 0; i < 100; i++) {
System.gc();
System.in.read();
System.in.read();
}
}
private static void gcTest() throws IOException {
System.in.read();
System.in.read();
Person p1 = new Person();
System.in.read();
System.in.read();
Person p2 = new Person();
p1.setMate(p2);
p2.setMate(p1);
System.out.println(“before exit gctest!”);
System.in.read();
System.in.read();
System.gc();
System.out.println(“exit gctest!”);
}
private static class Person {
byte[] data = new byte[20000000];
Person mate = null;
public void setMate(Person other) {
mate = other;
}
}
}
Java 中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是 Java 中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是 Java 中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中 (例如放在一个全局 map 对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
检查 Java 中的内存泄露,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄露。
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
我们来看个堆栈经典的例子,主要特点就是清空堆栈中的某个元素,并不是彻底把它从数组中拿掉,而是把存储的总数减少。
public class Stack {
private Object[] elements = new Object[10];
private int size = 0;
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[–size];
}
private void ensureCapacity() {
if (elements.length == size) {
Object[] oldElements = elements;
elements = new Object[(2 * elements.length) + 1];
System.arraycopy(oldElements, 0, elements, 0, size);
}
}
}
上面的原理应该很简单,假如堆栈加了 10 个元素,然后全部弹出来,虽然堆栈是空的,没有我们要的东西,但是这是个对象是无法回收的,这个才符合了内存泄露的两个条件:无用,无法回收。但是就是存在这样的东西也不一定会导致什么样的后果,如果这个堆栈用的比较少,也就浪费了几个 K 内存而已,反正我们的内存都上 G 了,哪里会有什么影响,再说这个东西很快就会被回收的,有什么关系。下面再看个例子。
public class Bad {
public static Stack s = Stack();
static {
s.push(new Object());
s.pop(); //这里有一个对象发生内存泄露
s.push(new Object()); //上面的对象可以被回收了,等于是自愈了
}
}
因为是 static,就一直存在到程序退出,但是我们也可以看到它有自愈功能,就是说如果你的 Stack 最多有 100 个对象,那么最多也就只有 100 个对象无法被回收,其实这个应该很容易理解,Stack 内部持有 100 个引用,最坏的情况就是他们都是无用的,因为我们一旦放新的进去,以前的引用自然消失!
内存泄露的另外一种情况:当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对 象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄露。
15、简述 Java 内存分配与回收策略以及 Minor GC 和 Major GC。
-
对象优先在堆的 Eden 区分配
-
大对象直接进入老年代
-
长期存活的对象将直接进入老年代
当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC。Minor GC 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 GC 的 频率较高,回收速度比较快;Full GC/Major GC 发生在老年代,一般情况下, 触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。
16、JVM 内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为 Eden 和 Survivor。
第一个问题我觉得是通过分新生代,老年代,持久代而更好的利用有限的内存空间。
第二个问题:
-
如果没有 Survivor,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发 Major GC。老年代的内存空间远大于新生代,进行一次 Full GC 消耗的时间比 Minor GC 长得多,所以需要分为 Eden 和 Survivor。
-
Survivor 的存在意义,就是减少被送到老年代的对象,进而减少 Full GC 的发生,Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
-
设置两个 Survivor 区最大的好处就是解决了碎片化,刚刚新建的对象在 Eden 中,经历一次 Minor GC,Eden 中的存活对象就会被移动到第一块 survivor space S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0 中的存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为这种复制算法保证了 S1 中来自 S0 和 Eden 两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。
Minor GC 触发条件:当 Eden 区满时,触发 Minor GC。
Full GC 触发条件:
-
调用 System.gc 时,系统建议执行 Full GC,但是不必然执行
-
老年代空间不足
-
方法区空间不足
-
通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
-
由 Eden区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
-
首先控制台查看错误日志
-
然后使用 jdk 自带的 jvisualvm工具查看系统的堆栈日志
-
定位出内存溢出的空间:堆,栈还是永久代(jdk8 以后不会出现永久代的内存溢出)。
-
如果是堆内存溢出,看是否创建了超大的对象
-
如果是栈内存溢出,看是否创建了超大的对象,或者产生了死循环。
这里老周给我们服务的 JVM 参数给大家参考下哈,按照自己线上应用来答就好了。
-
-server
-
-Xms4096M
-
Xmx4096M
-
-Xmn1536M
-
-XX:MetaspaceSize=256M
-
-XX:MaxMetaspaceSize=256M
-
-XX:+UseParNewGC
-
-XX:+UseConcMarkSweepGC
-
-XX:+CMSScavengeBeforeRemark
-
-XX:CMSInitiatingOccupancyFraction=75
-
-XX:CMSInitiatingOccupancyOnly
20.1 内存泄漏 memory leak
是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
20.2 内存溢出 out of memory
指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用,此时就会报错 OOM,即所谓的内存溢出。
20.3 二者的关系
-
内存泄漏的堆积最终会导致内存溢出
-
内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
-
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
-
内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错,
20.4 内存泄漏的分类(按发生方式来分类)
-
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
-
偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
-
一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
-
隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
20.5 内存溢出的原因及解决方法
20.5.1 内存溢出原因
-
内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
-
集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收;
-
代码中存在死循环或循环产生过多重复的对象实体;
-
使用的第三方软件中的 BUG;
-
启动参数内存值设定的过小。
20.5.2 内存溢出的解决方案
-
修改 JVM 启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
-
检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
-
对代码进行走查和分析,找出可能发生内存溢出的位置。
20.5.3 重点排查以下几点
-
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
-
检查代码中是否有死循环或递归调用。
-
检查是否有大循环重复产生新对象实体。
-
检查 List、Map 等集合对象是否有使用完后,未清除的问题。List、Map 等集合对象会始终存有对对象的引用,使得这些对象不能被 GC 回收。
我们可以从以下几个方面来进行排查
21.1 碎片化
对于 CMS,由于老年代的碎片化问题,在 YGC 时可能碰到晋升失败(promotion failures,即使老年代还有足够多有效的空间,但是仍然可能导致分配失败,因为没有足够连续的空间),从而触发Concurrent Mode Failure,发生会完全 STW 的 Full GC。Full GC 相比 CMS 这种并发模式的 GC 需要更长的停顿时间才能完成垃圾回收工作,这绝对是 Java 应用最大的灾难之一。
为什么 CMS 场景下会有碎片化问题?由于 CMS 在老年代回收时,采用的是标记清理(Mark-Sweep)算法,它在垃圾回收时并不会压缩堆,日积月累,导致老年代的碎片化问题会越来越严重,直到发生单线程的 Mark-Sweep-Compact GC,即FullGC,会完全 STW。如果堆比较大的话,STW 的时间可能需要好几秒,甚至十多秒,几十秒都有可能。
21.2 GC 时操作系统的活动
当发生 GC 时,一些操作系统的活动,比如 swap,可能导致 GC 停顿时间更长,这些停顿可能是几秒,甚至几十秒级别。
如果你的系统配置了允许使用 swap 空间,操作系统可能把 JVM 进程的非活动内存页移到 swap 空间,从而释放内存给当前活动进程(可能是操作系统上其他进程,取决于系统调度)。Swapping 由于需要访问磁盘,所以相比物理内存,它的速度慢的令人发指。所以,如果在 GC 的时候,系统正好需要执行 Swapping,那么 GC 停顿的时间一定会非常非常恐怖。
除了swapping 以外,我们也需要监控了解长 GC 暂停时的任何 IO 或者网络活动情况等, 可以通过 iostat 和 netstat 两个工具来实现。我们还能通过 mpstat 查看 CPU 统计信息,从而弄清楚在 GC 的时候是否有足够的 CPU 资源。
21.3 堆空间不够
如果应用程序需要的内存比我们执行的 Xmx 还要大,也会导致频繁的垃圾回收,甚至 OOM。由于堆空间不足,对象分配失败,JVM 就需要调用 GC 尝试回收已经分配的空间,但是 GC 并不能释放更多的空间,从而又回导致 GC,进入恶性循环。
同样的,如果在老年代的空间不够的话,也会导致频繁 Full GC,这类问题比较好办,给足老年代和永久代。
21.4 JVM Bug
什么软件都有 BUG,JVM 也不例外。有时候,GC 的长时间停顿就有可能是 BUG 引起的。例如,下面列举的这些 JVM 的 BUG,就可能导致 Java 应用在 GC 时长时间停顿。
6459113: CMS+ParNew: wildly different ParNew pause times depending on heap shape caused by allocation spread
fixed in JDK 6u1 and 7
6572569: CMS: consistently skewed work distribution indicated in (long) re-mark pauses
fixed in JDK 6u4 and 7
6631166: CMS: better heuristics when combatting fragmentation
fixed in JDK 6u21 and 7
6999988: CMS: Increased fragmentation leading to promotion failure after CR#6631166 got implemented
fixed in JDK 6u25 and 7
6683623: G1: use logarithmic BOT code such as used by other collectors
fixed in JDK 6u14 and 7
6976350: G1: deal with fragmentation while copying objects during GC
fixed in JDK 8
如果你的 JDK 正好是上面这些版本,强烈建议升级到更新 BUG 已经修复的版本。
21.5 显式 System.gc 调用
检查是否有显示的 System.gc 调用,应用中的一些类里,或者第三方模块中调用 System.gc 调用从而触发 STW 的 Full GC,也可能会引起非常长时间的停顿。如下 GC 日志所示,Full GC 后面的(System)表示它是由调用 System.GC 触发的 FullGC,并且耗时 5.75 秒:
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
cts during GC
fixed in JDK 8
如果你的 JDK 正好是上面这些版本,强烈建议升级到更新 BUG 已经修复的版本。
21.5 显式 System.gc 调用
检查是否有显示的 System.gc 调用,应用中的一些类里,或者第三方模块中调用 System.gc 调用从而触发 STW 的 Full GC,也可能会引起非常长时间的停顿。如下 GC 日志所示,Full GC 后面的(System)表示它是由调用 System.GC 触发的 FullGC,并且耗时 5.75 秒:
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-0uPNKeuy-1710778260180)]
[外链图片转存中…(img-sWzVLRox-1710778260180)]
[外链图片转存中…(img-7hByXwLh-1710778260181)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-zQKgNhXF-1710778260181)]