JVM虚拟机-黑马
2024-12-01 21:21:09 13 举报
AI智能生成
黑马2023年JVM课程的总结,包含基础篇和面试篇
作者其他创作
大纲/内容
类的加载
类的生命周期
加载
加载时机
代码中包含main()方法的主类再JVM进程启动后会被加载到内存,开始执行其中代码时,若其中使用了别的类,也会把其对应的“.class”文件加载到内存里。
<br>
连接
验证
根据java虚拟机规范,校验加载的".class"文件是否符合指定的规范
自动进行,一般不需要程序员参与
文件格式验证(魔数),元信息验证(必须有父类),验证程序执行指令的语义(指令跳转),符号引用验证(是否用到其他类中private方法)
准备
给静态变量(static)分配一定内存空间,并赋予默认初始值
final修饰的静态变量在准备阶段就会进行赋值(不用等到初始化)
解析
将常量池中的符号引用替换成指向内存的直接引用
初始化
触发初始化时机
当新建某个类的对象时,如通过new或者反射,克隆,反序列化等
当虚拟机启动某个被表明为启动类的类,即包含main方法的那个类,所以system.out.println(Test.class)
初始化一个子类的时候,会先初始化其父类
当调用某个类的静态方法时
当使用某个类或者接口的静态字段时
调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中类的方法时
触发初始化
1.访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。<br>2.调用Class.forName(String className)。<br>3.new一个该类的对象时。<br>4.执行Main方法的当前类。
不会触发初始化的情况
1.无静态代码块且无静态变量赋值语句。<br>2有静态变量的声明,但是没有赋值语句。<br>3.静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
初始化阶段会执行静态代码块中的代码,并为静态变量赋值。<br>初始化阶段会执行字节码文件中cliniti部分的字节码指令。
/new一个对象时的优先级:静态代码块(只在类加载时执行一次)> 实例(局部)代码块 > 构造代码块
/刚刚出去补了一下课,构造代码块比构造函数优先执行
执行静态代码块和静态变量的赋值
使用
new对象,我在使用
卸载
在垃圾回收篇进行讲解
类加载器和双亲委派机制
类加载器<br>
作用
类加载器<br>(ClassLoader)负责在类加载过程中的字节码获取并加载到内存这<br>一部分。通过加载字节码数据放入内存转换成byt],接下来调用虚拟机底层<br>方法将byte]转换成方法区和堆中的数据。
Java代码实现
JDK提供了多种不同渠道的类加载器,程序员也可以自己按需求定制
所有Java中实现的类加载器都需要继承ClassLoader这个抽象类<br>
Java虚拟机底层源码实现
保证Java程序运行中基础类被正确加载,比如String,保证可靠性,程序员了解即可
源代码位于虚拟机源码中,实现语言和虚拟机底层语言一致,比如Hotspot使用C++
JDK8和JDK9之间的区别很大
JDK8及以前
类加载器的分类
启动类加载器 Bootstrap ClassLoader
加载jdk安装目录下lib目录中的核心类库
不推荐到安装目录中进行拓展,而是使用参数进行扩展
最顶层的加载器
扩展类加载器 Extension ClassLoader
加载jdk安装目录下lib\ext目录
通用但不常用
不推荐到安装目录中进行拓展,而是使用参数进行扩展
次顶层加载器
应用程序加载器 Application ClassLoader
加载ClassPath环境变量中指定路径中的类,可以理解为加载你写的代码
三级的加载器
自定义类加载器
需要重写findClass方法
双亲委派机制
问题:由于Java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底由谁加载的问题。
假设应用程序类需要加载一个类,首先会委派给父类加载器加载,最总传导到顶层的类加载器去加载。但如果父类加载器在自己负责的加载范围内没找到这个类,就会下推权力给自己的子类加载器。
有父类就委派给父类,再自顶向下进行加载<br>
向上查找,避免重复加载<br>
避免改动核心类库,例如String,为了安全
这个父是一个成员变量,可以理解为上级,不是继承关系
图示
打破双亲委派机制<br>
自定义加载器
重写loadClass方法,不再实现双亲委派机制。
如果不打破双亲委派机制,那么同限定名的类就无法加载
正确的去实现一个自定义类加载器的方式是重写findClass方法,这样不会破坏双亲委派机制。
JDBC案例
问题:引入的jar包本应该在应用程序加载器加载,但是被父类加载器截获,所以需要打破双亲委派机制<br>
具体来说这一节就是jdbc为何不遵循双亲委派机制,drive。
SPI机制
JDK内置的一种服务提供发现机制,阿里的double也用到了<br>
类似于Spring的依赖注入<br>
SPI机制使用线程的上下文来获取应用程序类加载器<br>
获取线程上下文中保存的类加载器,默认就是应用程序类加载器
/打破双亲委派,本来DriverManger是Boot加载的,那么按理Driver也应该由Boot加载,单位DM委托APP去加载,所以说打破了
/简单的说,因为DriverManager只能拿到启动类加载器,而需要加载的驱动是第三方的必须由应用程序加载器去加载,所以通过线程上下文去拿到一个应用程序加载器去加载
/只是SPI给人好像打破了的感觉,其实到Application那里照旧走双亲委派
Tomcat这种web容器中类加载器设计实现思路。
图示
JDK8之后的类加载器
JDK9引入了module的概念,类加载器在设计上发生了很多变化
启动类加载器使用Java编写,Bootstrap的C++变成了BootClassLoader的java
扩展类加载器被替换成了平台类加载器(Platform Class Loader)
所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。
如何防止“.class”被反编译窃取公司源码
编译时,可采用小工具对字节码进行加密,或者做混淆处理
购买专业的第三方公司做商业级字节码文件加密的产品。
JVM内存区
总览
JVM组成
<br>
内存区域划分
意义
JVM在运行我们写好的代码时,需要使用多块内存空间,不同的内存空间用来存放不同的数据,然后配合我们写代码的流程,才能让我的系统运行起来。
Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区。
图示
详情
线程共享数据区域
方法区(元空间)
方法区是用来存储每个类的基本信息(元信息),一般称之为InstanceKlass对象。在类的加载阶段完成。
基本信息,常量池,字段,方法,虚方法表(实现多态的基础)<br>
JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。
方法区是《Java虚拟机规范》中设计的虚拟概念,每款)ava虚拟机在实现上都各不相同。Hotspoti设计如下
JDK7及之前的版本将方法区存放在堆区域中的<b><u><i><font color="#e74f4c">永久代(perm_gen)</font></i></u></b>空间,堆的大小由虚拟机参数来控制。<br>JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不<br>超过操作系统承受的上限,可以一直分配。
存放基础信息(我们写的类)的位置,线程共享<br>
类的元信息
方法区是用来存储每个类的基本信息(元信息),一般称之为InstanceKlass.对象。在类的加载阶段完成。
运行时常量池
方法区除了存储类的元信息之外,还存放了运行时常量池。常量池中存放的是字节码中的常量池内容。<br>字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,可以通<br>过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池。
字符串常量池
方法区中除了类的元信息、运行时常量池之外,还有一块区域叫字符串常量池(StringTable)。<br>字符串常量池存储在代码中定义的常量字符串内容。比如”123”这个123就会被放入字符串常量池。
和运行时常量池的关系
字符串常量池和运行时常量池有什么关系?<br>早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整<br>将字符串常量池和运行时常量池做了拆分。
/字符串常量池放在堆中,7之前在方法区申。7之后一直堆中
false
StringBuilder 拼接字符串使用 append,然后 toString 创建一个字符串对象,存放在堆内存中,和字符串常量池中的地址不相同
左false,右true
/字符串常量池中,保存的字符不会重复
JDK7及之后版本中由于字符串常量池在堆上,所以intern0方法会把第一次遇到的字符串的<br>用放入字符串常量池。
方法区溢出
JDK7和DK8在方法区的存放上,采用了不同的设计。
JDK7将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数-XX:MaxPermSize=值来控制。<br>JDK8将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受<br>的上限,可以一直分配。可以使用-XX:MaxMetaspacesize=值将元空间最大大小进行限制。
最好设置一下,256M就差不多,防止直接内存占用过高
-XX:PermSize
永久代最大大小
JDK1.8以后,这个参数被替换成了-XX:MetaspaceSize
-XX:MaxPermSize
永久代最大大小
JDK1.8以后,这个参数被替换成了-XX:MaxMetaspaceSize
堆内存
存放我们在代码中创建的各种对象的
栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实<br>现对象在线程之间共享。
一般Java程序中堆内存是空间最大的一块内存区域。
堆内存是会溢出的(最容易溢出)
堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出OutOfMemory<br>错误。
堆空间有三个需要关注的值,used total max。<br>used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大堆内存。
一般max大概有2G
当used=max=total时,堆内存就溢出了吗,不是,堆内存溢出判断条件比较复杂,因为垃圾回收器也在工作
堆内存的设置
如果不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64。在实际应用中一般都需要设置<br>total和max的值。
-Xms
Java堆内存的初始total大小
-Xmx
Java堆内存最大大小
限制,限制:Xmx必须大干2MB,Xms必须大干1MB
arthas使用memory可以监控堆内存使用情况
arthas中的heap堆内存使用了JMX技术中内存获取方式,这种方式与垃圾回<br>收器有关,计算的是可以分配对象的内存,而不是整个内存。
建议将-Xmx和-Xms设置为相同的值,减少了申请并分配内存时间上的开销
堆内存的划分
新生代
区域划分
Eden区
Survivor(from)区 设置survivor是为了减少送到老年代的对象
Survivor(to)区 设置两个Survivor区是为了解决碎片问题
划分比例
eden:survovir:survivor=8:1:1
老年代
线程私有数据区域
程序计数器
程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的的字<br>节码指令的地址。
/程序计数器:控制程序指令的执行
在多线程执行情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继<br>续解释运行。
程序计数器在运行中内存不会溢出,64位计算机每个线程只存储一个固定长度的内存地址
程序员无需对程序计数器做任何处理
每个线程会通过程序计数器记录当前要执行的的字节码指令的地址程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
我们写好的java代码会被翻译成字节码,对应各种字节码指令,字节码指令一条条被执行,才能实现我们写好的代码的执行效果。<br>当JVM加载类信息到内存之后,会使用自己的字节码执行引擎,去执行编译后的代码指令。<br>执行时,JVM需要一个特殊的内存区域,程序计数器(用来记录当前执行字节码指令位置的,每一个线程都会有自己的程序计数器),记录目前执行到哪一条字节码指令。<br>
虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理<b style=""><u style="">方法调用中</u></b>的基本数据,先<br>进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存。
栈帧在报错时会弹出栈中的所有数据,一大团at就是这个
栈帧的组成(了解为主)
局部变量表
局部变量表的作用是在运行过程<br>中存放所有的局部变量
栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot),long和double类型占用两个槽,其<br>他类型占用一个槽。
局部变量表的作用是在方法执行过程中存放所有的局部变量。编译成字节码文件时就可以确定局部变<br>量表的内容。
方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。<br>局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量。
操作数栈
操作数栈是栈帧中虚拟机在执<br>行指令过程中用来存放临时数<br>据的一块区域
深度
帧数据
帧数据主要包含动态链接、方<br>法出口、异常表的引用
动态连接
当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池<br>中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。
符号引用转内存引用的映射
方法出口
方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的<br>下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
存下一条指令的地址
异常表
异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
方便捕获异常对象
栈内存溢出
Java虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存谥出。<br>Java虚拟机栈内存谥出时会出现StackOverflowError的错误
/常见于递归忘写跳出逻辑
/不是死循环,是死递归
虚拟机栈的默认大小,linux是1MB,Windows基于操作系统默认值(大概率也是1M,1024k)
-Xss512k,修改为512k
Hotspot JVM对栈的大小有要求,Windows(x64)下最小180k,最大1024M,不然动态调整到这个范围
局部变量过多、操作数栈深度过大也会影响栈内存的大小。
一般情况下不动,也可以手动设置为-Xss256k节省内存
每个线程都有自己的java虚拟机栈,用来存放自己执行的那些方法的局部变量,执行了一个方法,就创建一个栈帧。<br>栈帧里就有这个方法的局部变量表,操作数栈,动态链接,方法出口等东西。
调用执行任何方法时,都会给方法创建栈帧后入栈,在栈帧里存放了这个方法对应的局部变量之类的数据,包括这个方法执行的其他相关信息,方法执行完毕后出栈。
本地方法栈
调用native方法的时候,就会有线程对应的本地方法栈。这个虚拟机栈类似,是存放各种native方法的局部变量表之类的信息。
Java虚似机栈存储了)ava方法调用时的栈帧,而本地方法栈存储的是native:本地方法的栈帧。<br>在Hotspot虚拟机中,Jva虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内<br>存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
直接内存
直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。
零拷贝
在JDK1.4中引入了NO机制,使用了直接内存,主要为了解决以下两个问题:<br>1、Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用.<br>2、IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。<br>现在直接放入直接内存即可,同时va堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
netty也是类似思想
拓展
要创建直接内存上的数据,可以使用Byte Buffer。<br>语法:ByteBuffer directBuffer=ByteBuffer.allocateDirect(size);<br>注意事项:arthas的memory命令可以查看直接内存大小,属性名direct。
还有一个区域,不属于JVM,通过NIO中的allocateDirect这种API,可以在java堆外分配内存空间,然后,通过java虚拟机里面的DirectByteBuffer来引用和操作堆外的空间。
直接内存溢出
直接内存也是会溢出的
-XX:MaxDirectMemorySize=1m
直接内存的最大大小
最好设置一下,压力测试决定<br>
内存的消耗
我们在java堆内存里创建的对象,都是占用内存资源的,而且内存资源有限。
举例
一个方法执行完毕,就会从虚拟机中出栈,那么原来那个栈帧里面的局部变量,比如一个实例对象,就没有任何一个变量指向它了。这种空占着资源着的内存资源,如何处理?答案是通过 JVM的垃圾回收机制。
我们创建的对象,在java堆内存中占用多少内存空间?
对象本身的一些信息
对象的实例变量作为数据占用的空间
内存相关参数
-Xms
Java堆内存的初始total大小
-Xmx
Java堆内存最大大小
-Xmn
Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了
-XX:PermSize
永久代最大大小
JDK1.8以后,这个参数被替换成了-XX:MetaspaceSize
-XX:MaxPermSize
永久代最大大小
JDK1.8以后,这个参数被替换成了-XX:MaxMetaspaceSize
-Xss
每个线程的栈内存大小
-XX:SurvivorRatio=8
定义了新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10
-XX:ThreadStackSize
设置JVM栈内存
-XX:+HeapDumpOnOutOfMemoryError
在OOM的时候,自动dump内存快照出来
-XX:HeapDumpPath
在OOM时候,dump内存快照保存位置
-XX:MaxDirectMemorySize=1m
直接内存的最大大小
垃圾回收机制
概览
C/C++的内存管理
在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现<br>内存泄漏。我们称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收。<br>内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。
自动垃圾回收<br>自动根据对象是否使用由虚拟机来回收对象<br>·优点:降低程序员实现难度、降低对象回收bug的可能性<br>·缺点:程序员无法控制内存回收的及时性
Java的内存管理
Java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。通过垃<br>圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。其他<br>很多现代语言比如C并、Python、Go都拥有自己的垃圾回收器。
手动垃圾回收<br>由程序员编程实现对象的删除<br>·优点:回收及时性高,由程序员把控回收的时机<br>·缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题
线程不共享的部分
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法之后就会<br>自动弹出栈并释放掉对应的内存。
方法区的回收
定一个类是否可以被卸载
此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。<br>加载该类的类加载器已经被回收。<br>该类对应的java.lang.Class对象没有在任何地方被引用。
三个条件都满足才会触发回收
<font color="#e74f4c">一般情况下自己写的类是应用程序类加载器进行加载的,这个类加载器不会被回收,自己写的类只要被加载就不会被回收</font><br>
开发中此类场景一般很少出现,主要在如OSGi、JSP的热部署等应用场景中。<br>每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类<br>加载器。重新创建类加载器,重新加载jsp文件。
手动触发回收的方法
●如果需要手动触发垃圾回收,可以调用System.gc()方法。<br>语法:System.gc()<br>注意事项:<br>调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要<br>执行垃圾回收Java虚拟机会自行判断。
堆回收
引用计数法和可达性分析法
如何判断堆上的对象可以被回收?
常见的有两种判断方法:引用计数法和可达性分析法。
Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还<br>在使用,不允许被回收。
例子
如果在main方法中最后执行a1=null,b1=nul,是否能回收A和B对象呢?<br>可以回收,方法中已经没有办法使用引用去访问A和B对象了。
引用计数器
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1,
图
优缺点
引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法,但是它也存在缺点,主要有两点:<br>1.每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响<br>2.存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。
图
Java根本没用这个。。。
/引用计数法无法解决循环引用,所以Java采用可达性分析法
可达性分析算法
垃圾回收跟对象(GC Root对象)和普通对象
图示
哪些对象被称为GC Root对象
线程Thread对象,引用线程栈帧中的方法参数、局部变量等。<br>系统类加载器加载的java.lang.Class对象。<br>监视器对象,用来保存同步锁synchronized关键字持有的对象。<br>本地方法调用时使用的全局对象。
程序运行的main就是一个主线程,刚刚的循环引用可以被回收,线程对象GC Root指向整个栈内存
/解释一下2:系统类加载器也就是应用程序类加载器,前面说到过,他是不可回收的,这个类加载器不会被回收,所以这个类也就不会被回收,那么这个类里面的静态变量也就不会被回收,所以就可以是GC root对象
/常量在方法区,instancekclass有其引用,而class是instrancekcalss的引用,能访问到常量,而class是gcroot
第三类
第四类,本地方法由Java虚拟机调用,不用程序员过多关注
查看GC Root(Arthas)
五种对象引用
强引用
可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,<br>普通对象就不会被回收。
软引用
软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,<font color="#e74f4c">当程序内存不足时,就会将软<br>引用中的数据进行回收。</font>常用于缓存
在JDK1.2版之后提供了SoftReference类来实现软引用<br>强引用的对象使用结束后,可以用软引用关联,内存不足时这个对象就会被回收<br>SoftReference对象也需要被GC Root对象用强引用进行关联,不然会被回收
软引用的执行过程如下:<br>1.将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。<br>2.内存不足时,虚拟机尝试进行垃圾回收。<br>3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。<br>4.如果依然内存不足,抛出OutOfMemory异常。
软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收。如何知道哪些SoftReference对<br>象需要回收呢?<br>SoftReference提供了一套队列机制:<br>1、软引用创建时,通过构造器传入<font color="#e74f4c">引用队列</font><br>2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列<br>3、通过代码遍历引用队列,将SoftReference的强引用删除
弱引用
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,<font color="#e74f4c">不管内存够不够都会直接被回收</font>。<br>在JDK1.2版之后提供了VeakReference类来实现弱引l用,弱引用主要在ThreadLocal中使用。<br>弱引用对象本身也可以使用引用队列进行回收。
用的不多,主要在ThreadLocal中
虚引用
虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回<br>收器回收时可以接收到对应的通知。Java中使用PhantomReference3实现了虚引用,直接内存中为了及时知道<br>直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
终结器引用
终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后<br>由一条由FinalizerThread:线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该<br>对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。
垃圾回收算法
Java是如何实现垃圾回收的呢?简单来说,垃圾回收要做的有两件事:<br>找到内存中存活的对象<br>释放不再存活对象的内存,使得程序能再次利用这部分空间
所以判断GC算法是否优秀,可以从三个方面来考虑:
1.吞吐量<br>吞吐量指的是CPU用于执行用户代码的时间与CPU总执行时间的比值,即吞吐量=执行用户代码时间/<br>(执行用户代码时间+GC时间)。吞吐量数值越高,垃圾回收的效率就越高。
比如:虚拟机总共运行了100分钟,其中GC花掉1分钟,<br>那么吞吐量就是99%
2.最大暂停时间<br>最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。比如如下的图中,黄色部分的STW就是最<br>大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时<br>受到的影响就越短。
3.堆使用效率<br>不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算<br>法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。<br>一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。<br><font color="#e74f4c">不同的垃圾回收算法,适用于不同的场景。</font>
STW
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所<br>有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。
标记清除算法<br>
标记清除算法的核心思想分为两个阶段:<br>1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root;开始通过引用链遍历出<br>所有存活对象。<br>2清除阶段,从内存中删除没有被标记也就是非存活对象。
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:1碎片化问题<br>由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一<br>个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。<br>缺点:2.分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才<br>能获得合适的内存空间。
复制算法
复制算法的核心思想是:<br>1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。<br>2.在垃圾回收GC阶段,将From中存活对象复制到To空间。<br>3.将两块空间的From和To名字互换。
优点:吞吐量高<br>复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历的过程,因而性<br>能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动<br>优点:不会发生碎片化<br>复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
缺点:内存使用效率低<br>每次只能让一半的内存空间来为创建对象使用
标记整理算法
标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。<br>核心思想分为两个阶段:<br>1.标记阶段,将所有存活的对象进行标记。Jva中使用可达性分析算法,从GC Root开始通过引用链遍历出<br>所有存活对象。<br>2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
优点:内存使用效率高<br>整个堆内存都可以使用,不会像复制算法只能使用半个堆内存<br>优点:不会发生碎片化<br>在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
缺点:整理阶段的效率不高<br>整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过Two-<br>Finger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能
分代垃圾回收算法
介绍:现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。
年轻代和老年代
虚拟机参数,调整内存区域的大小
/这节课老师讲的很烂
/8:1:1
分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。<br>随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为<br>Minor GC或者Young GC(是复制算法)。<br>Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。
接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。
此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。
注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。<br>当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。
Minor GC也会有STW,只不过比较短,Full GC停顿时间很长
Full GC无法回收掉老年代的对象,那么当对象<br>继续放入老年代时,就会抛出Out Of Memory异常。
为什么分代GC算法要把堆分成年轻代和老年代?
系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收,比如用户获取订单数据,订单数据返回<br>给用户之后就可以释放了。<br>老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。<br>在虚拟机的默认设置中,新生代大小要远小于老年代的大小。
分代GC算法将堆分成年轻代和老年代主要原因有:<br>1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。<br>2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记清除和标记整理<br>算法,由程序员来选择灵活度较高。<br>3、分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(ful<br>gc),STW时间就会减少。
/为了适配不同类型的应用程序
/灵活性强
minor gc尽可能多,full gc尽可能 少
优缺点
程序中大部分对象都是朝生夕死,在年轻代创建并且回收,只有少量对象会长期存活进入老年代。分代垃圾<br>回收的优点有:<br>1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。<br>2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法效率高、不会产生内存碎片,老年<br>代可以选择标记清除和标记整理算法,由程序员来选择灵活度较高。<br>3、分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收<br>(full gc),STW(Stop The World)由垃圾回收引起的停顿时间就会减少。
垃圾回收器
垃圾回收器的组合关系
垃圾回收器是垃圾回收算法的具体实现
各种垃圾回收器
年轻代-Serial垃圾回收器
Serial是是一种<font color="#e74f4c">单线程串行</font>回收年轻<br>代的垃圾回收器。
c瑞欧
老年代-SerialOld垃圾回收器
SerialOld是Serialf垃圾回收器的老年代版<br>本,采用单线程串行回收
年轻代-ParNew垃圾回收器
ParNew垃圾回收器本质上是对Serial在多<br>CPU下的优化,使用多线程进行垃圾回收
趴牛
老年代-CMS(Concurrent Mark Sweep)垃圾回收器
CMS垃圾回收器关注的是系统的暂停时间,<br>允许用户线程和垃圾回收线程在某些步骤中<br>同时执行,减少了用户线程的等待时间:
CMS执行步骤:<br>1.初始标记,用极短的时间标记出GC Rootsi能直接关联到的对象。<br>2.并发标记,标记所有的对象,用户线程不需要暂停。<br>3重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。<br>4并发清理,清理死亡的对象,用户线程不需要暂停。
发生STW的是初始标记和重新标记,时间也较短
缺点:<br>1、CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS会在Full GC时进行碎片的整理。<br>这样会导致用户线程暂停,可以使用-XX:CMSFullGCsBeforeCompaction=N参数(默认O)调整N次Full GC之后再整理。<br>2、无法处理在并发清理过程中产生的"浮动垃圾”,不能做到完全的垃圾回收。<br>3、如果老年代内存不足无法分配对象,CMS就会退化成Serial Old!单线程回收老年代。
/三色标记法
年轻代-Parallel Scavenge:垃圾回收器
Parallel Scavenge是JDK8默认的年轻代垃圾回收器<br>多线程并行回收,关注的是系统的吞吐量。具备<font color="#e74f4c">自动<br>调整堆内存大小</font>的特点。
帕瑞李奥 死该问之,说PS就行
吞吐量和最大暂停时间是矛盾的
可以设置,但冲突时可能满足不了
要多做测试,尽量大到最大值
Parallel Scavenge允许手动设置最大暂停时间和吞吐量。
老年代-Parallel Old垃圾回收器
Parallel Old是为Parallel Scavengel收集器<br>设计的老年代版本,利用多线程并发收集。
/适合大文件处理和导出 没有用户交互
/这里写错了吧,都Paralled肯定是多个线程收集器并行收集
/多核肯定是并行阿
/这是并行
G1垃圾回收器
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。<br>Parallel Scavenge:关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小。<br>CMS关注暂停时间,但是吞吐量方面会下降。
而G1设计目标就是将上述两种垃圾回收器的优点融合:<br>1.支持巨大的堆空间回收,并有较高的吞吐量。<br>2.支持多CPU并行垃圾回收。<br>3.允许用户设置最大暂停时间。
<font color="#e74f4c">JDK9之后强烈建议使用G1垃圾回收器。</font>
G1出现之前的垃圾回收器,内存结构一般是连续的
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。
G1垃圾回收有两种方式
年轻代回收(Young GC)<br>
年轻代回收(Young GC),回收Eden区和Survivorl区中不用的对象。会导致STW,G1中可以通过参数<br>-XX:MaxGCPauseMillis:=n(默认200)设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地<br>保证暂停时间。
混合回收(Mixed GC)
混合回收分为:初始标记(initial mark)、并发标记(concurrent mark)、最终标记(remark或者Finalize<br>Marking)、并发清理(cleanup)
G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbage<br>first)名称的由来。
比CMS快
最后的清理使用复制算法,不会产生内存碎片
G1垃圾回收器-执行流程
1、新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC。<br>2、标记出Eden和Survivorl区域中的存活对象,<br>3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivo区中(年龄+1),清空这些区域。
4、后续Young GC时与之前相同,只不过Survivorl区中存活对象会被搬运到另一个Survivorl区。<br>5、当某个存活对象的年龄到达阈值(默认15),将被放入老年代。
6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongousl区。比如堆内存是<br>4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。
何有忙这儿死(意思是巨大的)
7、多次回收之后,会出现很多Od老年代区,此时总堆占有率达到阈值时<br>(-XX:InitiatingHeap0 ccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和<br>部分老年代的对象以及大对象区。采用复制算法来完成。
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivorl区的平均耗时,以作为下次回收时的<br>参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。<br>比如-XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能▣收4个Region。
G1垃圾回收器-FULL GC
/此处的FullGC,Java9中是单线程的,Java10改进成多线程了
如何使用
参数使用
/SATB算法优于CMS
特定版本比较好的组合
图
/g1这个项目从立项到商用花了10年,你太小看g1了。
面试篇
什么是JVM?
1、定义:JVM指的是ava虚拟机(Java Virtual Machine)。JVM本质上是一个运行在计算机上的程序,<br>他的职责是运行ava字节码文件,Java虚拟机上可以运行Java、Kotlin、Scala、Groovy等语言。
2、作用:为了支持制ava中Write Once,Run Anywhere;编写一次,到处运行的跨平台特性。
3、JVM的功能
1,解释和运行
对字节码文件中的指令,实时的解释成机器码,让计算机执行
2,内存管理
自动为对象、方法等分配内存<br>自动的垃圾回收机制,回收不再使用的对象
3,即时编译
对热点代码进行优化,提升执行效率
4、JVM的组成
图
没有面试篇的好
常见的JVM
子主题
总结
什么是VM?<br>1、JVM指的是ava虚拟机,本质上是一个运行在计算机上的程序,他的职<br>责是运行ava字节码文件,作用是为了支持跨平台特性。<br>2、JVM的功能有三项:第一是解释执行字节码指令;第二是管理内存中对象的<br>分配,完成自动的垃圾回收;第三是优化热点代码提升执行效率。<br>3、JVM组成分为类加载子系统、运行时数据区、执行引擎、本地方接口这四部<br>分。<br>4、常用的JVM是Oracle提供的Hotspot虚拟机,也可以选择GraalVM、龙井、<br>Open)9等虚拟机。
字节码文件的组成
字节码文件本质上是一个二进制的文件,无<br>法直接用记事本等工具打开阅读其内容。需<br>要通过专业的工具打开。
基本信息<br>常量池<br>字段<br>方法<br>属性
什么是运行时数据区
运行时数据区指的是M所管理的内存区域,其中分成两大类:<br>线程共享 - 方法区、堆<br>线程不共享 - 本地方法栈、虚拟机栈、程序计数器<br>直接内存主要是NIO使用,由操作系统直接管理,不属于JVM内存!
程序计数器
程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的的字节码<br>指令的地址。主要有两个作用:<br>1、程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。<br>2、在多线程执行情况下,Jva虚拟机需要通过程序计数器记录cPU切换前解释执行到那一句指令并继续解释运行。
0x000001f248c072c0内存地址
栈
Jva虚拟机栈采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存。<br>每个线程都会包含一个自己的虚拟机栈,它的生命周期和线程相同。
c方法的栈帧<br>B方法的栈帧<br>A方法的栈帧<br>main方法的栈帧<br>栈内存
栈帧
栈帧主要包含三部分内容:<br>1、局部变量表,在方法执行过程中存放所有的局部变量。<br>2、操作数栈,虚拟机在执行指令过程中用来存放临时数据的一块区域。<br>3、帧数据,主要包含动态链接、方法出口、异常表等内容。
帧数据(了解即可)
动态链接:方法中要用到其他类的属性和方法,这些内容在字节码文件中是以编号保存的,运行过程中需要替换成<br>内存中的地址,这个编号到内存地址的映射关系就保存在动态链接中。<br>方法出口:方法调用完需要弹出栈帧,回到上一个方法,程序计数器要切换到上一个方法的地址继续执行,方法出<br>口保存的就是这个地址。<br>异常表:存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
本地方法栈
1,Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧。<br>2,在Hotspot虚拟机中,<font color="#e74f4c">Java虚拟机栈和本地方法栈实现上使用了同一个栈空间</font>。本地方法栈会在栈内<br>存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
堆
1,一般va程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。<br>2,栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实<br>现对象在线程之间共享。<br>3,堆是垃圾回收最主要的部分,堆结构更详细的划分与垃圾回收器有关。
堆内存结构和垃圾回收器有关,可能是一个空间,也可能是年轻代和老年代,不要着急说
图
方法区
方法区是)va虚拟机规范中提出来的一个虚拟机概念,在HotSpot不同版本中会用永久代或者元空间来实现。方法<br>区主要存放的是基础信息,包含:<br>1、每一个加载的类的元信息(基础信息)。<br>2、运行时常量池,保存了字节码文件中的常量池内容,避免常量内容重复创建减少内存开销。<br>3、字符串常量池,存储字符串的常量。
图
直接内存(可选)
直接内存并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。在JDK1.4中引入了NlO机<br>制,由操作系统直接管理这部分内容,主要为了提升读写数据的性能。在网络编程框架如Ntty中被大量使用。<br>要创建直接内存上的数据,可以使用ByteBuffer。<br>语法:ByteBuffer directBuffer=ByteBuffer.allocateDirect(size);<br>
衍生问题
哪些区域会出现内存溢出,会有什么现象?<br>JVM在JDK6-8之间在内存区域上有什么不同
总结
什么是运行时数据区?<br>运行时数据区指的是M所管理的内存区域,其中分成两大类:<br>线程共享 - 方法区、堆<br>方法区:存放每一个加载的类的元信息、运行时常量池、字符串常量池。<br>堆:存放创建出来的对象。<br>线程不共享 - 本地方法栈、<br>虚拟机栈、程序计数器<br>本地方法栈和虚拟机栈都存放了线程中执行方法时需要使用的基础数据。<br>程序计数器存放了当前线程执行的字节码指令在内存中的地址。<br>直接内存主要是NIO使用,由操作系统直接管理,不属于JVM内存。
哪些区域会产生内存溢出
内存谥出指的是内存中某一块区域的使用量超过了允许使用的最大值,从而使用内存时因空间不足而失败,虚拟机一般<br>会抛出指定的错误。<br>在Java虚拟机中,只有程序计数器不会出现内存谥出的情况,因为每个线程的程序计数器只保存一个固定长度的地址。
堆内存溢出
堆内存溢出:<br>堆内存谥出指的是在堆上分配的对象空间超过了堆的最大大小,从而导致的内存谥出。堆的最大大小使用-Xx参数进<br>行设置,如-Xmx10m代表最大堆内存大小为10m。<br>溢出之后会抛出OutOfMemoryError,.并提示是Java heap Space导致的:
栈内存溢出
栈内存溢出:<br>栈内存谥出指的是所有栈帧空间的占用内存超过了最大值,最大值使用-Xss进行设置,比如-Xss256k代表所有栈帧占用<br>内存大小加起来不能超过256k。<br>溢出之后会抛出StackOverflowError:
不能小于180k
方法区溢出
方法区内存溢出:<br>方法区内存益出指的是方法区中存放的内容比如类的元信息超过了方法区内存的最大值,JDK7及之前版本方法区使用<font color="#e74f4c">永<br>久代(-XX:MaxPermSize=值)</font>来实现,JDK8及之后使用<font color="#e74f4c">元空间(-XX:MaxMetaspacesize=值)</font>来实现。
子主题
直接内存溢出
直接内存溢出:<br>直接内存益出指的是申请的直接内存空间大小超过了最大值,使用-XX:MaxDirectMemorySize=值设置最大值。<br>溢出之后会抛出OutOfMemoryError: Direct buffer memory
JDK6 - 8之间在内存区域上有什么不同
方法区是《们ava虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同。Hotspot设计如下:<br>1,<font color="#e74f4c">JDK7及之前的版本</font>将方法区存放在<font color="#e74f4c">堆区域中的永久代空间</font>,堆的大小由虚拟机参数来控制。<br>2,<font color="#e74f4c">JDK8及之后的版本</font>将方法区存放在<font color="#e74f4c">元空间</font>中,元空间位于操作系统维护的直接内存中,默认情况下只要不<br>超过操作系统承受的上限,可以一直分配。也可以手动设置最大大小。
使用元空间替换永久代的原因:<br>1、提高内存上限:元空间使用的是操作系统内存,而不是M内存。如果不设置上限,只要不超过操作系统内存<br>上限,就可以持续分配。而永久代在堆中,可使用的内存上限是有限的。所以使用元空间可以有效减少OOM情况<br>的出现。<br>2、优化垃圾回收的策略:永久代在堆上,垃圾回收机制一般使用老年代的垃圾回收方式,不够灵活。使用元空间<br>之后单独设计了一套适合方法区的垃圾回收机制。
字符串常量池的位置
早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整<br>将字符串常量池和运行时常量池做了拆分。
字符串常量池从方法区移动到堆的原因:<br>1、<font color="#e74f4c">垃圾回收优化</font>:字符串常量池的回收逻辑和对象的回收逻辑类似,内存不足的情况下,如果字符串常量池中的<br>常量不被使用就可以被回收;方法区中的类的元信息回收逻辑更复杂一些。移动到堆之后,就可以利用对象的垃圾<br>回收器,对字符串常量池进行回收。<br>2、<font color="#e74f4c">让方法区大小更可控</font>:一般在项目中,类的元信息不会占用特别大的空间,所以会给方法区设置一个比较小的<br>上限。如果字符串常量池在方法区中,会让方法区的空间大小变得不可控。<br>3、<font color="#e74f4c">intern方法的优化</font>:JDK6版本中intern()方法会把第一次遇到的字符串实例复制到永久代的字符串常量<br>池中。JDK7及之后版本中由于字符串常量池在堆上,就可以进行优化:字符串保存在堆上,把字符串的引用放入<br>字符串常量池,减少了复制的操作。
/是的,在 Java 8 及以后的版本中,字符串常量池和运行时常量池都在堆中。
/勾史,别误导别人,8之后字符串常量池在堆上,运行时常量池是方法区的一部分在元空间
类的生命周期<br>
加载阶段
1、加载(Loading)阶段第一步是<font color="#e74f4c">类加载器</font>根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。<br><font color="#e74f4c">程序员可以使用Java代码拓展的不同的渠道</font>。
本地文件(磁盘上的字节码文件)
动态代理生成(Spring中大量使用)
通过网络传输的类(早期的Applet技术使用)
2、类加载器在加载完类之后,Jva虚拟机会将字节码中的信息保存到内存的方法区中。在方法区生成一个<br>InstanceKlass对象,保存类的所有信息。
3、在堆中生成一份与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息。
连接阶段
验证
验证内容是否满足《Java虚拟机规范》
连接(Linking)阶段的第一个环节是验证,验证的主要目的是检测)ava字节码文件是否遵守了《Java虚拟机规<br>范》中的约束。<font color="#e74f4c"> </font>。<br>主要包含如下四部分,具体详见《Java虚拟机规范》:<br>1.文件格式验证,比如文件是否以OxCAFEBABE开头,主次版本号是否满足当前ava虚拟机版本要求。<br>2.元信息验证,例如类必须有父类(super?不能为空)。<br>3验证程序执行指令的语义,比如方法内的指令执行到一半强行跳转到其他方法中去。<br>4.符号引用验证,例如是否访问了其他类中orivate的方法等。
/除Object类外,所有的类必须有父类;Object类是唯一一个没有父类的类。此外,接口并不继承Object类
准备
给静态变量赋初值
准备阶段为静态变量(static)分配内存并设置初值。final修饰的基本数据类型的静态变量,准备阶段直接会将<br>代码中的值进行赋值。
解析
将常量池中的符号引用替换成指向内存的直接引用
解析阶段主要是将常量池中的符号引用替换为直接引用。<font color="#e74f4c">符号引用就是在字节码文件中使用编号来访问常量池中<br>的内容。直接引用不在使用编号,而是使用内存中地址进行访问具体的数据</font>。
初始化阶段
初始化阶段会执行<font color="#e74f4c">静态代码块中的代码</font>,并<font color="#e74f4c">为静态变量赋值</font>。<br>初始化阶段会执行字节码文件中<font color="#e74f4c">clinit</font>部分的字节码指令。
代码例子
子主题
/这是构造代码块,会放到构造方法里
使用阶段
不解释
卸载阶段
判定一个类可以被卸载。需要同时满足下面三个条件:<br>1、此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。<br>2、加载该类的类加载器已经被回收。<br>3、该类对应的java.lang.Class对象没有在任何地方被引l用。
非常严格,工作场景中极少会遇到卸载的场景,在Tomcat的热部署中才可能会遇到
什么是类加载器
类加载器的作用
类加载器负载在类的加载过程中将字节码信息以流的方式获取并加载到内存中。
JDK8和JDK9类加载器的实现有不同,详见上
启动类加载器
启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的类加载器,JDK9之前使用C++编写的、JDK9之后使用Java编写。<br>默认加载划ava安装目录jre/Iib下的类文件,比如rt.jar,tools,.jar,resources,jar等。
string类核心类由启动类加载器加载,在Java中无法获得启动类加载器,无论JDK8或17
扩展/平台类加载器
扩展类加载器(Extension Class Loader)是)DK中提供的、使用Java编写的类加载器。JDK9之后由于采用了模块化,改名为Platform平台类加载器。<br>默认加载Java安装目录/ire/ib/ext下的类文件
应用程序类加载器
应用程序类加载器(Application Class Loader)是DK中提供的、使用Java编写的类加载器。<br>默认加载为应用程序classpath下的类。<br><br>
自定义类加载器(加分项)
自定义类加载器允许用户自行实现类加载的逻辑,可以从网络、数据库等来源加载类信息。自定义类加载器需要继承自<br>ClassLoader抽象类,<font color="#e74f4c">重写findClass方法</font>。
/基础篇重写loadClass方法是为了打破双亲委派
获取字节码文件中的字节码信息,也就是一个字节数组
什么是双亲委派机制
类加载器和父类加载器
类加载有层级关系,上一级称之为下一级的父类加载器。
什么是双亲委派机制
双亲委派机制指的是:当一个类加载器接收到加载类的任务时,<font color="#e74f4c">会向上查找是否加载过,再由顶向下进行加<br>载</font>。
/看源码应该向上委派,然后依次从上向下加载吧
每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如<br>果已经加载则直接返回,否则会将加载请求委派给父类加载器。
双亲委派机制的作用
1.保证类加载的安全性<br>通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如 java.Lang.String,确保核心类库的完整性和安全性。
2,避免重复加载<br>双亲委派机制可以避免同一个类被多次加载。
如何打破双亲委派机制(衍生)
如何打破双亲委派机制
ClassLoader中包含了4个核心方法
调用关系如下
只有重写loadClass方法才会打破双亲委派机制
自定义类加载器最好还是重写findClass方法,双亲委派机制还是很有用的
ClassLoader中包含了4个核心方法,对va程序员来说,打破双亲委派机制的唯一方法就是<font color="#e74f4c">实现自定义类加载器重写loadClass方法</font>,<br>将其中的双亲委派机制代码去掉。
Tomcat的自定义类加载器
Tomcat中,实现了一套自定义的类加载器。这一小节使用目前应用l比较广泛的Tomcat9(9.0.84)源码进行分析。
common类加载器
common类加载器主要加载tomcat自身使用以及应用使用的jar包,默认配置在<font color="#e74f4c">catalina.properties</font>文件中<br>common.loader="Sfcatalina.baselib""Sfcatalina.basel/lib/*jar"
加载tomcati和应用都使用的类
catalina类加载器
catalina类加载器主要加载tomcat自身使用的jar包,不让应用使用,默认配置在<font color="#e74f4c">catalina.properties</font>,文件中<br>serverloader= 默认配置为空,为空时catalina加载器和commo ni加载器是同一个。
加载tomcat使用的类
shared类加载器
shared类加载器主要加载应用使用的jar包,不让tomcat使用,默认配置在<font color="#e74f4c">catalina.properties</font>文件中.<br>shared.loader= 默认配置为空,为空时shared加载器和common加载器是同一个。
加载应用之间通用的类
ParallelWebappclassLoader类加载器<br>(并行Web应用类加载器)
ParallelWebappclassLoader类加载器可以多线程并行加载应用中使用到的类,每个应用都拥有一个自己的<br>该类加载器。
为什么每个应用会拥有一个独立的ParallelWebappClassLoader类加载器呢?<br><font color="#e74f4c">同一个类加载器,只能加载一个同名的类</font>。两个应用中相同名称的类都必须要加载。
/单例模式
执行流程
右边的打破了双亲委派机制
默认这里打破了双亲委派机制,应用中的类如果没有加载过。会先从当前类加载器加载,然后再交给父类加载器<br>通过双亲委派机制加载。
保证应用程序中的类可以被独立加载,避免相同名字的类加载冲突<br>
每个应用一个,加载本应用的类
加载应用的时候,会优先由自己加载,打破了双亲委派机制,如果自己加载不了,再交给父类加载,执行双亲委派机制<br>
JasperLoader类加载器
JasperLoader类加载器负责加载JSP文件编译出来的class:字节码文件,为了实现热部署(不重启让修改的<br>jsp生效),每一个jsp文件都由一个独立的JasperLoader负责加载。
每个jsp文件一个,加载jsp编译后的类
总结
如何判断堆上的对象有没有被引用
引用计数法
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。
缺点
引用计数法的优点是实现简单,缺点有两点:<br>1每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响<br>2.存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。
/也不是无法回收,只是需要手动把引用释放干净,太麻烦了
可达性分析算法
Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC<br>Root)和普通对象,对象与对象之间存在引用关系。
哪些对象被称之为GC Root对象呢<br>
<font color="#e74f4c">线程Thread对象,引用线程栈帧中的方法参数、局部变量等。<br>系统类加载器加载的java.lang.Class对象,引用类中的静态变量</font>。<br>监视器对象,用来保存同步锁synchronized关键字持有的对象。<br>本地方法调用时使用的全局对象。
可达性分析算法指的是如果从某个到GC Rooti对象是可达的,对象就不可被回收。最<br>常见的是GC Roo对象会引用栈上的局部变量和静态变量导致对象不可回收。
使用可达性分析法原因
JVM中都有哪些引用类型
<font color="#e74f4c">强引用</font>
强引用,VM中默认引用关系就是强引用,即是<font color="#e74f4c">对象被局部变量、静态变量等GCRoot关联的对象引用</font>,只要<br>这层关系存在,普通对象就不会被回收。
/强引用:不会被回收、软引用:内存不足会回收、弱引用:下次垃圾GC时回收
<font color="#e74f4c">软引用</font>
软引用,软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,<font color="#e74f4c">当程序内存不足<br>时,就会将软引用中的数据进行回收</font>。软引用主要在缓存框架中使用。
<font color="#e74f4c">弱引用</font>
弱引用,弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,<font color="#e74f4c">不管内存够不够都<br>会直接被回收</font>,弱引用主要在ThreadLocal中使用。
虚引用
虚引用(幽灵引用/幻影引用),不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回<br>收器回收时可以接收到对应的通知。<font color="#e74f4c">直接内存</font>中为了及时知道直接内存对象不再使用,从而<font color="#e74f4c">回收内存</font>,使用了<br>虚引用来实现。
终结器引用
终结器引用,终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队<br>列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次<br>被回收时,该对象才真正的被回收。
ThreadLocal中为什么要使用弱引用?(衍生)
ThreadLocal中为什么要使用弱引用?
ThreadLocal可以在线程中存放线程的本地变量,保证数据的线程安全。
ThreadLocal中是这样去保存对象的:<br>1、在每个线程中,存放了一个<font color="#e74f4c">ThreadLocalMap对象</font>,本质上就是一个数组实现的哈希表,里边存放多个Entryi对象<br>2、每个<font color="#e74f4c">Entry对象</font>继承自弱引用,内部存放ThreadLocal对象。同时用强引用,引用保存的ThreadLocaly对应的value值。
举例
不再使用Threadlocaly对象时,threadlocal=nul;由于是弱引用,那么在垃圾回收之后,ThreadLocal对象就可以被<br>回收。
此时还有Entry对象和value对象没有能被回收,所以在ThreadLocal类的set、get、remove方法中,在某些特定条件满<br>足的情况下,会主动删除这两个对象。
如果一直不调用set、get、remove方法或者调用了没有满足条件,这部分对象就会出现内存泄漏。强烈建议在<br>ThreadLocal不再使用时,调用remove方法回收将Entry对象的引用关系去掉,这样就可以回收这两个对象了。
总结
当Threadlocal对象不再使用时,使用弱引用可以让对象被回收;因为仅有弱引用没<br>有强引用的情况下,对象是可以被回收的。<br>弱引用并没有完全解决掉对象回收的问题,Entry对象和value值无法被回收,所以合<br>理的做法是手动调用remove方法进行回收,然后再将Threadlocal对象的强引用解除
有哪些常见的垃圾回收算法
垃圾回收算法的机制,优缺点
1960年John McCarthy发布了第一个GC算法:标记-清除算法。<br>1963年Marvin L.Minsky发布了复制算法。<br>本质上后续所有的垃圾回收算法,都是在上述两种算法的基础上优化而来。
子主题
标记清除
标记清除算法的核心思想分为两个阶段:<br>1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出<br>所有存活对象。<br>2清除阶段,从内存中删除没有被标记也就是非存活对象。
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。<br>缺点:1.<font color="#e74f4c">碎片化问题</font><br>由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一<br>个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。<br>2.<font color="#e74f4c">分配速度慢</font>。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才<br>能获得合适的内存空间。
子主题
复制
复制算法的核心思想是:<br>1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。<br>2.在垃圾回收GC阶段,将From中存活对象复制到To空间。<br>3.将两块空间的From和To名字互换。
优点:<font color="#e74f4c">吞吐量高</font><br>复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历的过程,因而性<br>能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动<br>优点:<font color="#e74f4c">不会发生碎片化</font><br>复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
缺点:<font color="#e74f4c">内存使用效率低</font><br>每次只能让一半的内存空间来为创建对象使用
1G的内存只有500M能用
标记整理
标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。<br>核心思想分为两个阶段:<br>1.标记阶段,将所有存活的对象进行标记。Jva中使用可达性分析算法,从GC Root开始通过引用链遍历出<br>所有存活对象。<br>2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
优点:内存使用效率高<br>整个堆内存都可以使用,不会像复制算法只能使用半个堆内存<br>优点:不会发生碎片化<br>在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
子主题
缺点:整理阶段的效率不高<br>整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过Two-<br>Finger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能
分代GC
分代垃圾回收算法
介绍:现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。
年轻代和老年代
虚拟机参数,调整内存区域的大小
/这节课老师讲的很烂
/8:1:1
分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。<br>随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为<br>Minor GC或者Young GC(是复制算法)。<br>Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。
接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。
此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。
注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。<br>当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。
Minor GC也会有STW,只不过比较短,Full GC停顿时间很长
Full GC无法回收掉老年代的对象,那么当对象<br>继续放入老年代时,就会抛出Out Of Memory异常。
为什么分代GC算法要把堆分成年轻代和老年代?
系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收,比如用户获取订单数据,订单数据返回<br>给用户之后就可以释放了。<br>老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。<br>在虚拟机的默认设置中,新生代大小要远小于老年代的大小。
分代GC算法将堆分成年轻代和老年代主要原因有:<br>1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。<br>2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记清除和标记整理<br>算法,由程序员来选择灵活度较高。<br>3、分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(ful<br>gc),STW时间就会减少。
/为了适配不同类型的应用程序
/灵活性强
minor gc尽可能多,full gc尽可能 少
优缺点
程序中大部分对象都是朝生夕死,在年轻代创建并且回收,只有少量对象会长期存活进入老年代。分代垃圾<br>回收的优点有:<br>1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。<br>2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法效率高、不会产生内存碎片,老年<br>代可以选择标记清除和标记整理算法,由程序员来选择灵活度较高。<br>3、分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收<br>(full gc),STW(Stop The World)由垃圾回收引起的停顿时间就会减少。
有哪些常用的垃圾回收器
垃圾回收器是垃圾回收算法的具体实现。<br>由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。
Serial和SerialOld
子主题
PS和PO
子主题
ParNew和CMS
子主题
子主题
缺点:<br>1、CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS会在FuGC时进行碎片的整理。<br>这样会导致用户线程暂停,可以使用-XX:CMSFullGCsBeforeCompaction=N参数(默认O)调整N次Full GC.之<br>后再整理。<br>2.、无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收:<br>3、如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。<br>4、并发阶段会影响用户线程执行的性能
第一个最重要
G1
子主题
Shenandoah和ZGC
Shenandoah是由Red Hat开发的一款低延迟的垃圾收集器,Shenandoah并发执行大部分GC工作,包括并<br>发的整理,堆大小对STW的时间基本没有影响。
ZGC是一种可扩展的低延迟垃圾回收器。ZGC在垃圾回收过程中,STW的时间不会超过一毫秒,适合需要低延<br>迟的应用。支持几百兆到16TB的堆大小,堆大小对STW的时间基本没有影响。
吞吐量可能比较差
垃圾回收器的技术演进
子主题
较好的组合
如何解决内存泄漏问题
内存泄漏和内存溢出
<font color="#e74f4c">内存泄漏</font>(emory leak):在ava中如果不再使用一个对象,但是该对象依然在GC ROOTE的引用链上,这<br>个对象就不会被垃圾回收器回收,这种情况就称之为<font color="#e74f4c">内存泄漏</font>。<br>少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,就像滚雪球雪球越滚越大,不管有多大的内存迟<br>早会被消耗完,最终导致的结果就是<font color="#e74f4c">内存溢出</font>。
解决内存泄漏问题的思路
子主题
发现问题
生产环境通过运维提供的Prometheus+Grafana等监控平台查看<br>开发、测试环境通过visualvm查看
诊断
●当堆内存溢出时,<br>需要在堆内存谥出时将整个堆内存保存下来,生成内存快照(Heap Profile)文件。<br>生成方式有两种
1、内存溢出时自动生成,添加生成内存快照的ava虚拟机参数:<br>-XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件.<br>-XX:HeapDumpPath=<path>:指定hprof文件的输出路径。
2、导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:<br>通过JDK自带的jmap命令导出,格式为:<br>jmap-dump:Live,format=b,file=文件路径和文件名进程ID<br>通过arthas的heapdump命令导出,格式为:<br>heapdump--Live文件路径和文件名
MAT定位问题
使用MAT打开hpof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏<br>的根源。
修复问题
子主题
常用工具
常用JVM工具
常见的JVM参数
-Xmx和-Xms,最大堆内存
-XX:MaxMetaspacesize和-Xss
-Xmn,年轻代大小
打印GC日志
JVM参数模板
0 条评论
下一页