全屋装修设计定制整装,成都网站优化多少钱,软件公司宣传册设计样本,工作单位怎么填文章目录 一. JVM内存划分二. 类加载机制1. 类加载过程2. 双亲委派模型 三. GC垃圾回收机制1. 找到需要回收的内存1.1 哪些内存需要回收#xff1f;1.2 基于引用计数找垃圾(Java不采取该方案)1.3 基于可达性分析找垃圾(Java采取方案) 2. 垃圾回收算法2.1 标记-清除算法2.2 标记… 文章目录 一. JVM内存划分二. 类加载机制1. 类加载过程2. 双亲委派模型 三. GC垃圾回收机制1. 找到需要回收的内存1.1 哪些内存需要回收1.2 基于引用计数找垃圾(Java不采取该方案)1.3 基于可达性分析找垃圾(Java采取方案) 2. 垃圾回收算法2.1 标记-清除算法2.2 标记-复制算法2.3 标记-整理算法2.4 分代回收 一. JVM内存划分
JVM 其实是一个 Java 进程该进程会从操作系统中申请一大块内存区域提供给 Java 代码使用申请的内存区域会进一步做出划分给出不同的用途。
其中最核心的是栈堆方法区这几个区域
堆用来放置 new 出来的对象类成员变量。栈维护方法之间的调用关系放置局部变量。方法区(旧)/元数据区(新)放的是类加载之后的类对象.class文件静态变量二进制指令方法。
细分下来 JVM 的内存区域包括以下几个程序计数器栈堆方法区图中的元数据区可以理解为方法区。
程序计数器内存最小的一块区域保存了下一条要执行的指令字节码的地址每个线程都有一份。
栈储存局部变量与方法之间的调用信息每一个线程都有一份但要注意“栈是线程私有的”这种说法是不准确的私有的意思是我的你是用不了的但实际上一个线程栈上的内容是可以被另一个线程使用到的。 栈在 JVM 区域划分中分为两种一种是 Java 虚拟机栈另外一种是本地方法栈这两种栈功能非常类似当方法被调用时都会同步创建栈帧来存储局部变量表、操作数栈、动态连接、方法出口等信息。
只不过虚拟机栈是为虚拟机执行 Java 方法也就是字节码服务而本地方法栈则是给 JVM 内部的本地Native方法服务的JVM 内部通过 C 代码实现的方法。
堆储存对象以及对象的成员变量一个 JVM 进程只有一个多个线程共用一个堆是内存中空间最大的区域Java 堆是垃圾回收器管理的内存区域后文介绍 GC 的时候细说。
方法区 JDK 1.8 开始叫做元数据区存储了类对象常量池静态成员变量即时编译器编译后的代码缓存等数据所谓的“类对象”就是被static修饰的变量或方法就成了类属性.java文件会被编译成.class文件.class会被加载到内存中也就被 JVM 构造成类对象了类对象描述了类的信息如类名类有哪些成员每个成员叫什么名字权限是什么方法名等同样一个 JVM 进程只有一个元数据区多个线程共用一块元数据区内存。 要注意 JVM 的线程和操作系统的线程是一对一的关系每次在 Java 代码中创建的线程必然会在系统中有一个对应的线程。
二. 类加载机制
1. 类加载过程
类加载就是把.java文件使用javac编译为.class文件从文件硬盘被加载到内存中元数据区得到类对象的过程。程序要想运行就需要把依赖的“指令和数据”加载到内存中。 这个图片所示的类加载过程来自官方文档类加载包括三个步骤Loading Linking Initialization。 官方文档https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
下面就来了解一下这三步是在干什么
第一步加载Loading找到对应的.class文件打开并读取文件到内存中同时通过解析文件初步生成一个代表这个类的 java.lang.Class 对象。
第二步连接Linking作用是建立多个实体之间的联系该过程有包含三个小过程
验证Verification主要就是验证读取到的内容是不是和规范中规定的格式完全匹配如果不匹配那么类加载失败并且会抛出异常一个.class文件的格式如下通过观察.class文件结构其实.class文件把.java文件的核心信息都保留了下来只不过是使用二进制的方式重新进行组织了.class文件是二进制文件这里的格式有严格说明的哪几个字节表示什么java官方文档都有明确规定。 来自官方文档https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1准备Preparation给类对象分配内存空间先在元数据区占个位置并为类中定义的静态变量分配内存此时类变量初始值也就都为 0 值了。解析Resolution针对字符串常量初始化将符号引用转为直接引用字符串常量得有一块内存空间存这个字符的实际内容还得有一个引用来保存这个内存空间的起始地址在类加载之前字符串常量是在.class文件中的此时这个引用记录的并非是字符串常量真正的地址而是它在文件的偏移量/占位符符号引用也就是说此时常量之间只是知道它们彼此之间的相对位置不知道自己在内存中的实际地址在类加载之后才会真正的把这个字符串常量给填充到特定的内存地址上中这个引用才能被真正赋值成指定内存地址直接引用此时字符串常量之间相对位置还是一样的这个场景可以想象你看电影时拿着电影票入场入座。
第三步初始化Initialization这里是真正地对类对象进行初始化特别是静态成员调用构造方法进行成员初始化执行代码块静态代码块加载父类…
类加载的时机
类加载并不是 Java 程序JVM一运行就把所有类都加载了而是真正用到哪个类才加载哪个整体是一个“懒加载”的策略只有需要用的时候才加载非必要不加载就会触发以下的加载
构造类的实例调用这个类的静态方法/使用静态属性加载子类就会先加载其父类
一旦加载过后后续使用就不必加载了。
2. 双亲委派模型
双亲委派模型是类加载中的一个环节属于加载阶段它是描述如何根据类的全限定名找到.class文件的过程。
在 JVM 里面提供了一组专门的对象用来进行类的加载即类加载器当然既然双亲委派模型是类加载中的一部分所以其所描述找.class文件的过程也是类加载器来负责的。
但是想要找全.class文件可不容易毕竟.class文件可能在 jdk 目录里面可能在项目的目录里面还可能在其他特定的位置因此 JVM 提供了多个类加载器每一个类加载器负责在一个片区里面找。
默认的类加载器主要有三个:
BootStrapClassLoader负责加载 Java 标准库里面的类如 StringRandomScanner 等。ExtensionClassLoader负责加载 JVM 扩展库中的类是规范之外由实现 JVM 的组织Sun/Oracle提供的额外的功能。ApplicationClassLoader负责加载当前项目目录中自己写的类以及第三方库中的类。
除了默认的几个类加载器程序员还可以自定义类加载器来加载其他目录的类此时也不是非要遵守双亲委派模型如 Tomcat 就自定义了类加载器用来专门加载webapps目录中的.class文件就没有遵守。 双亲委派模型就描述了类加载过程中的找目录的环节它的过程如下
如果一个类加载器收到了类加载的请求首先需要先给定一个类的全限定类名如“java.lang.String”。
根据类的全限定名找的过程中它不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成每一个层次的类加载器都是如此。
因此所有的加载请求最终都应该传送到顶层的启动类加载器中只有当父加载器反馈自己无法完成这个加载请求它的搜索范围中没有找到所需的类时子加载器才会尝试自己去加载去自己的片区搜索。
举个例子我们要去找标准库里面的String.class文件它的过程大致如下
首先ApplicationClassLoader类收到类加载请求但是它先询问父类加载器是否加载过即询问ExtensionClassLoader类是否加载过。如果ExtensionClassLoader类没有加载过请求就会向上传递到ExtensionClassLoader类然后同理询问它的父加载器BootstrapClassLoader是否加载过。如果BootstrapClassLoader没有加载过则加载请求就会到BootstrapClassLoader加载器这里由于BootstrapClassLoader加载器是最顶层的加载器它就会去标准库进行搜索看是否有String类我们知道String是在标准库中的因此可以找到请求的加载任务完成这个过程也就结束了。 再比如这里要加载我自己写的的Test类过程如下
首先ApplicationClassLoader类收到类加载请求但是它先询问父类加载器是否加载过即询问ExtensionClassLoader类是否加载过。如果ExtensionClassLoader类没有加载过请求就会向上传递到ExtensionClassLoader类然后同理询问它的父加载器BootstrapClassLoader是否加载过。如果BootstrapClassLoader没有加载过则加载请求就会到BootstrapClassLoader加载器这里由于BootstrapClassLoader加载器是最顶层的加载器它就会去标准库进行搜索看是否有Test类我们知道Test类不在标准库所以会回到子加载器里面搜索。同理ExtensionClassLoader加载器也没有Test类会继续向下到ApplicationClassLoader加载器中寻找由于ApplicationClassLoader加载器搜索的就是项目目录因此可以找到Test类全过程结束。
如果在ApplicationClassLoader还没有找到就会抛出异常。 总的来说双亲委派模型就是找.class文件的过程其实也没啥就是名字挺哄人。 之所以有上述的查找顺序大概是因为 JVM 代码是按照类似于递归的方式来实现的就导致了从下到上又从上到下过程这个顺序最主要的目的就是为了保证 Bootstrap 能够先加载Application 能够后加载这就可以避免说因为用户创建了一些奇怪的类引起不必要的 bug。
三. GC垃圾回收机制
在 C/C 中内存空间是需要进行手动释放如果没有手动去释放那么这块内存空间就会持续存在一直到进程结束并且堆的内存生命周期比较长不像栈随着方法执行结束自动销毁释放堆默认是不能自动释放的这就可能导致内存泄露的问题进一步导致后续的内存申请操作失败。
而在 Java 中引入了 GC 垃圾回收机制垃圾指的是我们不再使用的内存垃圾回收就是把我们不用的内存自动释放了。
GC的好处
非常省心使程序员写代码更简单一些不容易出错。
GC的坏处
需要消耗额外的系统资源也有额外的性能开销。GC 这里还有一个严重的 STWstop the world问题如果有时候内存中的垃圾已经很多了这个时候触发一次 GC 就会消耗大量系统资源其他程序可能就无法正常执行了GC 可能会涉及一些锁操作就可能导致业务代码无法正常执行极端情况下可会卡顿几十毫秒甚至上百毫秒。
GC 的实际工作过程包含两部分
找到/判定垃圾。再进行垃圾的释放。
1. 找到需要回收的内存
1.1 哪些内存需要回收
Java 程序运行时内存分为四个区分别是程序计数器栈堆方法区。 对于程序计数器它占据固定大小的内存它是随着线程一起销毁的不涉及释放那么也就用不到 GC对于栈空间函数执行完毕对应的栈帧自动销毁释放了也不需要 GC对于方法区主要进行类加载虽然需要进行“类卸载”此时需要释放内存但是这个操作的频率是非常低的最后对于堆空间经常需要释放内存GC 也是主要针对堆进行释放的。
在堆空间内存的分布有三种一是正在使用的内存二是不用了但未回收的内存三是未分配的内存那内存中的对象也有三种情况对象内存全部在使用相当于对象整体全部在使用对象的内存部分在使用相当于对象的一部分在使用对象的内存不使用对象也就使用完毕了对于这三类对象前两类不需要回收只有最后一类是需要回收的。 所以垃圾回收的基本单位是对象而不是字节对于如何找到垃圾常用有引用计数法与可达性分析法两种方式关键思路是抓住这个对象看看到底有没有“引用”指向它没有引用了它就是需要被释放的垃圾。
1.2 基于引用计数找垃圾(Java不采取该方案)
所谓基于引用计数判断垃圾就是给每一个对象分配一个计数器整数来记录该对象被多少个引用变量所指每次创建一个引用指向该对,计数器就1每次该引用被销毁了计数器就–1如果这个计数器的值为0则表示该对象需要回收比如有一个Test对象它被三个引用所指所以这个 Test 对象所带计数器的值就是3。
//伪代码
Test t1 new Test();
Test t2 t1;
Test t3 t1;如果上述的伪代码是在一个方法中待方法执行完毕方法中的局部引用变量被销毁那么Test对象的引用计数变为0此时就会被回收。
由此可见基于引用计数的方案非常简单高效并且可靠但是它拥有两个致命缺陷
内存空间浪费较多利用率低 需要给每个对象分配一个计数器如果按照4个字节来算代码中的对象非常少时无所谓但如果对象特别多了占用的额外空间就会很多尤其是每个对象都比较小的情况下。存在循环引用的问题会出现对象既不使用也不释放的情况看下面举例子来分析一下。
有以下一段伪代码
class Test {Test t null;
}//main方法中:
Test t1 new Test(); // 1号对象, 引用计数是1
Test t2 new Test(); // 2号对象, 引用计数是1
t1.t t2; // t1.t指向2号对象, 此时2号对象引用计数是2
t2.t t1; // t1.t指向1号对象, 此时1号对象引用计数是2执行上述伪代码运行时内存图如下 然后我们把变量t1与t2置为null伪代码如下
//伪代码:
t1 null;
t2 null;执行完上面伪代码运行时内存图如下 此时 t1 和 t2 引用销毁了一号对象和二号对象的引用计数都-1但由于两个对象的属性相互指向另一个对象计数器结果都是1而不是0造成对象无法及时得到释放而实际上这个两个对象已经获取不到了应该销毁了。
1.3 基于可达性分析找垃圾(Java采取方案)
Java 中的对象都是通过引用来指向并访问的一个引用指向一个对象对象里的成员又指向别的对象。
所谓可达性分析就是通过额外的线程将整个 Java 程序中的对象用链式/树形结构把所有对象串起来从根节点出发去遍历这个树结构所有能访问到的对象标记成“可达”不能访问到的就是“不可达”JVM 有一个所有对象的名单每 new 一个对象JVM 都会记录下来JVM 就会知道一共有哪些对象每个对象的地址是什么通过上述遍历将可达的标记出来剩下的不可达的未标记的就可以作为垃圾进行回收了。
可达性分析的起点称为GC Roots就是一个Java对象一个代码中有很多这样的起点把每个起点都遍历一遍就完成了一次扫描。
对于这个GCRoots一般很难被回收它来源可以分为以下几种
在虚拟机栈栈帧中的本地变量表中引用的对象例如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。在本地方法栈中 JNI即通常所说的Native方法引用的对象。常量池中引用所指向的对象。方法区中静态成员所指向的对象。所有被同步锁synchronized 关键字持有的对象。
可达性分析克服了引用计数的两个缺点但它有自己的问题
需要进行类似于 “树遍历”的过程消耗更多的时间但可达性分析操作并不需要一直执行只需要隔一段时间执行一次寻找不可达对象确定垃圾就可以所以慢一下点也是没关系的虽迟但到。可达性分析过程当前代码中的对象的引用关系发生变化了还比较麻烦所以为了准确的完成这个过程就需要让其他的业务暂停工作STW问题但 Java 发展这么多年垃圾回收机制也在不断的更新优化STW 这个问题现在已经能够比较好的应对了虽不能完全消除但也已经可以让 STW 的时间尽量短了。
2. 垃圾回收算法
垃圾回收的算法最常见的有以下几种
标记-清除算法标记-复制算法标记-整理算法分代回收算法本质就是综合上述算法在堆的不同区采取不同的策略
2.1 标记-清除算法
标记其实就是可达性分析的过程在可达性分析的过程中会标记可达的对象其不可达的对象都会被视为垃圾进行回收。
比如经过一轮标记后标记状态和回收后状态如图 我们发现内存是释放了但是回收后未分配的内存空间是零散的不是连续的我们知道申请内存的时候得到的内存得是连续的虽然内存释放后总的空闲空间很大但由于未分配的内存是碎片化的就有可能申请内存失败假设你的主机有 1GB 空闲内存但是这些内存是碎片形式存在的当申请 500MB 内存的时候也可能会申请失败毕竟不能保证有一块大于 500MB 的连续内存空间这也是标记-清除算法的缺陷内存碎片问题。
2.2 标记-复制算法
为了解决标记-清除算法所带来的内存碎片化的问题引入了复制算法。
它将可用内存按容量划分为大小相等的两块每次只使用其中的一块每次清理就将还存活着的对象复制到另外一块上面然后再把已使用过的这一块内存空间一次清理掉。
复制算法的第一步还是要通过可达性分析进行标记得到哪一部分需要进行回收哪一部分需要保留不能回收。
标记完成后会将还在使用的内存连续复制到另外一块等大的内存上这样得到的未分配内存一直都是连续的而不是碎片化的。 但是复制算法也有缺陷
空间利用率低。如果垃圾少有效对象多复制成本就比较大。
2.3 标记-整理算法
标记-整理算法针对复制算法做出进一步改进其中的标记过程仍然与“标记-清除”算法一致但后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都向内存空间一端移动然后直接清理掉边界以外的内存。 回收时是将存活对象按照某一顺序比如从左到右从上到下的顺序拷贝到非存活对象的内存区域类似于顺序表的删除操作会将后面的元素搬运到前面。 解决了标记-复制算法空间利用率低的问题也没有内存碎片的问题但是复制的开销问题并没有得到解决。
2.4 分代回收
上述的回收算法都有一定的缺陷分代回收就是将上述三种算法结合起来分区使用分代回收会针对对象进行分类以熬过的 GC 扫描轮数作为“年龄”然后针对不同年龄采取不同的方案。
分代是基于一个经验规律如果一个东西存在时间长了那么接下来大概率也会存在要没有早就没有了。
我们知道 GC 主要是回收堆上的无用内存我们先来了解一下堆的划分堆包括新生代Young、老年代Old而新生代包括一个伊甸区Eden与两个幸存区Survivor分代回收算法就会根据不同的代去采取不同的标记-xx算法。 在新生代包括一个伊甸区与两个幸存区伊甸区存储的是未经受 GC 扫描的对象年龄为 0也就是刚刚 new 出来的对象。
幸存区存储了经过若干轮 GC 扫描的对象通过实际经验得出大部分的 Java 对象具有“朝生夕灭”的特点生命周期非常短也就是说只有少部分的伊甸区对象才能熬过第一轮的 GC 扫描到幸存区所以到幸存区的对象相比于伊甸区少的多正因为大部分新生代的对象熬不过 GC 第一轮扫描所以伊甸区与幸存区的分配比例并不是1:1的关系HotSpot 虚拟机默认一个 Eden 和一个 Survivor 的大小比例是 8∶1正因为新生代的存活率较小所以新生代使用的垃圾回收算法为标记-复制算法最优毕竟存活率越小对于标记-复制算法复制的开销也就很小。
不妨我们将第一个 Survivor 称为活动空间第二个 Survivor 称为空闲空间一旦发生 GC会将 10% 的活动区间与另外 80% 伊甸区中存活的对象复制到 10% 的空闲空间接下来将之前 90% 的内存全部释放以此类推。
在后续几轮 GC 中幸存区对象在两个 Survivor 中进行标记-复制算法此处由于幸存区体积不大浪费的空间也是可以接受的。
在继续持续若干轮 GC 后这个对象已经再两个幸存区中来回考贝很多次了幸存区的对象就会被转移到老年代老年代中都是年龄较老的对象根据经验一个对象越老继续存活的可能性就越大要挂早挂了因此老年代的 GC 扫描频率远低于新生代所以老年代采用标记-整理的算法进行内存回收毕竟老年代存活率高对于标记-整理算法复制转移的开销很低。
还要注意一个特殊情况如果对象非常大就直接进入老年代因为大对象进行复制算法成本比较高而且大对象也不会很多。