先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引⽤(reference类型,它不同于对象本身,可能是⼀个指向对象起始地址的引⽤指针,也可能是指向⼀个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间是以 局部变量槽 来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。
局部变量表所需的内存空间在 编译期 间完成分配。当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError:若Java虚拟机栈的内存⼤⼩不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最⼤深度的时候,就抛出StackOverFlowError异常。(典型的场景有:递归调用和死循环)
OutOfMemoryError:若 Java 虚拟机栈的内存⼤⼩允许动态扩展,且当线程请求栈时内存⽤完了,⽆法再动态扩展了,此时抛出OutOfMemoryError异常。
在栈上分配对象
大多数对象都在堆上分配内存空间,但是由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配,标量替换优化手段已经导致对象在堆上分配不是那么绝对了。
③本地方法栈:线程私有
本地方法栈和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。本地⽅法被执⾏的时候,在本地⽅法栈也会创建⼀个栈帧,⽤于存放该本地⽅法的局部变量表、操作数栈、动态链接、出⼝信息。
⽅法执⾏完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和OutOfMemoryError 两种异常。
④堆:线程共享
Java 虚拟机所管理的内存中最⼤的⼀块,Java 堆是所有线程共享的⼀块内存区域,在虚拟机启动时创建。
此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存。
堆分区
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。
从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以Java堆还可以细分为:新⽣代和⽼年代。
新生代再细致⼀点有:Eden空间、From Survivor、To Survivor空间等。
进⼀步划分的⽬的是更好地回收内存,或者更快地分配内存。
上图所示的 eden区、s0区、s1区都属于新⽣代,tentired 区属于⽼年代。⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden区i>Survivor 区后对象的初始年龄变为1),当它的年龄增加到⼀定程度(默认为15岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
⑤方法区:线程共享
它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
⽅法区也被称为永久代。很多⼈都会分不清⽅法区和永久代的关系,为此我也查阅了⽂献。
永久代是对方法区的一种实现方式。方法区是一种定义,永久代是一种实现。
运⾏时常量池
运⾏时常量池是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息外,还有常量池信息(⽤于存放编译期⽣成的各种字⾯量和符号引⽤)
既然运⾏时常量池时⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运⾏时常量池从⽅法区中移了出来,在 Java 堆(Heap)中开辟了⼀块区域存放运⾏时常量池。
直接内存
直接内存并不是虚拟机运⾏时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使⽤。⽽且也可能导致 OutOfMemoryError 异常出现。
2. JDK1.7和1.8 内存模型的区别
JDK1.7JVM内存模型:
线程私有:Java虚拟机栈、本地方法栈、程序计数器
线程共享:方法区、堆
JDK1.8JVM内存模型:
JDK1.8与1.7最大的区别是1.8将永久代(方法区)取消,取而代之的是元空间。
JDK1.7方法区是由永久代实现的,JDK1.8方法区是由元空间实现的,元空间属于本地内存,所以元空间的大小受本地内存的限制。
JDK1.7 JVM内存模型
JDK1.8 JVM内存模型
3. Java内存模型(JMM)
Java内存模型规定所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本的拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
主内存:主要对应Java堆中的对象实例数据部分。
工作内存:对应于虚拟机栈中的部分区域。
4. Java对象的创建过程
① 类加载检查 : 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
② 分配内存 : 在 类加载检查 通过后,接下来虚拟机将为新⽣对象 分配内存 。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆 中划分出来。 分配⽅式 有 “ 指针碰撞 ” 和 “ 空闲列表 ” 两种, 选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
③ 初始化零值 :内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
④ 设置对象头 : 初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
⑤ 执⾏ init ⽅法 : 在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。
[注意]:执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
5. 堆内存中对象分配的基本策略
①对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
②大对象直接进入老年代
所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。可通过配置设置令操作设置值的对象直接在老年代分配。
这样做的目的避免在Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)。
③长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的华,将被移动到Survivor空间中,并且对象的年龄设置为 1.对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)就将会被晋升到老年代中。年龄阈值可通过配置调整-XX:MaxTenuringThreshold参数。
④动态年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold参数才能晋升到老年代。
如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到配置参数中要求的年龄。
⑤空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。
如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的。如果小于或者设置不允许冒险,这时要改为进行一次Full GC。
6. 如何判断对象是否死亡
堆中⼏乎放着所有的对象实例,对堆垃圾回收前的第⼀步就是要判断哪些对象已经死亡(即不能再被任何途径使⽤的对象)。
①引用计数法
给对象中添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它,计数器就加1;当引⽤失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使⽤的。
(但是,引用计数法很难解决对象之间相互循环引用的问题)
②可达性分析算法
通过一些列的称为GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots之间没有任何引用链相连,则证明这个对象是不可用的。
7. 判断不可达一定会被回收吗
即使在可达性分析算法判断不可达的对象,也并非是“非死不可的”。
如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被调用过,finalize()方法都不会执行,该对象将会被回收。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列中,然后由Finalizer线程去执行。GC将会对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联,那么在第二次标记时将会被移除“即将回收”的集合,否则该对象将会被回收。
(比如:把自己(this关键字)赋值给某个类变量(static修饰)或者对象的成员变量)
8. GC Roots 有哪些
1.在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2.在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
3.在方法区中常量引用的对象,譬如字符串常量池里的引用。
4.在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
6.所有被同步锁(synchronized关键字)持有的对象。
7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
9. 强引用、软引用、弱引用、虚引用
⽆论是通过引⽤计数法判断对象引⽤数量,还是通过可达性分析法判断对象的引⽤链是否可达,判定对象的存活都与“引⽤”有关。
JDK1.2之前,Java中引⽤的定义很传统:如果reference类型的数据存储的数值代表的是另⼀块内存的起始地址,就称这块内存代表⼀个引⽤。
JDK1.2以后,Java对引⽤的概念进⾏了扩充,将引⽤分为强引⽤、软引⽤、弱引⽤、虚引⽤四种(引⽤强度逐渐减弱)
强引⽤(StrongReference)
强引用就是指类似于“Object obj = new Object()”这类的引用。被强引用关联的对象,垃圾回收器绝不会回收它。当内存空间不⾜时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终⽌,也不会靠随意回收具有强引⽤的对象来解决内存不⾜问题。
软引⽤(SoftReference)
被软引用关联的对象只有在内存空间不足时才会被回收。软引⽤可⽤来实现内存敏感的⾼速缓存。软引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果软引⽤所引⽤的对象被垃圾回收,JAVA虚拟机就会把这个软引⽤加⼊到与之关联的引⽤队列中。在JDK1.2后,提供了SoftReference
弱引⽤(WeakReference)
弱引⽤与软引⽤的区别在于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引⽤的对象。弱引⽤可以和⼀个引⽤队(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃圾回收,Java虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中。
虚引⽤( PhantomReference)
"虚引⽤"顾名思义,就是形同虚设,与其他⼏种引⽤都不同,虚引⽤并不会决定对象的⽣命周期。 如果⼀个对象仅持有虚引⽤,那么它就和没有任何引⽤⼀样,在任何时候都可能被回收 。 为一个对象设置虚引用的唯一目的就是这个对象被垃圾回收器回收时收到一个系统通知 。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
虚引⽤与软引⽤和弱引⽤的⼀个区别在于: 虚引⽤必须和引⽤队列(ReferenceQueue)联合使⽤ 。当垃圾回收器准备回收⼀个对象时,如果发现它还有虚引⽤,就会 在回收对象的内存之前 ,把这个虚引⽤加⼊到与之关联的引⽤队列中。程序可以通过判断引⽤队列中是 否已经加⼊了虚引⽤,来了解被引⽤的对象是否将要被垃圾回收。程序如果发现某个虚引⽤已经被加⼊到引⽤队列,那么就可以在所引⽤的对象的内存被回收之前采取必要的⾏动。
(软引用是在 垃圾回收之后 把引用加入到引用队列中,虚引用是在 垃圾回收之前 把引用加入到引用队列中)
特别注意,在程序设计中⼀般很少使⽤弱引⽤与虚引⽤,使⽤软引⽤的情况较多,这是因为软引⽤可以加速JVM对垃圾内存的回收速度,可以维护系统的运⾏安全,防⽌内存溢出(OutOfMemory)等问题的产⽣。
10. 垃圾收集算法
①标记-清除算法
算法分为“标记”和“清除”阶段:⾸先标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不⾜进⾏改进得到。这种垃圾收集算***带来两个明显的问题:
1. 效率问题,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量的增长而降低。
2. 空间问题(标记清除后会产⽣⼤量不连续的碎⽚,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作)
②标记-复制算法
标记-复制算法简称“复制算法”
为了解决效率问题,“复制”收集算法出现了。
它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。
优点:1.解决了标记-清除算法面对大量可回收对象时执行效率低的问题。2.避免了内存碎片。
缺点:将可用空间缩小为原来的一半,空间利用率较低。
新生代中大部分对象“朝生夕灭”,熬不过第一轮收集。因此并不需要按照1:1的比例来划分新生代内存空间。
将新生代划分为一个Eden区和两个Survivor区,大小为8:1:1
每次分配内存时只使用Eden区和其中一个Survivor区。发生垃圾回收时,将Eden和Survior中仍然中存活的对象一次性复制到另外一块Survivor空间中,然后直接清理掉Eden和已经使用过的Survivor区。
③标记-整理算法
根据⽼年代的特点推出的⼀种标记算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。
(复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法)
优点:不会产生内存碎片。
缺点:移动存活对象并更新引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。(即Stop the world)
④分代收集算法
当前虚拟机的垃圾收集都采⽤分代收集算法,根据对象存活周期的不同将内存分为⼏块,⼀般将java堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
在 新⽣代 中,每次收集都会有⼤量对象死去,所以可以选择标记-复制算法,只需要付出少量存活对象的复制成本就可以完成每次垃圾收集。
⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择 “ 标记 - 清除 ” 或 “ 标记 - 整理 ” 算法进⾏垃圾收集。
11.垃圾收集器
垃圾收集器主要有:Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS、G1
①Serial收集器
Serial收集器一个新生代、单线程的收集器,采用标记-复制算法。它在进行垃圾回收时,必须暂停所有用户线程,直到它收集结束。(stop the world)
②ParNew收集器
ParNew收集器一个新生代、多线程的收集器,采用标记-复制算法。只有Serial和ParNew收集器能与CMS配合工作。ParNew收集器是激活CMS后的默认新生代收集器。
③Parallel Scavenge收集器
Parallel Scavenge收集器是一款新生代收集器,基于标记-复制算法实现的。能够进行并行收集的多线程收集器。
④Serial Old收集器
Serial Old收集器:一个老年代、单线程的收集器,采用标记-整理算法。它在进行垃圾回收时,必须暂停所有用户线程,直到它收集结束。(stop the world)
⑤Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集(多个GC线程),基于标记-整理算法实现。
⑥CMS收集器
CMS收集器是老年代收集器,基于标记-清除算法实现的。
⑦G1收集器
G1收集器:新生代+老年代(将Java堆划分成多个Region),面向全堆的收集器,不需要其他新生代收集器的配合工作。
搭配:
①新生代Serial+老年代Serial Old
②新生代ParNew+老年代Serial Old
③新生代Parallel Scavenge+老年代Parallel Old
④新生代ParNew+老年代CMS(Parallel Scavenge无法与CMS配合)
12. CMS收集器
CMS( Concurrent Mark Sweep )收集器是⼀种以获取 最短回收停顿时间 为⽬标的收集器。它⽽⾮常符合在注重⽤户体验的应⽤上使⽤。
CMS( Concurrent Mark Sweep )收集器是 HotSpot 虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。
从名字中的 Mark Sweep 这两个词可以看出,CMS收集器是⼀种 “ 标记 - 清除 ” 算法实现的,它的运作过程相⽐于前⾯⼏种垃圾收集器来说更加复杂⼀些。整个过程分为四个步骤:(初始标记和重新标记这两步仍然要stop the world)
stop the world:除垃圾收集器线程之外的线程都被挂起。
① 初始标记 : 暂停用户线程,并标记下GC Roots能直接关联到的对象,速度很快 ;
② 并发标记 : 同时开启GC和⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以GC线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅。
从GC Root开始对堆中的对象进行可达性分析,找出存活的对象。
(书本:并发标记阶段就是进行GC Roots Tracing的过程)
③ 重新标记 : 暂停用户线程,重新标记阶段就是为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短。
最后
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
快 ;
② 并发标记 : 同时开启GC和⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以GC线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅。
从GC Root开始对堆中的对象进行可达性分析,找出存活的对象。
(书本:并发标记阶段就是进行GC Roots Tracing的过程)
③ 重新标记 : 暂停用户线程,重新标记阶段就是为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短。
最后
[外链图片转存中…(img-jeYd6ZXd-1713598515743)]
[外链图片转存中…(img-nCQ3KOof-1713598515743)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-UVa8JOcL-1713598515744)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!