目录
什么是JVM
- JVM是Java虚拟机的意思,也就是用例执行.class文件,对于不同的系统有不同的JVM版本,那么我们的windows编译出来的*.class文件也可以在Linux上运行,也就是我们的一次编译多次运行
- JVM属于JRE,JRE也就是我们Java的运行时环境
- JVM其实是一个类别,官方是为其书写了标准,要一个软件遵守这个标准,就可以成为了JVM,我们一般使用的是Oracle官方提供的JVM
JVM的结构
- 从功能我们大体可以分为三大块,类加载模块,内存管理模块,执行引擎模块,而我们的GC逻辑上是属于执行引擎
- 内存分区的内存不是属于JVM,而是被JVM管理抽象
关于类加载
为什么需要类加载
- 首先我们计算机的主要组成是输入设备,输出设备,计算核心(运算器-控制器)CPU,内存,其中我们的核心CPU只能和内存交互,不能直接和其他设备(包括硬盘)交互的
- 我们开发工作将需求产品变成一组源代码*.java,然后通过编译器将源代码变成一组类文件*.class文件,通常我们将其类文件保存到硬盘上,而且我们一些JDK提供的类文件也是存储在硬盘中
- 这时候我们就需要解决一个问题:我们CPU只和内存打交道,我们的数据(类文件)放在硬盘上,我们就需要将我们的类文件从硬盘加载到内容中,而负责这项工作的就是JVM的类加载的子模块
什么时候需要进行一个类的加载
- 我们的JVM一般都是使用方案乙这种方式
- 引出的问题就是什么叫一个类被用到了
什么时候类被用到
- 实例化对象的时候,需要用到类(既有new,也有反射)
- 访问类的静态属性和类的静态方法
- 子类被用到时候,父类也自然被用到(含接口)
如何找到相应的类
- 首先类的名称=包名+类名
- 我们的类文件在硬盘上,硬盘上的数据被抽象成文件了(一颗文件系统树),我们的类加载如何去找到类文件的
- 我们自己写的类和官方停供的类处理方式一样吗,肯定处理方式不可能,所以对于不同的类文件需要不同的类加载器去处理
- 我们的JVM就是通过类加载器+类的权威名称来唯一确定一个类
类加载器的种类
- 启动类加载器,负责加载我们常见的JDK提供的类(比如java.lang.String)
- 应用类加载器,负责加载我们自己写的类,引入第三方类(@Autowired,自己写的类)
- 扩展类加载器,负责加载JDK提供的,但是不常用的一些类
- 对于启动类加载器,加载类的路径是绑定在JVM的程序路径上的,也就是说我们的启动类加载器最后都是从这个rt包中去调用相对路径下的包
- 对于应用类加载器,需要启动的时候告诉JVM,去告诉去哪些地方去找类,通常把这个路径列表称为classpath
类文件的主要结构
- 类的元信息(类名,父类,接口,访问限定符),类文件的常量池,属性列表,方法列表
- 指令:方法中指令,不区分静态还是普通方法
类加载的过程划分
- 装载:1找到类文件验证类数据是否符合刚才说的规范 2文件读操作,解析 3将读取的内容组织成一个逻辑整体“类”放到内存的某个位置
- 链接:类文件中是不会存内存地址,但最终运行需要内存地址,所以当和其他类(内存的某个位置)产生关系
- 初始化:涉及类的基本信息初始化 1静态属性 2静态代码块 按照我们的代码顺序去从上往下执行
- 这三步,对于父类得先加载完,子类才开始
类加载到内存的什么区域
- 方法区:指令一定在方法区,基本元信息没有规定,跟着不同的JVM的实现走
类被卸载
- 类的卸载条件,一个类没有被用到的时候可以被卸载
类如何被确定
- 类加载器+类名称
如何确定由是那个类加载器的加载
- 默认情况下,一个类的加载器是由这个加载指令所在的类当时的类加载器进行加载
- 如何获得一个类的类加载器 类名称.Class.getClassLoader()或者对象.getClassLoader(),或者Class.forName(包名.类名).getClassLoader
双亲委派模型
- 这个是三个类加载器采用的策略,JVM鼓励大家自己自定义类加载器的时候遵守这个策略,但是不是强制的,比如Tomcat自定义的类加载WebappClassLoader,就没有遵守双亲委派模型
- 双亲是partent,只是一个名称,其实只有一个,比如应用类加载的parent是启动类加载器
策略思想
- 比如我们这个Scanner类的加载,如果按照我们的默认方式,这个Scanner会被我们的SomeA这个类的类加载器去加载,那么就出现了一个问题,官方的类去加载我们官方的类,那么就会可能出现风险,如果我们这个类加载器中也有这个Scanner类呢,那么就是分不清了
- 所以我们引入了双亲委派模式,我们的应用类的双亲是启动类加载器,就不会出现上面那种情况
执行引擎模块
- 我们的执行引擎其实类似于硬件的CPU
- JVM这里的执行引擎是和线程绑定的,JVM中的线程类似硬件中的CPU的一个核,每个线程都有自己独立的PC——>每个核都有自己的程序计数器(PC)
内存管理模块(GC)
- GC含义1 垃圾回收器,负责进行内存管理的组件
- GC含义2 一次垃圾回收的过程
执行引擎上面时候会用到内存空间
- 类加载的时候,存储类的信息 对应静态属性/指令
- 实例化对象的时候,存对象中的属性为代表的数据
- 当一个方法被调用的时候——存储乙局部变量为代表的数据
- 创建一个新线程的时候(属于实例化对象的一种特殊情况)
标准规定的内存区域划分
- 方法区:存放方法指令,“类”
- 堆区:存放对象信息
- Java调用栈/本地方法调用栈,存放方法中调用的栈帧,局部变量
- 运行时常量池:类文件的常量池的数据
- PC每个线程独有的,存放下一条指令的地址
- 堆,方法区,运行时常量池是共享的,PC和栈是私有的
- 对于这样划分逻辑上的区域,我们对于不同的区域进行不同的处理,比较方便
- JMM是CPU和内存的抽象,工作存储区(寄存器和缓存),主存储(内存)
关于什么时机分配
- PC什么时候被分配:一个线程被创建的时候 那么对应着当线程被终止的时候,就会被回收
- 栈中的内存什么时候被分配:一个方法被调用的时候,这个线程的方法,那么在调用结束的时候(栈帧)
- 堆中的内存:一个对象被实例化的时候,那么在对象不再使用的时候,就会回收
- 方法区和运行时常量池:一个类加载的时候,那么对应在一个类被卸载的时候
- 这些分配和回收中,其实只有对象不被使用这个点是比较难明确的
所以我们的 GC主要探讨的就是内存回收的问题。PC和栈,方法区的回收是比较确定,所以就将GC的问题聚焦在了堆的内存回收
引出的问题
- 如何判断垃圾对象
- 如何进行垃圾回收
- 什么情况下进行垃圾回收
如何判断垃圾对象
- 怎么判断呢?靠引用的持有情况。在理想情况下,应用不再使用一个对象的任何一个引用,那么这个对象一定不再被使用了
- 在理想情况下,我们是将所有不再被使用的对象都被回收,但是理想情况是不好到达的,所以就会出现两种退而求其次的情况
垃圾对象的判定算法
引用计数法:JVM没有使用,但是在PHP,C++的智能指针的原理
- 定义一个属性refCount表示这个对象被引用的指向的次数,当某个对象的 refCount==0的时候,这个对象成为垃圾对象,就可以被回收了,并不一定是就立即回收
- 什么时候加1呢和减1呢
出现的一个问题——循环引用的问题
可达性分析法: 这是JVM使用的方法,我们JVM管理的对象,最终会以几张有管理。我们从应用持有的引用出发,进行图的遍历,无法到达的结点,一定就是垃圾
我们假定内存回收是整体的回收,GC期间,整个时间是静止的(也就是这几张图的结构是不会变化的,应用程序也不再进行执行任何指令)
GC出发进行遍历的根,我们称为GC RootS
- 所有静态属性中的引用
- 所有线程中的栈里的栈帧是不会变了,目前栈中的栈帧是以后会有用的,所以目前这些栈帧中的局部变量引用
引用的分类
- 引用的作用就是通过引用来找到对象
- 强引用:就是平时用的引用 1可以通过引用找到对象 2严格决定了对象的生死
- 软引用:1可以通过引用找到对象 2并不现在GC对对象进行回收(只是被软引用指向的对象回收的优先级比较低,只有在其他垃圾对象被回收之后,内存还不够用才回收)
- 弱引用 :1 可以通过引用找到对象 2并不现在GC对对象进行回收(正常回收)
- 虚引用:仅仅在对象被回收时收到通知
GC的内存如何工作
对于我们这种内存碎片化并没有好的解决方法,所以引出对堆上的内存进行分区的思想来缓解
Java 7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区
- Young Generation Space 新生区 Young/New 又被细分划分为 Eden 区和 Survivor 区
- Tenure generation space 养老区 Old/Tenure
- Permanent Space 永久区 Perm
Java 8及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间
- Young Generation Space 新生区 Young/New 又被细分划分为 Eden 区和 Survivor 区
- Tenure generation space 养老区 Old/Tenure
- Meta Space 元空间 Meta
什么叫复制和回收
- 就是将垃圾回收,然后将不是垃圾的数据进行复制到另一块内存
关于状态的转换
- 对象的一生,对象每经过一次GC,其对象的年龄就会增加1
- 我们的对象诞生在伊甸区,我们的伊甸区只有少量的主角会到生存区,在一次GC后,可能很大对象都会死掉,存活下来的就会复制到SurviveA区域,伊甸区标记为全部可用,又经过一次GC,SurviveA活下来的对象+伊甸区活下来的对象被复制到SurviveB,同时标记SurviveA不可用
- 如果这个主角对象一直或者,随着年纪的增长,当某一次GC发生的时候,GC:我们的对象成年了,就离开生存区(新生代),被复制到了老年代,如果对象的大小特别大,是可能一开始就被扔到老年带
- 老年代的GC 清理+回收
GC回收的时期
- 内存不够用的,就会触发,一般设置一个空间阈值,比如超过60%进行一次垃圾回收,GC不能太频繁,因为GC的成本很大
- 定期进行GC
- Full GC(整个清理)=新生代的GC+老年代的GC
- 大量的GC,只会进行新生代的GC,所以耗时并不高,但是随着新生代GC的进行,不断会有对象进入老年代,老年代也会触发阈值,导致老年代GC,老年代GC发生,通常已经进行了新生代的GC,所以也可以认为老年代GC是Full GC