JVM
# JVM
个人理解,JVM内存空间,也就是JVM管理的内存,相当于就是存放数据的地方,由执行引擎来从中获取数据进行相关操作。
# JDK、JRE、JVM区别与联系
# JDK
JDK(Java SE Development Kit),Java标准开发包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库(就是String、集合之类的)等。
下图是JDK的安装目录:
# JRE
**JRE( Java Runtime Environment) 、Java运行环境,主要提供java程序的运行环境,**用于解释执行Java的字节码文件。普通用户而只需要安装JRE(Java Runtime Environment)来运行 Java 程序。而程序开发者必须安装JDK来编译、调试程序。
下图是JRE的安装目录:里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
# JVM
**JVM(Java Virtual Mechinal),Java虚拟机,负责解释执行字节码文件为计算机可以识别运行的机器码指令,**是JRE的一部分。它是整个java实现跨平台的最核心的部分,是可运行java字节码文件的虚拟计算机。
# 总结
开发人员需要JDK,因为需要用来开发java程序以及编译java文件,
有了项目的字节码文件后运行,需要JRE,因为要提供java程序的运行环境,
字节码文件会被加载到JVM中,由JVM解释字节码指令转化为计算机可以识别运行的机器码指令。
# Java字节码文件
# 组成
基本信息: 主要是两个,一个是魔数,一个是版本号。魔数用来校验文件的类型(是否是class文件),因为不能做到凭借文件扩展名就能判断文件类型,软件版本号用来标识编译该字节码文件的jdk的版本号,当前字节码版本号需要小于实际运行时jdk的的版本号才能运行。
魔数就是Java字节码文件的文件头,软件不是通过文件扩展去判断文件类型的,而是通过文件开头的几个字节,也就是文件头去判断文件类型,魔数的作用就是用于在JVM加载的时候判断是不是字节码文件,不是的话软件会报错。
一般来说,JDK的版本号就是 主版本号-44 ,如52就是JDK8。至于副版本号一般不太关心。可以理解为用于细分主版本号。
# 重点介绍
# 常量池
常量池存放一些字符串常量,会在方法里的字节码指令中被引用。
# 方法
# 简单字节码指令的阅读
阅读字节码指令,解释下图问题。
# 类的生命周期
最重要的是初始化阶段,因为这个阶段可以进行人为的干涉
# 加载阶段
通过类加载器将不同渠道的字节码文件加载到内存当中,在方法区上生成一个对应的InstanceKlass对象来保存类的相关信息(也就是字节码文件的相关信息),然后在堆区生成对应的Class对象(就是反射时常用的)该对象同样保存类的相关信息,如字段,方法等,但要少于InstanceKlass对象,原因在于开发者实际上不需要访问方法区里的所有信息,为了控制开发者访问数据的范围,和数据的安全性,开发者通过访问Class对象来获取类的相关信息。两个对象之间彼此有引用相互关联。
注意:此时静态变量并未处理,此处知识说明静态变量会存储在该地方。
# 连接阶段
链接阶段可细分为三个阶段
- 验证:校验字节码文件是否符合JVM虚拟机规范
- 准备:在堆上的Class对象中为静态变量分配内存并初始化(初始化的原因是防止旧的内存地址上有数据残留)。如果有值不会赋值,但是如果是final就会在该阶段直接赋值
- 解析:在字节码文件当中使用常量池的数据是通过符号引用。具体表现为常量池里的常量是有编号的,在他处使用就是通过编号访问,解析阶段会将符号引用转化为内存地址的直接引用。
# 初始化阶段
初始化阶段:从代码层面上来说会执行类的静态代码块的代码,并且为静态变量赋值。从字节码层面来讲是会执行字节码文件中的clinit方法里的字节码指令。
存在没有初始化过程的情况
一般会在以下情况触发初始化
继承情况下的初始化 :父类初始化不会影响父类,子类初始化会先初始化父类。
# 使用阶段
就正常使用,没啥特别。
# 卸载阶段---未完成
与累计回收有关,待续
# 类加载器
类加载器:类加载器是一种将字节码文件加载到内存上的技术,但是也仅仅就是加载到内存上,会调用本地接口的方法去在方法区与堆去上分别创建类的InstanceKlass对象和Class对象。
# 类加载器的分类
大体分为两类
有JVM源码提供的,一般使用C++、C 语言编写,如HotSpot就是用C++写的。
JDK提供的、使用Java语言编写的。
# JDK8版本
启动器加载器:负责加载Java中的核心类库,比如说我们常用的String,List。实际上你同样也可使用该加载器帮你加载指令的Jar包,但是不规范
扩展类加载器:负责加载一些通用的且不核心的类,实际上你同样也可使用该加载器帮你加载指令的Jar包,但是不规范。
应用程序加载器:主要负责加载classpath下的类文件。也就是我们编写的应用的类的字节码文件,以及应用所依赖的第三方jar包文件。
每个类加载器都有自己要加载的目录,会从该目录中加载字节码文件,我们也可以指定去加载额外的目录,当目录在多个类加载器所要加载的目录中存在时,就需要双亲委派机制来决定具体由哪个类加载器加载
# JDK8以后的版本
# 双亲委派机制
所谓双亲委派机制是一种解决在程序运行中,一个类到底是由哪个类加载器来加载的机制。
类被加载进内存有两点最基本的要求:
- 要避免重复的加载
- 要保证类加载的安全性,
是什么,用于解决什么问题,详细介绍
具体流程
# 如何打破双亲委派机制
findClass是实现从什么地方获取字节码文件,封装成字节数组,然后调用defineClass,该方法就是本地接口(final修饰)了,负责将字节码文件加载进内存,做的事情就是类的加载阶段做的事情
为什么,怎么打破
# 打破的几种方式---未完成
# 运行时数据区(JVM管理的内存)
线程共享的是每个线程都可以访问,线程不共享的是每个线程都有自己的一份(无线程安全问题,线程结束就会释放)
# 程序计数器
字节码文件加载到内存后,每一条字节码指令都会有自己的内存地址,
程序计数器的作用就是保存下一条将要执行的字节码指令的地址,方便解释器获取到转化成机器指令运行,在此基础上产生两个特别重要的作用
控制程序的执行,实现分支,跳转,异常等逻辑。
多线程环境下,能够保存线程将执行的下一条指令地址,在得到cpu的执行权时继续运行程序。
该区域不会出现内存溢出,因为程序计数器中只会保存一个固定长度的内存地址,(用来存放字节码指令的地址)而内存地址的长度是不会发生改变的,具体的长度要看具体的硬件配置。
# Java虚拟机栈和本地方法栈
Java虚拟机栈存放的是java语言编写的方法的栈帧,本地方法栈存放的是C++编写的方法的栈帧,**栈帧就是存放该方法的状态和变量的栈空间。**HotSpot虚拟机则是没这个区分,都用一个Java虚拟机栈存放栈帧。
已经执行完的方法的栈帧会从栈中弹出,栈中的方法遵循先进后出的规则执行。
Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈。
# 栈帧的组成
栈帧本质就是存放着方法的变量和相关状态数据,主要提供数据,具体对数据的操作来自于程序计数器里的字节码指令
栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
# 局部变量表
用于存放方法执行过程中的所有局部变量。
局部变量表的结构
槽主要保存一下几种数据:
- 方法参数 。其顺序与方法中参数定义的顺序一致。
- 实例方法的this对象(第一个槽)
- 方法内申明的局部变量
# 操作数栈
操作数栈用于存放方法执行过程中的中间数据。在对局部变量进行操作的时候是需要将其从局部变量表中copy到操作数栈中的。
在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。
# 帧数据
主要包括:
动态链接
引用别的类的属性或方法在类加载阶段不会将符号引用变成内存地址的直接引用。
运行时常量池???
方法出口
异常表
异常表里面存放着该方法的异常处理逻辑中,对应的异常情况应该跳转的字节码指令位置,控制了方法中存在异常处理时的代码跳转逻辑。实际上就是实现了异常的处理。在编译的时候就已经生成。
在编译的时候就已经生成
# 栈内存溢出
会出现内存溢出
# 堆
创建出来的对象基本都存在堆上,静态变量也存在于堆上。
该内存区域存在内存溢出
# 堆内存的划分---未完成
随着程序使用过程中Used的空间越来越大,total开始变得不够,此时会向JVM申请扩大total(可用堆内存) 但是不是超过Max(最大堆内存)
并不是used=total=max就发生堆内存溢出,判断情况比较复杂,这与垃圾回收有关
# 如何设置可用堆内存与最大堆内存
# 方法区
方法区是存在溢出的
# 组成
字符串常量池只存放字符串常量,但运行时常量池不是,他还存放字段名,类名,等常量
存放类的元信息
运行时常量池
字符串常量池
# 实现方式
# 总结---不同版本之间运行时数据区的区别
# 直接内存---未完成
与元空间的关系???
# 垃圾回收
# 线程不共享部分不参与垃圾回收的原因
该部分的内存空间是线程独有的,随着线程的创建而创建,销毁而销毁。
内存泄漏: 一个对象如果不再使用,但是没有释放其所占内存,就会发生内存泄漏。
# 手动与自动的内存管理
# 自动垃圾回收的应用场景
# 方法区的回收
方法区的回收主要就是回收不再使用的类。
# 回收的条件
条件2
条件3
# 堆回收
# 回收的条件与判定对象是否存在引用的方法
回收的条件的基本思想:
对于强引用来说,只要对象没有强引用引用到该对象,那么就会被回收,否则不会回收
对于软引用:当内存不足的时候,会回收只被软引用引用的对象。
弱引用和虚引用:都是不管内存是否不足都会被回收只被弱引用和虚引用引用的对象
# 引用计数法
引用计数法会为每个对象维护一个计数器,对象每被引用一个就加一,如果为0就代表可回收。
# 可达性分析算法
JVM虚拟机采用的就是可达性分析算法来分析对象是否可以被回收
**注意:以上两种方法都是用于判定强引用的。**指的引用是强引用。
# 对象的几种引用类型
注意:引用的概念:引用不是对象,只要你在方法中,代码中用到了其他类,你就是强引用了该类的对象,要不引用就让其为null
B sd = new B() sd是指向B类的一个对象的引用, sd = null这个操作不会对内存上的对象有任何影响,只是减少了引用该对象的一个强引用
2
Java 中的引用有四种,分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。
强引用:无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
强引用就是我们平时写代码的引用
软引用:当内存不足的时候,会回收只被软引用引用的对象。
使用SoftReference来包装普通对象,则该普通对像就有了一个软引用引用他,但SoftReference本身是强引用。
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(obj, queue);
//强引用对象滞空,保留软引用
obj = null;
2
3
4
5
弱引用和虚引用:都是不管内存是否不足都会被回收只被弱引用和虚引用引用的对象
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
WeakReference reference = new WeakReference(obj, queue);
//强引用对象滞空,保留弱引用
obj = null;
2
3
4
5
6
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
PhantomReference reference = new PhantomReference(obj, queue);
//强引用对象滞空,保留虚引用
obj = null;
2
3
4
5
6
# 垃圾回收的指导思想-垃圾回收算法
垃圾回收算法
垃圾回收算法的评判标准
可以从以下三个方面来评价:
标记清除算法:
内存是连续的,在对象被删除之后,就会出现许多不连续的随机大小的可用内存单元,当我们需要一片连续的内存单元来使用时,可能无法找到满足要求的空闲内存。
也可能没有合适的内存空间,从而再次触发垃圾回收以获得更多的可用内存。
复制算法:
核心思想就是:使用两块内存,GC的时候将存活对象复制到另一片空间,那么原来的那块内存空间就可以全部回收掉,就不存在内存碎片的问题。
标记整理算法:
标记整理算法的出现就是解决标记清除算法的内存碎片问题,多了一个整理的过程:把存活的对象移动到内存的一段,那么剩下的就是可以回收的连续内存空间。回收即可
分代GC:
分代GC是对上述算法的组合使用
在YoungGC的过程中,存活下来的对象的年龄会加一,当达到某个阈值的时候,我们就认为,该对象是存活时间较长的对象,会长期被使用,于是会移动到老年区。
在YoungGC的过程中,存活下来的对象的年龄会加一,当达到某个阈值的时候,我们就认为,该对象是存活时间较长的对象,会长期被使用,于是会移动到老年区。
放入老年代的对象也不一定年龄全部都达到了阈值,也可能因为年轻代整个已经满了放不下对象了,会将一些未达到阈值的对象放入老年代。
当老年区满了首先会进行一次YoungGC ,如果年轻代GC后可以放心将要放入老年区的对象,那就没事,否则会触发FullGC,也就是对整个方法区和堆区的回收。
最主要的原因就是减少FullGC,因为FullGC对应的STW时间是比较长的,时间过长就会影响业务程序的运行。也就是尽可能在YoungGC的时候将对象回收掉。减少FullGC的频次
Young GC 与 Full GC