
目录一. JVM简介二. JVM内存区域划分2.1 程序计数器2.2 栈区2.2.1 虚拟机栈2.2.2 本地方法栈2.3 堆区2.4 元数据区三. JVM的类加载机制3.1 类加载流程3.1.1 加载3.1.2 验证3.1.3 准备3.1.4 解析3.1.5 初始化3.2 双亲委派模型四. JVM垃圾回收机制GC五. JVM的垃圾回收器一. JVM简介JVM是Java Virtual Machine的简称意为Java虚拟机。各组件核心作用- JVMJava虚拟机最核心底层仅负责运行Java字节码无编译、无类库单独无法运行程序。- JREJava运行环境 JVM 核心类库java.lang/java.util等 运行时支撑工具满足仅运行Java程序的全部需求普通用户/服务器仅需JRE。- JDKJava开发工具包 JRE 开发编译工具javac编译器、javadoc文档工具、jdb调试器等是开发者的完整工具包能编、能调、能运行。注意开发Java代码安装JDK包含了JRE。不需要开发只需要运行一个Java程序已编译并打包好的字节码文件安装JRE即可。在我们把代码写好并点击运行时会先触发JDK的javac编辑器将代码编译成 .class字节码文件编译成功后再调用Java命令运行字节码创建出独立的JVM实例在JVM中字节码文件会被解释/翻译成二进制机器指令最后再在CPU上运行。JDK包含JREJRE又包含JVMJRE是运行时环境全程提供运行支撑。JVM是JRE中预制的基础运行环境执行Java程序时会基于这个预制环境为当前程序创建一个独立的JVM实例来专属使用。在CPU执行机器指令的整个过程中JVM会全程做底层核心管理工作而非转码后闲置。JVM负责Java程序运行的所有专属管理逻辑线程调度垃圾回收等CPU只负责无脑执行二进制机器指令不参与任何程序层面的管理。JVM不是“只有一个”有很多版本Windows版本 Linux版本Macos版本 Android版本......----都能解释执行相同的字节码文件。JAVA引入java虚拟机JVM的目的一次编译到处执行”是引入JVM最核心、最根本的目的也是Java跨平台特性的底层支撑。即直接把写好的并编译好的字节码文件放在不同平台上执行平台上有预装专属JVMJRE中的运行字节码时创建JVM实例完成本地翻译完全不需要修改代码更不能重新编译。二. JVM内存区域划分每次运行Java程序本质上就创建了一个对应的JVM。每个Java进程内部都包含了一个JVM。Java程序中使用的“内存”其实是JVM的内存JVM启动时从操作系统申请一大块内存应用程序后续需要使用的时候就可以从JVM的内存中进行分配。JVM内存划分如下2.1 程序计数器很小的区域只保存一个数字即下一条要执行的java字节码指令的地址。在内存中由软件维护JVM源码注意一个JVM中不是只有一个程序计数器而是java程序中每个线程都有自己的程序计数器2.2 栈区栈区分为虚拟机栈和本地方法栈2.2.1 虚拟机栈虚拟机栈是给java程序使用的栈维护了方法调用的关系嵌套关系虚拟机栈仅服务java方法除了native本地方法以外的方法包括自定义的方法以及Java核心库中的方法一个Java线程执行过程中调用过多少个Java方法含自定义、系统内置虚拟机栈中就会对应创建多少个栈帧仅嵌套调用的方法栈帧会同时存在无嵌套的方法栈帧会按执行顺序依次创建、执行、销毁彼此不会共存。注意一个栈帧对应了一个方法每个栈帧存储着对应Java方法的调用方法的实参方法内部的局部变量方法结束后要返回的上层方法的位置返回值等。同一时间永远只有一个栈帧在执行2.2.2 本地方法栈本地方法栈是为Java调用的native本地方法底层多由C/C实现 专属准备的线程私有内存。JVM底层就是C实现的我们在Java中写的代码往下调用着就调用到了C的范围native方法比如说 Thread 的 sleep 方法就是在本地方法栈中开出的栈帧调用并执行的。main函数执行到Thread.sleep后的流程1. Java主线程启动虚拟机栈中压入 main 栈帧栈顶正在执行2. main 执行到 Thread.sleep() 时JVM立刻在当前线程的本地方法栈中为这个native的sleep方法开辟专属栈帧并压入成为本地方法栈的栈顶3. 执行引擎切换到本地方法栈执行sleep的C/C底层逻辑系统级休眠4. 休眠结束sleep执行完毕本地方法栈立即弹出sleep的栈帧并销毁5. 执行引擎切回虚拟机栈回到 main 方法中调用sleep的下一行代码继续执行。注意虚拟机栈和本地方法栈都是JVM为每一个线程都开辟出的独属于这一个线程的内存空间虚拟机栈和本地方法栈虽然相互独立但同属于一个线程2.3 堆区堆区是JVM中最大的内存区域。我们新new的对象都放在堆中2.4 元数据区元数据区在之前也称为“方法区”元数据区的作用用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。JVM在运行时就会把 .class文件放到内存中还需要通过一些特定的结构来表示------即构成 类对象不是我们new出来的实例化对象。一个类只有唯一一个类对象相当于描述类的“模板”总结Java程序中常见java变量对象等的存储区域栈存放局部变量开辟的临时空间即栈帧。栈中还存储了thisthis作用this 就是非静态方法的“对象身份证”存着当前操作对象的堆地址靠这个身份证方法才能精准操作堆中对应的对象而不是其他对象也不是凭空操作。堆new出来的对象以及new出来对象的对象头和其中的非静态成员变量的实际值/地址不包含非静态成员变量的类型访问限定修饰符以及名称。非静态成员方法以及静态成员方法都不在堆中在调用方法时元数据区存放类对象其中就包含静态成员变量的存储描述初始目标值非静态成员变量的存储描述以及静态/非静态成员方法的结构描述方法的执行临时数据都在虚拟机栈帧中注意方法里 new 的对象本体永远在堆里和局部变量无关只有指向这个堆对象的引用变量才是存放在栈帧局部变量表中的局部变量。上面这些内存区域针对程序计数器和栈存在多份每个线程又一份自己的针对堆和元数据区一个进程只有一份在同一个进程中的多个线程共享一份堆和元数据区什么情况下内存溢出栈溢出包含了方法的调用关系栈帧太多了。比如递归代码时递归的结束条件有错误导致无限递归。又比如创建了太多的局部变量不容易触发堆溢出new的对象太多了。比如无限循环的往某个集合类中添加元素三. JVM的类加载机制JVM的类加载机制就是把 .class文件读取到内存中构建出类对象的过程放在元数据区3.1 类加载流程类加载的流程分为以下5个步骤3.1.1 加载根据代码中写的“全限定类名”Java中要使用哪个类就要import全限定类名找到对应的 .class文件然后打开文件并读取文件中的二进制数据到内存中.最终到元数据区3.1.2 验证根据读到的二进制内容验证是否是合法的格式JAVA官方有明确规定的格式https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.1注意JVM是先有要加载类的全限定类名再去找对应的 .class文件并且一个Java程序中有多个全限定类名每一个全限定类名都要找到对应的 .class文件且每一个 .class文件都要验证3.1.3 准备给要创建的类对象分配内存空间元数据区非静态成员→分配内存。仅存描述信息元数据区无实际内存分配静态成员→分配内存赋默认值均在元数据区完成非静态/静态成员方法→分配内存存储完整的描述信息注意由于这里只是给静态成员变量赋予默认值并不是目标值对于静态/非静态方法也只是在元数据区中保存了它的结构信息直到真正执行时才会在栈上开辟空间3.1.4 解析解析阶段是Java虚拟机将常量池内的符号引⽤替换为直接引⽤的过程也就是初始化常量的过程。注意元数据区存储的是字符串常量的地址指向的是在堆中的字符串常量这个的对象字符串常量这个对象实际存在于堆中的字符串常量池中3.1.5 初始化初始化类的静态成员变量赋目标值执行静态代码块以及对父类的加载非静态成员变量赋目标值是在new对象时而不是在类加载时触发类加载的时机类加载遵循“懒汉”思想用到的时候才加载new这个类的实例时调用这个类的静态方法/访问静态成员针对子类的加载也会触发父类的加载当加载一个类时如果它引用了其他类比如自定义类中包含了标准库类或其他自定义类这些被引用的类也会被触发加载类加载一次即可且每个类的类对象在一个JVM进程中也是单例的3.2 双亲委派模型双亲委派模型出现在类加载的第一步找到对应 .class文件----涉及到一个模板---类加载器如果⼀个类加载器收到了类加载的请求它⾸先不会⾃⼰去尝试加载这个类⽽是把这个请求委派给 ⽗类加载器去完成每⼀个层次的类加载器都是如此因此所有的加载请求最终都应该传送到最顶层 的启动类加载器中只有当⽗加载器反馈⾃⼰⽆法完成这个加载请求它的搜索范围中没有找到所需 的类时⼦加载器才会尝试⾃⼰去完成加载。JVM中默认包含了三个类加载器名称与层级关系自上而下BootstrapclassLoader ExtensionclassLoaderApplicationLoader这三个类加载器有父子关系不是父亲子类的关系只是这个类加载器中有一个parent这样的属性指向父亲的引用三个类加载器的作用BootstrapclassLoader负责加载Java标准库中的类标准库类的 .class文件都放在特定的目录中BootstrapclassLoader就负责在这些特定目录中找到对应的 .class文件ExtensionclassLoader负责加载Java扩展库中的类是由JDK厂商做出的扩展--同样也是由Java官方做出的扩展ApplicationLoader负责加载第三方库中的类以及你当前项目中的类自定义的类注意程序员还可以自定义类加载器很少涉及到双亲委派模型的工作流程比如代码中由一个类my.app.User (我们项目中自定义的类的全限定类名)优先级优先加载标准库的其次是扩展库最后是第三方库/自己项目中双亲委派模型既负责“找到”文件也负责“加载”文件。双亲委派模型的本质是“先委派查找确定加载者最后才由确定的加载器执行真正的加载”。四. JVM垃圾回收机制GCJVM垃圾回收机制是为了更好的应对内存泄漏GC只负责堆内存中的对象JVM专门指派一些线程这些线程周期性的扫描你的代码中已经申请内存new出来的对象并自动判定当前这个内存是否不再使用。如果不再使用就释放掉对象/内存java的GC要回收的内容是啥-----堆中存放的没有引用指向的对象注意GC是以对象为单位进行内存回收的不是以字节为单位一个对象要么整个释放要么不释放不会出现”释放一半“的情况GC的进行首先要找到那些对象是不再使用的对象如何判断看某个对象是否有引用指向---在Java中使用对象只有通过引用这一种方式。但是有一些情况下不太好确定某个对象到底是不是垃圾但是宁可放过也不要错杀错杀后果会非常严重如何判定对象有没有引用指向呢有以下几个方案方案一引用计数不是Java的方案是Python/PHP的方案给每个对象身上再安排一个空间这个空间存储一个整数用来表示指向这个对象的引用的个数当围绕对象进行”引用复制“时就会更新这个数。如果计数是0了此时就可以释放了但是这个方案窜在两个缺点消耗更多的内存空间每个对象都要有一个这样的计数器在产生循环引用时会导致出现误判。如下图所示方案二可达性分析java方案在一个Java代码中一系列对象的引用存在一定的关联关系--类似于树形结构可达性分析的过程就是从根节点出发根节点可能有多个尝试遍历这个引用树所指向的对象遍历的过程中凡是经过的对象都标记成”可达“。另一方面JVM自身知道自己一共有那些对象除去”可达“的对象剩下的就是”不可达“的需要被回收的对象。哪些可以是根节点GCRoots栈上的局部变量常量池的引用所有引用类型的静态成员变量注意根节点GCRoots是对象的引用而不是对象本身谁new的对象谁持有引用对象的地址就存在谁那里。如常量池对象的引用存在元数据中常量池对象本身存在于堆中由于随着代码的运行对象之间的引用关系实时变化所以上述”可达性分析“需要周期性的进行可达性分析的优点可达性分析没有引入额外的内存空间没有循环引用问题----原因1. 以 GC Roots外部引用为判断标准不受内部循环影响遍历的是引用而不是对象本身。2. 一次扫描中每个对象只遍历一次不会遍历第二次避免死循环。即使是图状这样复杂的关系也能有限避免可达性分析的缺点需要消耗更多的CPU资源/更多的时间拿时间换空间识别出垃圾之后如何释放内存?有以下4个方法法一标记 - 清除不推荐把标记出来的垃圾直接释放掉当我们申请内存时都是申请”连续内存“总的空闲空间充足但是由于离散的空间太多了尝试申请一大块内存时就可能会申请失败法二复制算法不推荐将内存空间一分为二同一时刻只使用其中的一半把不是垃圾的对象复制到另外一半然后将全是垃圾对象的那一半的空间全部一起释放随着GC的周期执行将对象左右两边来回复制复制非垃圾的对象复制算法的缺点空间利用率很低最多只能用到50%的空间如果当前存活的对象很多复制开销就会很大复制算法的优点有效的解决内存碎片的问题法三标记-整理不推荐标记-整理 类似于顺序表删除中间元素搬运---消耗可能大可能小标记-整理 对于空间利用率有所改善法四分代回收最终GC使用分代回收就是根据不同对象的情况/特点即对象的”年龄“采取不同的方案(综合上面三个方案)。经验规律如果某个对象年龄比较大有很大概率继续存活下去对象的”年龄“用GC扫描的轮次来描述分代回收是将整个堆分为两个大的部分---新生代和老年代。其中的新生代还分为一个伊甸区和两个幸存区分代回收的流程1. 新new的对象放在伊甸区2. 伊甸区中的对象经过第一次的GC大部分都会被淘汰掉经验规律大部分的新对象生命周期都很短而没有被淘汰掉的对象通过复制算法进入幸存区幸存区有两部分一次只用一部分另一部分会被清除伊甸区的垃圾回收是复制算法的一部分---将不是垃圾的对象复制到幸存区再将伊甸区全部清空3.下一轮的GC也会针对幸存区进行扫描以此还会淘汰掉一把部分的对象没有淘汰对象通过复制算法进入另外一个幸存与复制算法的方法一致4.随着每一次GC的进行对象就会在新生代的幸存区来回拷贝每经过一次拷贝对象的年龄就15.经过一定时间之后对象的年龄就达到一定的阈值此时就会把这个对象直接搬运到老年代6.对象进入老年代之后针对老年代的GC频率就很低了以此减少扫描的开销老年代发现对象是垃圾采用标记-整理的方式处理---虽然整理一次比较消耗资源但是整理的频率低针对老年代的扫描称为Major GC开销比较大但是频率低---存活的对象极多针对新生代的扫描称为Minor GC开销比较但是频率高---存活的对象极少注意如果一个对象的内存特别大就直接放入老年代---避免复制开销巨大上面的分代回收只是属于一种”思想方法“并且严格来说不是”真实情况“而是一个简化版本每种垃圾回收器的机制之间有差异五. JVM的垃圾回收器JVM的垃圾回收器有很多比较老的已被淘汰的垃圾回收器Serial收集器ParNew收集器Parallel Scavenge收集器Serial Old收集器Parallel Old收集器如今还存在的垃圾回收器CMS并发标记、并发清除追求低停顿产生内存碎片G1分区域回收优先处理垃圾多的区域可控制停顿无碎片ZGC几乎全程并发停顿极短支持超大堆无内存碎片