网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
一、JVM简介
JVM意为Java虚拟机。虚拟机是指通过软件模拟的具有完整的硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
常见的虚拟机:JVM、WMware、Virtual Box。
JVM和其他两个虚拟机的区别:
- VMware等是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
- JVM则是通过软件模拟Java字节码的指令集,JVM只是保留了PC寄存器,其他的寄存器都进行了裁剪。
JDK、JRE、JVM的关联关系:
JDK和JRE的目录下都存在java.exe,通过其来运行class字节码文件,启动的时候就会创建一个JVM。JDK默认的JVM是HotSpot VM。
二、运行时数据区
1、堆(heap)
程序中所有创建的对象(数组也是对象)都存放在堆中。
- 对象指的是等号右边的;
- 变量指的是等号左边的。
注:jdk1.8,常量池(字符串常量池)也在堆中。
JVM参数设置:
- -Xms最小堆内存;
- -Xmx最大堆内存
堆里面分为两个区域(逻辑上):新生代和老生代。新生代放新建的对象,经过一定GC次数之后仍然存活的对象以及创建的大对象会放入老生代。
垃圾回收的时候会将Eden中存活的对象放在一个未使用的Survivor中,并把当前正在使用的Eden和Survivor清理掉。
2、方法区
用来存放被虚拟机加载的类信息(class字节码,进行类加载,把类加载到方法区,保存类的代码数据,同时堆中会生成一个类对象)、常量、静态变量、即时编译器编译后的代码等。
- 永久代(JDK1.7):属于Java内存进程;
- 元空间(JDK1.8):内存属于本地内存,不再受JVM最大内存的影响,与本地内存的大小有关。
- javac:编译器(编译Java代码为字节码文件);
- java:包含解释器(Java程序运行的时候,把class字节码文件翻译为机器码);
- 即时编译器:运行时,将热点代码翻译为机器码,之后就不用再进行翻译。
3、Java虚拟机栈
- Java虚拟机栈的生命周期和线程相同,
- 每个方法执行的时候都会创建一个栈帧,进行入栈操作,方法返回时,出栈;包含局部变量表、操作栈、动态链表、方法返回地址。
Java虚拟机栈:
- 局部变量表:**存放八大基本数据类型、变量(等号左边)。**局部变量表所需的空间在编译时分配。进入一个方法时,方法在帧内分配的局部变量空间是确定的,执行期间局部变量表的大小不会发生改变。
- 操作栈:每个方法会生成一个先进后出的操作栈;
- 动态链表:指向运行时常量池的方法引用;
- 方法返回地址:PC寄存器的地址。
4、本地方法栈
**调用Java方法,就创建栈帧,**放在线程的Java虚拟机栈中;如果调用其他的函数,就是用本地方法栈。
5、程序计数器
记录当前线程执行的行号。
6、内存布局中的异常问题
- 内存溢出(OOM):指存放数据的大小超出该区域的内存大小。运行时数据区域中,除了程序计数器,都可能发生内存溢出。内存溢出会导致整个进程都挂掉;
- 内存泄漏:线程生命周期太长,始终引用一些不使用的数据(没法进行gc垃圾回收),随着使用时间越来越长,不适用的垃圾就越来越多,可用空间越来越少——可能导致OOM。(解决方法:直接重启)
- StackOverflow:如果栈中的栈帧数量超过jvm规定,就会出现该异常。(递归使用不恰当)
三、类加载
1、类加载过程
(1)加载
加载class字节码到Java进程的内存中,在堆中创建类对象。
(2)验证
验证class字节码的规范(是否符合jvm规范)。
(3)准备
正式为类中的变量(静态变量)分配内存并对类变量进行初始化:
- static常量:赋值为初始值;
- static变量:赋值为缺省值;(Integer缺省值为null,int为0)
(4)解析
把常量池中的符号引用替换为直接引用:
- 符号引用:class文件(字节码)private static int x=123;此时进程还没有启动,无法表示变量x指向123(本质是指向内存地址),此时使用符号引用来表示这个指向关系;
- 直接引用:进程运行起来,x直接指向123(内存地址);
- 替换:把class文件常量池中的符号引用,替换为进程运行起来的运行时常量池中的直接引用。
(5)初始化
是类的初始化,不是对象初始化。初始化阶段,Java虚拟机真正开始执行类中编写的Java程序代码。初始化阶段就是执行类构造器方法的过程。
类对象的初始化:执行静态变量和静态代码块。
2、双亲委派模型
(1)什么是双亲委派模型
类加载的机制:双亲委派模型(jdk默认的类加载机制),其它机制(破坏双亲委派模型的其它机制)。
**类加载器:**包含四种,从上而下(上下关系,不是以extends继承来实现的,是逻辑上的上下关系):
- BootStrap ClassLoader 启动类加载器(主要负责加载Java核心类库,即%JRE_HOME%\lib目录)
- ExtClassLoader 扩展类加载器(主要负责加载目录%JRE_HOME%\lib\ext目录下的类)
- AppClassLoader 系统/应用类加载器(加载当前应用的classpath目录下的类)
- 自定义加载器
启动/扩展/应用这三类类加载器,只是加载jar目录下不同的jar包。
什么是双亲委派模型?
基于四种类加载器,按照从上到下的顺序,来加载类。类加载器收到类加载请求时,不会立即去自己加载,而是将这个请求向上传递,每一层都是如此,因此所有的加载请求最终应该在最顶层的启动类加载器中。因为类加载只执行一次,所以,上边找到,下边就不执行加载;上边没有找到,就交给下一级的加载。
(2)双亲委派模型的优点
- 避免重复加载类:如A类和B类都有一个父类C,当A启动起来的时候就会加载C,则B类进行加载时就不需要重复加载C类。
- 安全性:确保优先采用启动/扩展/应用类加载器来加载类。
如果自定义一个类加载,加载自定义的java.long.Object类:
【1】遵循双亲委派模型,就不会加载到自定义的Object,还是jre中的(安全);
【2】不遵循双亲委派模型,就可以加载到自定义的Object类(不安全)。【注】在jdk中,自定义类加载器时,进行了安全校验:加载类,如果类的全限定名以java./javax.开头的包,报错。
(3)破坏双亲委派模型
遵循双亲委派机制的类加载,某些场景下,可能无法实现知道需要加载的类名(jdbc中,jdk是无法知道数据库驱动类的类名)。
解决方案:破坏双亲委派机制。
常用的方案:SPI机制(jdk提供的一个ServiceLoader.load,约定了从jar包下/META-INF/services/文件名 中进行加载)
四、垃圾回收(GC)
- Java语言,不用自己分配内存,也不用自己回收内存(jvm中,实现了垃圾回收机制,自动回收)。
- 垃圾回收区域:堆(回收的主要区域)、方法区(保存类信息,静态变量,很少回收);
1、死亡对象判断算法
(1)引用计数算法
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器-1;任何时刻计数器为零的对象不能再被使用,即对象已死。**但jvm中没有选用引用计数器来管理内存:引用计数器无法解决对象的循环引用问题。**如下:
//此时instance的引用计数器会陷入死循环
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
(2)可达性分析算法
通过一系列称为“GC Roots”的对象作为起始点,从这些节点往下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有引用链相连时,证明是“不可达的”。
引用的分类(强度依次递减):
- 强引用:只要引用还在,就不会被回收;
- 软引用(SoftReference):系统要发生内存溢出之前,会对其及逆行回收;
- 弱引用:每次GC发生时都会进行回收;
- 虚引用:无法使用,只是在对象被回收时发起一个系统通知。
2、垃圾回收算法
(1)标记清除算法(老年代回收算法)
算法分为两个阶段:
- 标记:标记出所有需要回收的对象;
- 清除:标记完成后统一回收所有被标记的对象。
缺陷:
- 效率低:标记和清除过程效率都不高;
- 清除后产生大量不连续的内存碎片,导致在之后的运行中需要分配较大对象时,无法找到足够的连续内存不得不提前进行下一次垃圾回收。
(2)复制算法(新生代算法)
复制算法解决了”标记-清理“的效率问题。将可用的内存按容量分为大小相等的两块,每次只使用一块。当内存需要进行垃圾回收时,会将该区域存活的对象复制到另一块上面,然后将已经使用过的内存区域清理掉。
**使用场景:**大多数对象具有朝生夕死的特性。新生代的对象符合该特性,使用复制算法。
**缺陷:**空间利用率不高,只有50%。
JVM中,新生代的回收算法,是复制算法的优化版本:
- 新生代中98%以上的对象都是“朝生夕死”的,所以不需要按照1:1来划分内存空间,而是将内存划分为一块较大的Eden区和两块较小的Survivor区(8:1:1);
- 每次使用Eden和一块Survivor。回收时,将Eden和Survivor中存活的对象一次性复制到另一块Survivor空间,最后清理掉Eden和使用的Survivor空间。
- 部分对象会在两个Survivor区域来回复制,如此交换15次(默认15)之后如果还存活,会vu你放到老年代。
(3)标记整理算法(老年代算法)
复制收集算法在对象存活率较高时会及逆行大量的赋值操作,效率比较低。因此来年代一般不使用。标记整理算法过程和标记清除算法一致,不同的是,标记完后,不直接进行清除,而是将存活的对象移动到一端,最后清除掉端边界以外的内存。
(4)分代算法
分代算法(没有具体的算法实现)是通过区域划分,实现不同的区域不同的垃圾回收策略,从而更好地实现垃圾回收。JVM垃圾收集都采用的是“分代收集”算法,只是根据对象的存活周期的不同将内存分为几块。一般是分为新生代和老年代。新生代中,每次垃圾回收只有少量的对象存活,使用复制算法;老年代中,对象存活率高,使用标记清除算法或标记整理算法。
哪些对象进入新生代?哪些对象进入老年代?
- 默认创建的对象(非大对象)都进入新生代;
- 老年代:
【1】大对象:对象占据的空间超出jvm规定的阈值;
【2】新生代中年龄超过15的对象。
【3】新生代GC时,分配担保失败的对象:新生代GC时把Eden区域和s0中的存活对象复制到s1区域中。(新生代对象的特点是朝生夕死,大多数情况下,存活的对象不足10%,但是也有可能s1区域中放不下,此时就放入老年代中。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
入新生代;
- 老年代:
【1】大对象:对象占据的空间超出jvm规定的阈值;
【2】新生代中年龄超过15的对象。
【3】新生代GC时,分配担保失败的对象:新生代GC时把Eden区域和s0中的存活对象复制到s1区域中。(新生代对象的特点是朝生夕死,大多数情况下,存活的对象不足10%,但是也有可能s1区域中放不下,此时就放入老年代中。
[外链图片转存中…(img-QKU0m70j-1714969695278)]
[外链图片转存中…(img-Y0Ygt58a-1714969695279)]
[外链图片转存中…(img-oNNXjMIh-1714969695279)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新