网站建设合同违约,合肥专业网站优化价格,php网站开发工程师,汽车之家官网网页版目录 一、JVM中的内存区域划分
二、JVM的类加载机制
1、类加载的触发时机
2、双亲委派模型
1.1、向上委派
1.2、向下委派
三、JVM中的垃圾回收机制#xff08;GC#xff09;
1、确认垃圾
1.1、引用计数#xff08;Java实际上没有使用这个方案#xff0c;但是Pytho…目录 一、JVM中的内存区域划分
二、JVM的类加载机制
1、类加载的触发时机
2、双亲委派模型
1.1、向上委派
1.2、向下委派
三、JVM中的垃圾回收机制GC
1、确认垃圾
1.1、引用计数Java实际上没有使用这个方案但是Python、PHP采用了
1.1.1、循环引用
1.2、可达性分析被Java采用
2、释放内存
2.1、标记清除
2.2、复制算法
2.3、标记整理
2.4、分代回收 一、JVM中的内存区域划分
JVM就是一个Java进程Java进程会从操作系统中申请一大块内存区域给Java代码使用。这里申请的一大块内存区域又被进一步划分给出了不同的用途。 堆存放new出来的对象也就是说对象中成员变量在堆上栈用来维护方法之间的调用关系也就是说方法中的局部变量在栈上方法区Java8之前的叫法/元数据区Java8开始的叫法存放的是类加载之后的类对象类名.class。也就是存在被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据的。 ❌❌这里需要明确的一点我们在网上看到的有些资料中说到内置类型的变量是在栈上引用类型的变量是在栈上。这个说法是错误的。 这里变量在堆上还是在栈上与这个变量是什么类型无关如果你在类中定义了一个内置类型的变量通过new关键字创建这个类的对象那么这个内置类型的变量就是在堆上。判断一个变量在堆上还是在栈上需要关注的是这个变量是局部变量还是在成员变量如果是局部变量则这个变量在栈上。如果是成员变量则这个变量在堆上。 public class Test{public int a;public String b;
}
class Test1{public static void main(String[] args){Test s new Test();}
}
//通过上述的代码可以看见定义的int类型的变量a是一个内置类型的变量,但是他是一个成员变量所以它在堆上。
//在main方法中的s变量是对象的引用他是一个引用类型的变量他在main方法内部是一个局部变量所以变量s在栈上。 虚拟机栈早期也叫Java栈每个线程在创建时都会创建一个虚拟机栈其内部保存一个个的栈帧对应这一次次的Java方法调用。虚拟机栈是给Java代码使用的。主管Java程序的运行它保存方法的局部变量。部分结果并参与方法的调用和返回。本地方法栈是给jvm内部的本地方法使用的。jvm内部是通过C代码实现的方法。程序计数器是用来记录当前程序执行到那个字节码指令了。 堆和元数据区(方法区)在一个JVM进程中只有一份。但是栈本地方法栈和虚拟机栈和程序计数器则是存在多份每个线程中都会存在一份栈和程序计数器。 二、JVM的类加载机制
把.class文件加载到内存得到类对象的过程称为类加载。因为程序要执行就需要把依赖的指令和数据加载到内存中。类加载的过程有5个步骤加载、验证、准备、解析、初始化。
1️⃣加载阶段Loading
这个阶段是找到.class文件并且读取文件内容。这里找class文件的方法中最常问到的是双亲委派模型。
2️⃣验证阶段Verification
这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
3️⃣准备阶段Preparation
给类对象分配内存空间这里分配的内存空间是未初始化的空间内存空间中的数据是全0的。
4️⃣解析阶段Resolution
针对字符串常量进行初始化。也就是Java虚拟机将常量池内的符号引用替换为直接引用的过程。 符号引用字符串常量本身在class文件中存在但是因为他是在文件中还没有在内存中并不直到自己的地址只能使用一些特殊符号来占位。这就得到了符号引用。直接引用将字符串常量加载到了内存中就会把字符串常量填充到内存中的特定地址上但是字符串在内存中的相对位置与class文件中相对位置还是不变的。 5️⃣初始化阶段Initialization
针对类对象进行初始化。初始化静态成员变量、执行静态代码块、如果一个类存在父类还需要加载父类。
1、类加载的触发时机
类加载不是JVM一启动就会把所有的.class文件都在加载了他是一个懒加载的策略也就是非必要不加载。
只有在下面的情况下才会触发类加载 创建了这个类的实例这个时候就会触发类加载虽然没有创建这个类的实例但是使用了这个类的静态方法或者静态属性就会触发类加载使用子类会触发父类的加载 2、双亲委派模型
双亲委派模型正对的是Java虚拟机中三个类加载器的这三个加载器分别是 启动类加载器BootStrap ClassLoader:负责加载Java标准库中的类扩展类加载器Extension ClassLoader:负责加载一些非标准库的类是由Sun/Oracle扩展的库的类。因为Java最初是是属于Sun公司的但是最后被Oracle公司收购应用程序类加载器Application ClassLoader:负责加载项目中自己写的类以及第三方库中的类。 当具体加载一个类的时候它的过程是需要先给定一个类的权限定类名java.lang.String(字符串)。然后是向上委派在然后是向下委派
1.1、向上委派
然后从Application ClassLoader这个类加载器开始收到类加载的请求后不会自己记载这个类而是把这个类加载请求向上委派给它的父类去完成父类收到这个请求后又继续向上委派给自己的父类一次类推直到所有的请求委派到启动类加载器中。 1.2、向下委派
BootStrap ClassLoader接收到类加载请求后先是在自己负责的范围内查找如果搜索到就直接进行后续加载步骤验证、准备、解析、初始化如果没有搜索到这个类父类会把这个信息返回交给孩子Extension ClassLoader来处理直到这个请求被成功加载但是一直到自定义加载器都没有找到JVM就会抛出ClassNotFund异常 三、JVM中的垃圾回收机制GC
JVM的垃圾回收机制是帮助程序员自动释放内存的。内存释放这是一个比较难把控的事情因为申请内存的时机是明确的使用到了就必须申请释放的时候是模糊的彻底不使用了才能释放。这就特别依赖程序员的水平就像C/C程序员他们释放内存就需要自己手动释放。但是Java通过JVM自动判定基于一系列策略就可以让这个准确性比较高。这里我们说的释放内存空间指的就是释放堆上的申请的空间。
垃圾回收主要分为两个阶段 确认垃圾找没有被引用的对象释放垃圾将找到的对象释放掉 如何判断一个对象是否有引用指向这里有两个方法引用计数和可达性分析。
1、确认垃圾
1.1、引用计数Java实际上没有使用这个方案但是Python、PHP采用了 ✨这个引用计数的方法存在两个缺陷。 浪费内存空间空间利用率不高引用计数占用内存空间。存在循环引用的情况 会导致引用计数的判断逻辑出错 1.1.1、循环引用
当new了两个对象正常情况下两个对象的引用各自指向引用的对象但是这个两个对象中的成员变量相互指向对方对象这个时候两个对象中的计数器在各自加1. 这个时候t和t1不在指向之前的引用两个对象的引用计数都减1但是这两个对象的引用计数并没有被清空实际上这两个对象已经不被使用了但是无法被当作垃圾无法释放。这两个对象也无法再次被使用想要使用一个对象就需要找到另一个对象这就在逻辑上形成了循环。
1.2、可达性分析被Java采用
把对象之间的引用关系理解成了一个树形结构从一些特殊的起点出发进行遍历只要能遍历访问到的对象就是可达的再把不可达的当作垃圾即可。 ✨可达性分析的关键要点进行上述遍历需要有起点被称为gcroots 栈上的局部变量每个栈的每个局部变量都是起点常量池中引用的对象方法区中静态成员引用的对象 可达性分析总的来所就是从所有的gcroots的起点出发看看该对象里又通过引用能访问那些对象依次遍历把所有可以访问的对象都给遍历一遍遍历的同时把对象标记成可达剩下的遍历不到的对象就是不可达.
可达性分析的特点可达性分析克服了引用计数的两个缺点但是也有自己的问题 消耗的时间更多因此某个对象成了垃圾也不一定能第一时间发现因为扫描的过程需要消耗时间在进行可达性分析的时候依次遍历一旦这个过程中当前代码中的对象引用关系发生了变化这就会使情况变得更加复杂。比如当一个对象指向下一个对象刚遍历完这个对象这个对象的引用变了。因此我们为了更准确的遍历需要让其他的业务线程暂停工作STW问题。 2、释放内存
这里存在三个典型的策略 标记清除复制算法标记整理分代回收 前三种释放内存的方式都存在一定的缺点所以实际上JVM的实现思路是结合了上述的几种思想方法。
2.1、标记清除
这种策略就是直接把垃圾对象的内存释放但是这个方式的缺点就是会产生内存碎片。 我们从内存中申请空间的时候都是整块的连续的空间现在这里空闲的空间是离散的独立的空间总的空间可能很大。假如总的空闲的空间可能超过了1G但是你想申请500MB可能都不一定申请到。
2.2、复制算法
为了解决内存碎片的问题又引入了复制算法。复制算法十八整个内存空间分成两半一次只用一半。 复制算法解决了内存碎片化的问题但是也有缺点 内存利用率比较低如果当前的对象大部分都是要留的垃圾很少此时复制成本就比较高 2.3、标记整理 这个方法也可以结局内存碎片化的问题但是他的搬运开销还是比较大的。
2.4、分代回收
针对不同的情况使用不同的策略。给对象设定年龄这样的概念当然这个单位并不是年龄这里用年龄作为类比描述了这个对象存在多久了如果一个对象刚创建认为是0岁没经过一轮扫描可达性分析没有被标记成垃圾这个时候对象就涨一岁通过这个年龄来区分这个对象的存活时间。
根据年龄的大小来区分采用什么样的策略 新城建的对象放到伊甸区当垃圾回收扫描到伊甸区之后绝大部分对象都会在第一轮GC中被回收如果伊甸区的对象在第一轮的GC中没有被回收就会通过复制算法拷贝到幸存区幸存区分成了大小均等的两部分一次只使用其中的一半。垃圾回收扫描幸存区的对象发现垃圾通过复制算法将不是垃圾的对象复制到生存区的另一半空间中然后将前一半的空间全部释放。当这个对象在生存区经过了若干轮GC之后年龄增长到一定的程度就会通过复制算法拷贝到到年代。进入老年代的对象年龄都很大了在消亡的概率就比前面的新生代中的对象小了很多针对老年代GC的扫描频次就会降低很多。如果老年代中出现了垃圾对象就会使用标记整理的算法进行清理因为老年代中消亡的对象就很少了所以使用标记整理的算法开销还不是很大。特殊情况如果对象非常大直接进入老年代大对象进行复制算法成本比较高而且大对象也不是很多 根据经验规律如果一个对象存活的时间很长了他将继续存在更长的时间。