JVM是Java Virtual Machine的简称,意为Java虚拟机
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统
一个进程在运行过程中,要从操作系统这里申请一定的资源;而JVM也是如此,JVM会搞出一大块内存,供Java代码运行时使用;且JVM会再次把这一大块内存分割成几块区域,作为不同的用途
①JVM内存分布中最大的内存区域
②凡是使用new创建的对象都在堆上保存,还有成员变量
③堆是虽然程序开始运行时创建,随着程序退出而销毁,只要堆中数据还在就不会被销毁
①存储的是类(.class文件)、方法内容、static静态变量、常量.....
②方法编译出的字节码就是保存在这里
只是一块很小的空间;用来保存下一条执行的指令的内存地址
栈和程序计数器是每个线程都有一份的,每个线程都有自己的执行逻辑,有了程序计数器就能知道自己执行到哪里了,有了栈就可以记录方法的调用关系
①存储与方法调用相关的一些信息
②每个方法被调用、执行或递归时都会创建一个栈帧,栈帧里面存储的有局部变量表、操作数栈、动态链接、返回地址
③当方法结束后,栈帧就会被销毁,栈帧保存的数据也就被销毁了
与虚拟机栈作用类似,但是保存的内容时Native方法的局部变量
(有些JVM中,本地方法栈和虚拟机栈时一起的,比如hotspot)
JVM类加载,通俗来讲,就是把类(.class文件)从硬盘加载到内存中去
JVM类加载的第一步
(这个加载只是JVM类加载的一个小环节,不是整个加载流程)
作用:先找到.class文件,读取文件内容,并尝试解析格式
JVM类加载的第二步
作用:检查一下当前的.class文件是否符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
JVM类加载的第三步
作用:给类对象分配内存空间,其里面内容的值全为0
(为最终目标做准备,因为我们的最终目标就是构造出完整的类对象,完整的类对象就包括分配内存+初始化,这里我们已经分配好了内存)
JVM类加载的第四步
作用:主要是初始化类对象涉及中的一些字符串常量
(Java虚拟机将常量池内的符号引用替换为直接引用的过程)
JVM类加载的第五步
作用:对类对象进行更具体的初始化操作
(比如:初始化静态成员、执行静态代码块、加载父类.....)
描述类加载的过程中,如何找到.class文件
说是"双亲",其实是“单亲”或者“父亲”更为准确
JVM加载.class文件时,需要用到“类加载器”模块
在JVM中,就自带三个类加载器
(当然,程序员也可以自己写类加载器)
①Bootstrap ClassLoader:负责加载Java标准库中的类
②Extension ClassLoader:负责加载JVM扩展的库
(除了Java标准库之外,实现JVM的厂商可能还会添加一些类)
③Applicaiton ClassLoader:负责加载第三方库
(比如mysql jdbc driver、servlet、jackson;还有我们代码自己写的类)
这三个类加载器就存在父子关系
(当然这里的父子关系并非是父类子类的继承,而是对象里有一个parent引用指向父类加载器实例)
Applicaiton ClassLoader的父亲就是Extension ClassLoader
Extension ClassLoader的父亲就是Bootstrap ClassLoader
Bootstrap ClassLoader没有父亲
总结:先父后子,父没有再交给子
(说白了,双亲委派模型就是找文件的过程)
加载的优先级
标准库的类优先加载,其次到扩展库,最后是第三方库
比如:假设标准库有一个自己的类java.lang.tostring,而我们自己又写了一个类java.lang.tostring,那么就会优先找标准库里的类
①先执行Applicaiton ClassLoader,但它不会立即就搜索第三方库的目录,而是先把加载的任务委派给父亲,让它的父亲Extension ClassLoader先尝试加载
②此时需要执行Extension ClassLoader,但它也不会立即就搜索扩展库的目录,而是把加载的任务委派给父亲,让它的父亲Bootstrap ClassLoader先尝试加载
③这时候就到了Bootstrap ClassLoader执行,但是Bootstrap ClassLoader没有父亲,只能自己动手来搜索类了
(1)如果找到了类,就会进行后续的加载,此时就跟Extension ClassLoader和Applicaiton ClassLoader都没关系了,就不会加载到后面了
(2)如果没有找到这个类,任务就会交回给Extension ClassLoader孩子去完成
④如果Bootstrap ClassLoader没有找到这个类,就把任务交给孩子Extension ClassLoader去完成,此时Extension ClassLoader就要去搜索扩展库目录看看有没有这个类
(1)如果找到了类,就会进行后续的加载,此时就跟Applicaiton ClassLoader没关系了
(2)如果没有找到这个类,任务就会交回给Applicaiton ClassLoader孩子去完成
⑤如果Extension ClassLoader没有找到这个类,就会把任务交给孩子Applicaiton ClassLoader去完成,此时Applicaiton ClassLoader就去第三方库目录去看看有没有这个类
(1)如果找到了类,就会进行后续的加载
(2)如果没有找到这个类,就会抛出异常
JVM会自动的判定某个内存是否会继续使用,如果不会,就把这个内存当作"垃圾",然后自动的把这个内存给释放
①C语言:完全靠程序员手动释放
(缺点:非常不靠谱)
②C++:引入了"智能指针",一定程度上解决了C语言的问题
③Java/Python/Go/PHP/Ruby:引入了垃圾回收机制,最好的解决办法,最大程度的解放了程序员
(缺点:消耗额外的系统资源,消耗一定的时间,可能带有STW问题)
关于STW问题,可参考大佬写的文章:JVM的STW(stop the world)机制及调优案例
④Rust:进行强编译期检查;在编译过程中,智能的分析你的代码,看你代码中在哪里插入释放语句比较合适,自动给你插入,不用程序员自己写
(缺点:Rust的语法很复杂)
回收的是对象;以对象为单位进行回收
GC并非是判定哪几个字节需要回收,而是去判定对象是不是垃圾,进一步进行回收
①栈空间
栈空间不需要GC回收,因为栈里面包含很多"栈帧",每个"栈帧"对应一个方法,该方法执行结束,此时这个栈帧就销毁了,栈帧上的局部变量啥的自动销毁,也就不需要GC回收了
②程序计数器
程序计数器不需要GC回收,线程销毁,自然就跟着销毁
③方法区
很少会涉及到对象的卸载,一般都是只进不出
④堆
GC回收主要是针对堆而言,上文我们说过凡是new出来的对象都在堆上存储
步骤一:判定对象是否为垃圾
如何判定:如果一个对象在后续的代码中没有任何引用指向它,即认为该对象是垃圾
(下文会说到如何更详细的判定!!!)
步骤二:进行内存释放,清理垃圾
(下文会说到如何更详细的清理,包含了垃圾回收算法)
方法:给这个对象里面安排一个计数器,每次有引用指向它,计数器+1;每次引用被销毁,计数器-1;当计数器为0时,就认为该对象是垃圾
注意:这个引用计数并非是JVM中使用的判定方式
方法:JVM首先会从现有代码中能直接访问到的引用出发,尝试遍历所有能访问的对象,只要对象能访问到,就会标记成"可达";完成整个遍历之后,"可达"之外的对象,就会标记为"不可达",也就相当于是垃圾
注意:这个可达性分析是JVM中使用的判定方式
标记清除算法是最基础的收集算法
算法分为"标记"和"清除"两个阶段
①首先标记出所有需要回收的对象
②在标记完成后统一回收所有被标记的对象
标记后直接进行释放
缺点:直接进行释放对象,可能会引起"内存碎片";因为我们在申请内存的时候,都是申请的“连续”的内存空间,释放内存就可能会破坏原有的连续性,导致内存不连续了,即有内存但是申请不了
复制算法是为了解决标记清除算法的效率问题
本质就是通过冗余的内存空间,把有效对象复制到另一部分的空间,从而避免内存碎片
缺点:复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低
标记过程仍与"标记清除算法"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存
缺点:这种有效对象往前移的成本其实也是挺高的
①Java代码中对象的分类
(1)生命周期特别短的对象
(2)生命周期特别长的对象
②分代算法的本质:按照对象的年龄来制定不同的回收策略
(1)新生代:进行复制算法
原因:每一轮GC留下来的有效对象都不多,复制算法的开销不大
(2)老年代:进行标记清除算法和标记整理算法
原因:不会太有对象真销毁;此时标记整理的开销也不大
(特殊情况:如果这个对象体积特别大,就会直接进入老年代)
如果说上面我们讲的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现
垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的 死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间
这些只是部分的垃圾收集器,并不是全部;随着时代的发展,不停的会有新的收集器诞生,也会有旧的消亡