【Java SE】认识JVM:内存区域划分、类加载机制与GC
文章目录一、JVM基础与内存划分1.1 什么是JVM1.2 JVM完整执行流程1.3 内存区域划分二、类加载机制2.1 类完整生命周期2.2 双亲委派模型类加载器双亲委派模型过程三、垃圾回收GC原理3.1 GC工作流程1. 找到垃圾1引用计数算法2可达性分析2. 释放垃圾3.2 垃圾收集器一、JVM基础与内存划分1.1 什么是JVMJVM全称Java Virtual MachineJava虚拟机是一套软件模拟的虚拟计算机隔离底层操作系统实现Java一次编译到处运行。1.2 JVM完整执行流程Java程序从.java到CPU执行分为四个模块协同工作类加载子系统 ClassLoader读取class字节码文件加载进内存运行时数据区 Runtime Data Area存放程序运行期间所有内存数据执行引擎 Execution Engine解析/编译字节码转为操作系统指令本地接口 Native Interface调用C/C本地库完成底层操作。完整流程.java编译→.class字节码文件 → 类加载器加载至运行时数据区 → 执行引擎解析字节码 → 调用本地方法接口操作系统资源CPU执行。1.3 内存区域划分在真实的操作系统中对于进程的地址空间进行了功能区域划分JVM仿照真实的机器和真实的操作系统进行设计。JVM从操作系统中申请到了一些内存空间供JVM进程的代码使用。JVM内存空间的划分就是把JVM自身申请到的内存空间再把内存按不同功能进行划分。类比把一栋大楼分成若干层每一层细致划分。核心区域1. 程序计数器记录当前线程执行字节码行号执行Java方法存指令地址这里的程序寄存器指内存上的空间与计组中的程序寄存器不同计组中的程序寄存器是在CPU上的唯一特性JVM规范中唯一不会发生OOM的内存区域线程私有多线程切换时依靠PC寄存器恢复执行位置。2. 元数据区保存当前类被加载好的数据.java文件经过javac编译变成.class文件想要运行这个文件就需要把字节码文件加载到内存中也就是元数据区。元信息就是指属性例如类名称访问权限继承自哪些类或实现了哪些接口方法名称参数返回值等。Java8之前元数据区也称为”方法区“3. 栈保存方法调用关系每次调用方法时都会进入方法内部当方法调用完毕返回调用位置程序继续向后执行。这里的栈与数据结构中的栈逻辑类似都具有后进先出的特性。与操作系统中的栈联系JVM进程是C代码编写的程序本身就是存在一系列C中的方法调用。维护这些C方法的栈就是操作系统原生的栈。这些方法调用构成了JVM虚拟机程序。通过这些C代码解释执行.class文件中的字节码这个过程又涉及到了Java方法调用就构造出了Java的栈栈区的两种异常StackOverflow栈所占用的空间只有几十MB如果递归函数出口逻辑错误就有可能导致大量方法调用但是不返回栈深度超出范围造成栈溢出。OOM多线程大量创建线程虚拟机无法分配栈内存。4. 堆保存对象实例是JVM中最大的空间区域假设创建一个对象实例Test t new Test()如果t是局部变量t保存在栈中函数调用完毕就会销毁如果t是成员变量t保存在堆中如果t是静态成员变量t保存在元数据区随类加载创建new Test()一定保存在堆上如果堆上的对象不再使用就会被释放也就是垃圾回收的工作。元数据区和堆整个Java进程共用一份程序计数器和栈一个进程中可能有多份每个线程各一份。二、类加载机制2.1 类完整生命周期加载 → 连接验证、准备、解析 → 初始化 → 使用 → 卸载前3阶段合称类加载过程。加载 Loading找到.class文件根据类的全限定名包名类名例如java.lang.String打开文件读取文件内容到内存中验证 Verification解析校验.class文件合规性防止恶意字节码破坏虚拟机并把.class里的内容转换为结构化的数据也就是符合虚拟机规范的格式准备 Preparation给类对象申请内存空间此时的空间相当于”全0“的状态解析 Resolution初始化字符串常量。字符串常量本身就包含在.class中解析阶段就要把解析出的字符串常量放到内存中元数据区的常量池初始化 Initialization针对类对象进行最后的初始化。为类对象各种属性进行填充包括静态成员变量。如果这个类有父类也会触发父类的加载。一个进程中一个类的加载只会触发一次并不是Java程序一启动就会加载所有的类类加载也是懒汉模式懒加载的一种体现。触发类加载的时机构造这个类的实例调用类的静态属性或静态方法使用某个类时如果父类还没加载也会触发父类的加载2.2 双亲委派模型类加载器JVM中有专门的模块负责类加载默认提供了三种类加载器BootstrapClassLoaderExtensionClassLoaderAplicationClassLoaderAplicationClassLoader中的parent引用指向ExtensionClassLoaderExtensionClassLoader中的parent引用指向BootstrapClassLoaderBootstrapClassLoader中的parent引用指向null构成了类似链表关系。这三个类加载器主要作用都是找.class文件各自负责找的目录不同BootstrapClassLoader负责找Java标准库中的类ExtensionClassLoader负责找Java扩展库中的类。扩展可以认为JVM厂商对于Java的库做的扩充AplicationClassLoader负责找第三方库/当前项目。当前学习过程中通过Maven下载的类都属于第三方库双亲委派模型过程进行类加载时通过全限定名找.class文件时就会从AplicationClassLoader作为入口开始把“加载类”这样的任务委托给它的父亲ExtensionClassLoader来执行ExtensionClassLoader也不会立即干活先把任务委托给它的父亲AplicationClassLoader来执行AplicationClassLoader也想踢足球但是它的parent为空只能自己进行类加载从标准库中找是否匹配的.class文件。如果没找到就把任务还给孩子ExtensionClassLoaderExtensionClassLoader在扩展库中继续找.class文件找到就加载没找到就继续还给孩子AplicationClassLoaderAplicationClassLoader继续负责找.class文件找到就加载没找到就抛出ClassNotFount异常也可以自定义类加载器参与到双亲委派模型中双亲委派模型两大优势避免类重复加载安全防护防止开发者自定义java.lang.Object篡改核心API。三、垃圾回收GC原理GC仅关注堆、方法区虚拟机栈、本地方法栈、程序计数器生命周期随线程自动释放无需回收。3.1 GC工作流程GC主要做两个工作找到垃圾不再使用的对象释放垃圾释放对应的内存1. 找到垃圾1引用计数算法每个对象在创建的的时候就会分配一小块内存空间保存一个整数表示有多少个引用指向它每次进行引用赋值的时候都会触发引用计数的修改。在Java中使用对象一定要通过引用实现当引用计数为0时说明没有引用指向这个对象了这样就可以判断这个对象就是垃圾缺陷内存消耗更多尤其对象本身较小时引用计数消耗的占比就更大可能出现循环引用举个例子classTest{Testtnull;}classMain{publicstaticvoidmain(String[]args){TestanewTest();//计数器1TestbnewTest();//计数器1a.tb;//计数器1b.ta;//计数器1}}此时两个对象实例各有两个引用指向如果把a和b都置为null程序计数器各-1不为0但是都不能被调用。PHPPython都采用计数算法搭配其他方案解决循环引用问题2可达性分析以代码中特定的对象作为遍历的起点也称为GCRoots进行遍历来判定某个对象是否能被访问到到。每遍历一个对象就标记为“可达”剩余没有被标记的就认为是不可达就是接下来要回收的垃圾了。可作为GC Roots的对象栈上的局部变量引用类型常量池中引用指向的对象静态成员引用类型2. 释放垃圾垃圾清除有四种算法标记-清除把垃圾对象的内存直接进行释放缺陷会产生“内存碎片问题”很难找到大块的连续的内存空间复制算法把内存分为两部分起始都保存在A区域当垃圾回收时把不释放的数据复制到B区域再把A区域整体释放。这样就确保空闲内存都是连续的缺陷内存空间利用率低直接砍半如果不需要释放的数据很多复制开销很大标记-整理类似于顺序表删除把不需要释放的数据重新搬运整理缺陷内存搬运数据的操作开销也很大只解决了空间成本问题。分代收集综合前三种方案。“代”指的是对象的年龄单位是GC的轮次。每个对象初始年龄是0每经过一轮GC可达性分析不是垃圾的对象年龄1根据年龄把对象分为新生代和老年代。经过统计发现如果一个对象是新生儿大概率死的很快如果一个对象是老油条大概率能活很长时间。对象在新生代反复被GC筛选年龄增长进入老年代。堆区域划分新生代 Young新建对象存放98%对象朝生夕死。新生代进一步划分Eden 伊甸园80%新生代是所有新对象诞生地由伊甸园到幸存区采用复制算法因为绝大部分对象都活不过一轮GC复制对象少开销可控S0/S1 Survivor幸存者各10%。幸存区的对象也要经历GC扫描每一轮GC消灭一大批对象然后通过复制算法把存活的对象复制到另一个幸存区如果一个对象经过多次复制都存活下来就可以晋升到老年代了老年代 Old长期存活对象、大对象。GC频次比较低采用标记-整理算法一个对象经历的区域伊甸园 - 幸存区-幸存区-...-幸存区-老年代新生代统一采用复制算法老年代采用标记整理特殊情况如果某个对象很大复制成本很高可以跳过新生代进入老年代3.2 垃圾收集器在JVM中由垃圾回收器来实现上述分代回收的策略。分代回收只是最基本的策略落实到垃圾收集器上还有更特殊和进阶的策略。收集器分代特点使用场景Serial新生代单线程串行STWClient客户端、小内存程序ParNew新生代Serial多线程版唯一可配合CMS老年代CMS搭配使用Parallel Scavenge新生代吞吐量优先自适应调参后台计算、无交互服务Serial Old老年代单线程标记整理Client、CMS失败后备方案Parallel Old老年代多线程吞吐量优先高吞吐离线任务CMS老年代并发低停顿标记清除碎片多JDK8前互联网低延迟服务G1全堆分区Region可预测停顿自动压缩替代CMS大内存服务端JDK9默认