Jvm
- Jvm:java虚拟机,是一个运行在计算机上的程序,职能是运行java字节码文件
- 核心功能:内存管理,执行解释虚拟机命令,即时编译
字节码文件
组成部分
基本信息
magic魔数:
- 文件是无法通过文件拓展名来确定文件类型的,文件拓展名可以随意修改,不影响文件内容
- 软件是通过文件的头几个字节去校验文件的类型,如果软件不支持这种类型就会报错
- java字节码文件中,将文件头称为magic魔数(字节数4,CAFEBABE)
主副版本号:
- 编译字节码的jdk版本号,主版本号用来标识大版本号,jdk1.2之后每升级一个大版本46就加一(比如1.8就是52)
- 版本号的作用主要是判断当前字节码的运行版本和运行时的jdk是否兼容(低版本无法兼容高版本)
常量池
- 常量池中的数据都有一个编号,编号从1开始,在字段或者字节码指令中通过编号可以快速找到数据
- 字节码指令中通过编号引用到常量池的过程称为符号引用
- 作用:避免相同的内容重复定义
方法
- i++
- i++的操作是先iload之后再进行iinc操作然后再istore,++i则相反,先进行iinc之后再进行iload操作
类的生命周期
- 类的生命周期:加载-连接(验证,准备,解析)-初始化-使用-卸载
加载
- 查看内存中的对象jdk安装目录下面的sa-jdi.jar
- 启动命令:C:\Program Files\Java\jdk-1.8\lib
- java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
- 1.根据类的全限定名通过不同渠道以二进制流的方式获取字节码信息
- 2.保存在方法区(instanceKlass对象–c++创建的,堆区的是java创建的,可以被java读取)
- 3.除了保存在方法区之外还会在堆中生成一份与方法区中数据相似的
class对象文件(作用是去获取类的信息–反射,jdk8之后还会将静态字段存放在此,jdk8之前存放在方法区)
连接
细分为:
- 验证(验证内容是否满足java虚拟机规范)
魔术版本号之类的 - 准备(给静态变量赋初始值–比如int赋值为0)
特殊情况:final修饰的基本数据类型的静态变量(final修饰的值不会有变化),会在准备阶段直接进行赋值 - 解析(将常量池中的符号引用替换成指向内存的直接引用)
初始化阶段
- 执行静态代码块的代码,并为静态变量赋值
- clinit(类的初始化)方法中的执行顺序与java编写顺序是一致的
导致类初始化的方式
- 访问一个类的静态变量或者静态方法,final修饰的并且等号右边是常量不会触发初始化
- 调用class.forName(String className)
- new一个该类的对象时
- 执行main方法当前的类
clinit在特定情况下不会出现初始化
- 无静态代码块且无静态变量赋值语句
- 有静态变量的声明,但是没有给静态变量赋值
- 静态变量定义时使用了final关键字
2和3这两种情况在连接的准备阶段便已经赋值了
####### 继承的情况
- 直接访问父类的静态变量不会触发子类的初始化
- 子类的初始化clinit调用之前会先调用父类的clinit初始化方法
类加载器(classLoader)
作用:类加载过程中的字节码文件的获取并加载到内存中。通过加载字节码数方入内存转换为byte[],
然后调用虚拟机底层的方法将byte[]转换成方法区和堆中的数据
类加载器分类
- java代码实现:
1.jdk默认提供或自定义
2.继承抽象类classLoader - java虚拟机底层源码实现
jdk8-jdk9(模块化)差别较大
** 使用arthas查看:命令classloader
** 程序加载器,拓展加载器,启动类加载器,应用程序类加载器
**
** BootstrapClassLoader:启动类加载器
** ArthasClassloader:阿尔萨斯提供的加载器
** ExtClassLoader:拓展类加载器
** Appclassloader:应用程序类加载器,用于自己编写的或者是第三方jar提供的
启动类加载器
- 是由hotspot虚拟机提供的,使用c++编写的类加载器
- 默认加载java安装目录/jre/lib下的类文件,
- 用户自定义jar包可以通过1.放入/jre/lib下面进行拓展(不推荐,不要去修改jdk目录安装的
- 内容,缺有些环境对文件名称有校验);2.使用参数进行拓展(-Xbootclasspath/a:jar路径)
jdk8之前默认的加载器
启动类加载器(Bootstrap)
- 是由hotspot虚拟机提供的,使用c++编写的类加载器
- 默认加载java安装目录/jre/lib下的类文件,
- 用户自定义jar包可以通过1.放入/jre/lib下面进行拓展(不推荐,不要去修改jdk目录安装的
- 内容,缺有些环境对文件名称有校验);2.使用参数进行拓展(-Xbootclasspath/a:jar路径)
- 作用是加载java中最核心的类
拓展类加载器(extension)
- 默认加载java安装目录下的/jre/lib/ext下的类文件
- 拓展同启动类加载器:d1.目录下拓展 2.参数拓展,使用-Djava.ext.dirs=jar包目录(会覆盖掉原始的目录–1目录下的文件)
可以使用;(Windows)或者是:(linux)追加上原始的目录
程序应用类加载器(application)
-
加载的classpath目录下的文件
-
拓展类加载器和应用程序类加载器都是jdk提供的,使用java编写的加载器
-
是一个静态内部类,继承自URLClassloader,具备通过目录或者制定jar包将字节码加载到内存中
自定义类加载器
重写findclass方法
双亲委派机制
含义
- 当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载
- 向下委派加载起到了一个优先级的作用
作用
- 保证核心类加载的安全性
- 避免重复加载
注
- 应用程序类加载器application的parent父类加载器是拓展类加载器extension,而拓展类加载器的parent是空的
,但在代码逻辑上拓展类加载器依旧会把启动类加载器当时父类加载器处理 - 启动类加载器使用c++编写,没有父类加载器
打破双亲委派机制
打破双亲委派机制的三种方式
- 自定义类加载器
- 线程上下文加载器
- osgi框架的加载器
自定义类加载器
可能出现的情况:当不同应用处于同一个web容器中,且全限定名相同时可能会出现找不到第二个类的情况(ClassNotFound)
- tomcat使用了自定义类加载器来实现应用之间的隔离,每个独立的类加载器加载对应的类
分析
-
classloader包含4个核心方法
-
双亲委派机制的核心代码就在loadclass中
使用到loadclass类只是被解析,还没被加载 -
自定义加载器不指定父类加载器的话默认就是application应用程序类加载器
-
在java虚拟机中,只有同一个加载器+相同的类限定名才会被认同为同一个类
线程上下文类加载器
Thread.currentThread().getContextClassLoader()
可以自己设置,使用setContextClassLoader()
- jdbc案例(启动类加载器加载的类委派应用程序类加载器去加载类)
spi机制
- 是jdk内置的一种服务提供发现机制
- 工作原理
- 在classpath路径下的META-INF/serviceices文件中,以接口全限定名来命名的文件,对应的
文件中写了接口的实现 - 使用serviceLoader加载实现类
spi中使用了线程上下文中保存的类加载器进行类的加载(Thread.currentThread().getContextClassLoader())
- 两种看法,打破:因为跳过了拓展类加载器,直接由启动类加载器委托应用程序类加载器
正常的委派是从底向上
- 未打破:drivermanager位于jdk的核心包rt.jar,启动类加载器加载,驱动是使用application程序类加载器加载
jdk9之后的类加载器
变化
- jdk8之前,拓展类加载器和程序启动类加载器原码都位于rt.jar包中的sun.misc.launcher.java
- 本质上是通过目录的jar包去加载类
- jdk9之后引入了模块化(module)概念
- 启动类加载器使用java进行编写,但启动类依旧无法通过java代码找到,返回的仍然是null
- 拓展类加载器被替换成了平台类加载器
运行时数据区(jvm管理的内存)
java虚拟机在运行过程中管理的内存区域称之为运行时数据区
主要分为两大类
1.线程共享(方法区,堆区)
2.线程不共享 (程序计数器,java虚拟机栈,本地方法栈)
运行时内存的结构
程序计数器
程序计数器只会存放一个固定长度的内存地址,因此不会发生内存溢出
开发者无需对程序计数器进行任何处理
- 代码执行过程中,程序计数器会记录下一行字节码指令的地址,执行完当前指令后虚拟机的执行引擎
根据程序计数器执行下一行指令 - 多线程的情况下:java虚拟机需要通过程序计数器记录cpu切换前执行到哪一句指令并继续运行
栈
先进后出
java虚拟机栈
栈帧
组成:
- 局部变量表
在方法执行过程中存放所有的局部变量,控制局部变量的访问范围
栈帧中的局部变量表是一个数组,数组中的每一个位置称之为慒(slot),long和double类型占两个慒,
其他类型占用一个慒
局部变量表保存的内容有:实例方法的this对象(序号为0),方法的参数,方法体中声明的局部变量
为了节省空间,局部变量表中的慒是可以复用的,某个局部变量不再生效,当前慒就可以被再次使用 - 操作数栈
- 栈帧中虚拟机在执行指令过程中存放中间数据的一块区域,也是一种栈式结构
- 在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小
- 帧数据
-
动态链接
-
方法出口:方法在正确或者是异常结束时,当前栈帧就会被弹出,同事程序计数器会指向
上一个栈帧下一条指令的地址,所以在当前栈帧中需要存储此方法出口的地址。 -
异常表:存放的是代码中异常处理的信息,包含了异常捕获的生效范围以及异常发生之后跳转的字节码指令的位置
-
java虚拟机栈随着线程的创建而创建,回收则会在线程销毁时进行,每个线程都有自己的虚拟机栈
java虚拟机栈-栈内存溢出
- java虚拟机如果栈帧过多,占用内存超过了栈内存可以分配的最大最大值就会出现内存溢出
- java虚拟机栈内存溢出时会出现stackoverflowerror
- java虚拟机栈可以通过自己手动来设置大小 -Xss栈大小(字节(默认,必须是1024的倍数),k,m,g)
- 注意事项:
- 同Xss类似,也可以使用-XX:ThreadStackSize调整堆栈大小
格式为:-XX:ThreadStackSize=1024 - hotspot对栈的最大值和最小值有要求(180k-1024m),超出范围会失效,使用默认范围
- 局部变量过多,操作数栈深度过大也会影响内存大小
本地方法栈
- 存储的是native本地方法栈的栈帧
- 在hotspot虚拟机中,本地方法栈和java虚拟机栈实现上使用了同一个栈空间
堆
- 堆内存溢出会抛出OutOfMemoryError
- 如果不设置虚拟机参数。max默认是系统内存的1/4,total默认是系统内存的1/64
- 可自行设置max和total大小,max-Xmx ,total-Xms
- java服务端开发时,建议将xms和xmx设置为相同的值,这样程序启动之后可使用的总内存就是最大内存
,因而不需要向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后
堆收缩的情况。
区域
- used已使用区域
- total分配的剩余可使用区域
- max最大可分配使用区域
当堆中对象增多,total可使用内存不足时,java虚拟机会继续分配max的内存给堆
方法区
- 存储每个类的基本信息(元信息),一般称为instanceklass对象,在类加载阶段完成
- 存放运行时的常量池
- 字符串常量池
-
字符串常量池存储在代码定义的常量字符串内容,比如’123’就会存放在字符串常量池中
-
方法区是一个虚拟概念,每款虚拟机在实现上都有所不同
- jdk7之前的版本将方法区放在堆中的永久代空间,使用虚拟机参数-XX:MaxPermSize=值来控制
- jdk8之后将方法区存放在元空间中,元空间位于直接内存中,使用虚拟机参数-XX:MaxMetaspaceSize
内存溢出同:堆区,outofmemoryerror
- 静态变量在jdk6之前是存放在永久代的方法区中,jdk7之后存放在堆的class对象中,脱离了永久代
直接内存
- 不属于java运行时的内存区域
- jdk1.4之后引入NIO机制,使用直接内存为了解决以下两个问题
- java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用
- 使用直接内存可以减少数据复制的开销
- 内存溢出:OutOfMemory
- 手动调整大小: -XX:MaxDirectMemorySize=大小
垃圾回收
java自动垃圾回收(GC),主要回收堆上的数据
优点:降低程序员实现的难度,降低对象回收bug的可能性
缺点:无法控制内存回收的及时性
- 手动触发垃圾回收
system.gc
调用system.gc并不会马上进行垃圾回收,只是给java虚拟机发送一个垃圾回收的请求
自动垃圾回收
方法区的回收
- 方法区中能回收的内容主要就是不再使用的类
一个类可以被卸载需要同时满足以下三个条件
- 此类的所有实例对象都已经被回收,堆中不存在任何该类的实例对象以及子类对象
- 加载该类的类加载器已被回收
- 该类对象的java.lang.class对象没有在别个地方使用
堆回收
引用计数法和可达性分析法
判断对象是否能被回收
- 引用计数法:为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减
- 优点:
- 实现简单
- 缺点:
- 每次引用和取消引用都需要维护计数器,对系统性能有一定影响
- 无法解决循环引用的问题
- 可达性分析算法:
java是通过可达性分析算法来判断对象是否可以被回收,可达性分析算法将对象分为两类:垃圾回收的根对象(GC Root)和普通对象
对象直接存在引用关系
对象没有引用链可以指向gcroot对象,就可以回收 - GC Root对象
- 线程thread对象,引用线程栈帧中的方法参数,局部变量等
- 系统类加载器加载的java.lang.class对象
- 监视器对象,用来保存同步锁synchronized关键字持有的对象
- 本地方法调用时使用的全局对象
五种对象引用
- 强引用:可达性分析算法
- 软引用:如果一个对象只有软引用关联,当程序内存不足时就会将软引用中的数据进行回收(常用语缓存中)
jdk1.2之后提供了softreference类来实现软引用,使用时softreference时也要被强引用 - 弱引用:整体机制同软引用,区别于弱引用包含的对象在垃圾回收时,不管内存够不够都会被直接回收
jdk1.2之后提供了weakreference类来实现弱引用,弱引用主要在ThreadLoacl使用 - 虚引用(幽灵引用/幻影引用)
不能通过虚引用对象获取到包含的对象,虚引用唯一的用途就是当对象被垃圾回收器回收时可以接收到对应的通知。
java中使用phantomreference实现虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用实现 - 终结器引用
对象需要被回收时,对象将会被放置在finalizer类的引用队列中,并由finalizerThread线程从队列中获取对象,然后执行对象的finalize方法。
垃圾回收算法
- 评判标准
java垃圾回收会通过单独的GC线程来完成,但使用GC算法都会有部分阶段需要停止所有的用户线程,这个过程被称为stop the world(STW)
-
吞吐量
cpu用于执行用户代码的时间与cpu总执行的时间的比值,吞吐量=执行用户代码时间/(执行用户代码时间+GC时间),吞吐量数值越高,垃圾回收效率就越高 -
最大暂停时间
所有在垃圾回收过程中的STW时间最大值,越小越好 -
堆使用效率
不同垃圾回收算法,对堆内存的使用方式是不同的
- 标记清除算法
核心思想分为两个阶段
- 标记阶段:
将所有存活的对象进行标记,java中使用可达性分析算法,从gc root开始通过引用链遍历出所有存活对象 - 清除阶段:
从内存中删除没有被标记的非存活对象 - 优缺点
- 优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可
- 缺点:
- 碎片化问题
内存是连续的,所以在对象删除之后内存中会出现很多细小的可用内存单元,如果我们需要的是一个比较大的空间,这一部分内存会因为过小无法分配 - 分配速度慢
由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表最后才能获得合适的空间
- 复制算法
核心思想:
- 准备两块空间from空间和to空间,每次在对象分配阶段只能使用其中一块空间(from空间)
- 在垃圾回收GC阶段,将from中存活对象复制到to空间
- 将两块空间的from和to名字互换
- 优缺点
优点: - 吞吐量高,只需要遍历一遍存活对象复制到to空间即可,比标记整理算法少了一个遍历过程,因而性能较好,但是不如标记清除算法,标记清除算法不需要对对象进行移动
- 不会发生碎片化
复制算法在复制之后就会将对象按顺序放入to空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间
缺点:
-
每次只能让一半的内存空间来为创建对象使用
- 标记整理算法(标记压缩算法)
是对标记清除算法中容易产生内存碎片问题的一种解决方案
核心思想:
-
标记阶段
同标记清除算法 -
整理阶段
将存活对象移动到堆的一端 -
优缺点
优点:内存使用效率高:整个堆内存都可以使用,不同于复制算法
不会发生碎片化:整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
缺点:整理阶段的效率不高
- 分代GC
将整个内存区域划分为年轻代和老年代
流程:
1.分代回收时,创建出来的对象首先会放入eden(伊甸园区)
2.如果eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为minorGC或者是YoungGC。
YoungGC会把需要eden中和from需要回收的对象回收,把没有回收的对象放入to区。
3.每个对象都有一个计数器,每次回收时未被回收就会加一,当达到阈值(最大15)之后就会放入老年代
4.当老年代空间不足,无法放入新的对象时,先尝试minorGC
5.如果还是不足就会触发fullGC,fullGC(STW停顿较长)会对整个堆进行垃圾回收
- 设计原因
- 可以通过调整年轻代和老年代的比例来适应不用类型的应用程序,提高内存的利用率和性能
- 新生代和老年代使用不同的垃圾回收算法,新生代使用复制算法,老年代使用标记清除或者是标记整理算法,由程序员选择灵活的较高
- 分代设计中允许只使用minorGC,如果能满足对象分配的要求就不需要对整个对象进行fullGC,stw时间就会减少
垃圾回收器
是垃圾回收算法的具体实现
- 垃圾回收器的种类
- 年轻代-serial垃圾回收器
- 单线程串行回收年轻代的垃圾回收器,使用复制算法
- 优点:单cpu处理器下吞吐量非常出色
- 缺点:多cpu下吞吐量不如其他垃圾回收器,堆偏大会让用户线程处于长时间的等待
- 使用场景:java编写的客户端程序或者硬件配置有限的场景
- 老年代-serial垃圾回收器
- 同年轻代serial垃圾回收器,唯一区别是老年代serial使用的是标记整理算法
- 优缺点同
- 使用场景:与serial垃圾回收器搭配使用,或者是在CMS特殊情况下使用
- 开启使用:-XX:UserSerialGC 新生代和老年代都是用串行回收数据
适用于cpu资源比较匮乏的情况下
- 年轻代-ParNew垃圾回收器
- 本质上是对serial在多cpu下的优化,使用多线程进行垃圾回收
- 开启使用:-XX:+UserParNewGC 新生代使用ParNew回收器,老年代使用串行回收器(老年代-serial)
- 优点:多cpu处理器下停顿时间较短
- 缺点:吞吐量和停顿时间不如G1
- 使用场景:jdk8之前与CMS老年代垃圾回收器搭配使用,jdk9之后已弃用
- 老年代-CMS(Concurrent Mark Sweep)垃圾回收器
- cms关注的是系统的暂停时间,允许用户线程与垃圾回收线程在某些步骤中同时执行,减少用户线程的等待时间
- 开启使用:XX:-UserConcMarkSweepGC
- 老年代,标记清除算法
- 优点:系统由垃圾回收出现的停顿时间较短,用户体验好
- 缺点:内存碎片化;退化问题(可能会退化成serial-old这种单线程垃圾回收器);浮动垃圾问题
- 使用场景:大型的互联网系统中用户请求数据量大,频率高的场景,如订单接口,商品接口
- 执行步骤:
-
- 初始标记,用极短的时间标记出GC Root能直接关联的对象
-
- 并发标记,标记所有的对象,用户线程不需要暂停
-
- 重新标记,并发阶段有些对象会发生变化,存在错标,漏标等情况,需要重新标记
-
- 并发清理,清理死亡对象,用户线程不需要等待
- 并发清理,清理死亡对象,用户线程不需要等待
- 年轻代-Parallel Scavenge垃圾回收器
- 是jdk8默认的年轻代回收器,多线程并行回收,关注的是系统的吞吐量,具备自动调整堆内存大小的特点
- 年轻代,复制算法
- 优点:吞吐量高,而且手动可控,为了提高吞吐量,虚拟机会动态调整堆的参数
- 缺点:不能保证单次停顿时间
- 适用场景:后台任务,不需要与用户进行交互,并容易产生大量的对象,如:大数据处理,大文件导出
- 使用:参数-xx:+UseParallelGC或-XX:+UseParallelOldGC可以使用两个组合
- 手动参数:最大暂停时间:-XX:MaxGCPauseMillis=n
吞吐量:-XX:GCTimeRatio=n
自动调整内存大小:-XX:+UseAdapitveSizePolicy=true
- 老年代-Parallel Old垃圾回收器
-
为parallel scavenge设计的老年代版本,利用多线程并发收集
-
老年代,标记整理算法
-
优点:并发收集,多核cpu下效率较高
-
缺点:暂停时间比较长
-
适用场景:与parallel scavenge配套使用
- G1回收器
- jdk9之后的默认垃圾回收器(garbage first)
- 使用垃圾回收器
内存产生的原因
调优的基本方法
吞吐量:-XX:GCTimeRatio=n
自动调整内存大小:-XX:+UseAdapitveSizePolicy=true
[外链图片转存中…(img-XpGQOcaE-1709172967284)]
- 老年代-Parallel Old垃圾回收器
-
为parallel scavenge设计的老年代版本,利用多线程并发收集
-
老年代,标记整理算法
-
优点:并发收集,多核cpu下效率较高
-
缺点:暂停时间比较长
-
适用场景:与parallel scavenge配套使用
- G1回收器
- jdk9之后的默认垃圾回收器(garbage first)
[外链图片转存中…(img-yZnzN6IP-1709172967284)]
- 使用垃圾回收器
内存产生的原因
[外链图片转存中…(img-V5dJMwQ2-1709172967284)]