JVM
字节码、类的生命周期、内存区域、垃圾回收
-
JVM主要功能:
- 解释运行(翻译字节码)
- 内存管理(GC)
- 即使编译(Just - In - Time, JIT)
- 将短时间内常使用到的字节码翻译成机器码存储在内存中,达到减少解释的次数,性能提升,空间换时间
-
JVM的组成:
-
类的生命周期:
-
加载 --> 连接(验证–>准备–>解析)–>初始化–>使用–>卸载
-
加载:
-
类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。
-
程序员可以使用java代码拓展的不同的渠道:
- 本地文件:磁盘上的字节码文件
- 动态代理技术:查询运行时使用动态代理生成
- 通过网络传输的类:早期Applet技术使用
-
类加载器在加载完类之后,JVM会将字节码中的信息保存到内存的方法区中,生成一个InstanceKlass对象,保存类的所有信息
- 这个InstanceKlass对象是用CPP编写的,不能用Java操作
-
同时,JVM还会在堆中生成一份与方法去中数据类似的java.lang.Class对象
-
这个堆中对象的信息是少于InstanceKlass对象的,只有需要用到的信息
-
作用是在Java代码中去获取类的信息以及存储静态字段的数据
-
-
方法区和堆中的这两个对象是通过引用相互关联的
-
对于开发者来说,只需要访问堆中的对象,而不是InstanceKlass对象,这样JVM就能很好地控制开发者访问数据的范围
-
-
连接(验证–>准备–>解析):
- 验证: 验证内容是否满足《Java虚拟机规范》
- 准备:给静态变量赋初值(默认值)
- 如果是final修饰就会这一步直接初始化
- 解析:将常量池中的符号引用替换成指向内存的直接引用
- 符号引用就是字节码文件中使用编号来访问常量池中的内容
-
初始化:
- 执行静态代码块中的代码,为静态变量赋值
- 执行字节码文件中clinit部分的字节码指令
- 以下四种方式会导致类的初始化:
- 访问一个类的静态变量或静态方法,如果是final修饰过的且等号右边是常量,就不会触发初始化
- 调用Class.forName(String clsaaName)
- new一个该类的对象
- 构造代码块{ }优于构造方法先执行
- 执行Main方法的当前类
- clinit指令在特定情况下不会触发:
- 无静态代码块且无静态变量赋值语句
- 有静态变量声明,无静态变量赋值语句
- 静态变量用final修饰
- 还有一点:
- 直接访问父类的静态变量,不会触发子类的初始化
- 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法
-
卸载(在GC中讲到)
-
-
类加载器(ClassLoader):
-
classloader是JVM提供给应用程序去实现获取类和接口字节码数据的技术
-
类加载器的分类:(JDK8及以前)
-
分为两类,一类是java实现,一类是JVM底层源码实现
-
JVM底层CPP实现的
- Bootstrap启动类加载器:加载java中最核心的类
-
Java实现的
- Extension扩展类加载器:允许扩展Java中比较通用的类
- Application应用程序加载器:加载应用使用的类
-
-
双亲委派机制:
-
作用:
- 保证类加载的安全性:避免恶意代码替换掉JDK核心类
- 避免重复加载
-
双亲委派机制指的是:
- 当一个类加载器收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载(看是否在自己的加载路径中)
-
如何去主动加载一个类:
-
使用Class.forName方法,使用当前类的类加载器去加载指定的类
ClassLoader classLoader = Demo1.class.getClassLoader();
-
获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载
Class<?> clazz = classLoader.loadClass("com.xxx.xxx");
-
-
-
打破双亲委派机制:
-
自定义类加载器:
- 关键就是重写loadClass的findclass方法,将双亲委派机制的代码去除
- Tomcat就是通过这种方式实现应用之间的类隔离
-
线程上下文类加载器:
-
JDBC的例子:
-
SPI就是使用的线程上下文中保存的类加载器进行类的加载,这个类一般是应用程序类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
-
这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制
-
当然JDBC这个案例有不同的看法:
- 从SPI角度是打破了双亲委派机制的
- 如果从类加载器的角度去看,又是符合双亲委派机制的
-
-
OSGi模块化
-
-
JDK8之后的类加载器:
- 启动类加载器使用java编写,继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件
- 扩展类加载器被替换成平台类加载器,继承自BuiltinClassLoader
-
-
运行时数据区:
-
定义:JVM在运行java程序过程中管理的内存区域
-
分类:
-
栈:
-
JVM栈:
-
JVM栈存的是java方法,每一个方法的调用使用一个栈帧来保存
-
JVM栈存的栈帧多了是会内存溢出的
-
栈帧的组成:
- 局部变量表
- 保存的内容有:实例方法的this对象,方法的参数,局部变量
- 栈帧的局部变量表是一个数组,数组中每一个位置称之为槽(slot),浮点数类型占用两个slot,其他类型占用一个slot
- 为了节省空间,表中的slot是可以复用的,一旦某个局部变量不再生效,当前槽可以复用
- 操作数栈
- 执行指令的过程中,用来存放临时数据的一块区域
- 编译器就确定了栈的最大深度
- 帧数据
- 保存了直接引用到内存引用的映射关系(动态链接)
- 保存了方法出口
- 保存了异常表
- 局部变量表
-
-
本地方法栈:
- 本地方法栈存的是native本地方法的栈帧
- 在Hotspot虚拟机中,JVM栈和本地方法栈实现上使用了同一个栈空间
-
-
堆:
-
一般程序堆内存是看见最大的一块内存区域。
-
创建的对象都存在堆上
-
对象放多了也会内存溢出
-
堆空间有三个需要的值:
- used:当前已使用的堆内存
- total:JVM已经分配的可用堆内存
- max:JVM可以分配的最大堆内存
-
-
方法区:
- JDK7及之前,方法区是放在堆里面的永久代空间
- JDK8及之后,方法区放在元空间中,元空间位于OS维护的直接内存中
- 方法区存放了三部分内容:
- 类的元信息:类的基本信息(InstanceKlass对象)
- 运行时常量池:字节码文件中的常量池内容
- 字符串常量池(放在堆里面):字符串常量
-
-
直接内存;
- 并不属于java运行时的内存区域
- 引入了NIO机制,直接使用直接内存
- 主要解决了以下两个问题:
- java堆中的对象如果还不再使用要回收,回收时会影响对象的创建和使用
- IO操作比如读文件,要先把文件读入内存再把数据复制到堆中。现在直接放入直接内存,同时堆上维护直接内存的引用,减少数据复制的开销
-
GC:
-
PC,JVM栈,本地方法栈这三个是线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法后就会弹出栈并释放掉对应内存
-
方法区的回收:
- 方法区主要就是回收不再使用的类:
- 判定一个类是否可以被回收的三个条件:
- 该类的所有实例对象都被回收,在堆中没有其实例对象及子类对象
- 加载该类的类加载器已经被回收
- 该类的java.lang.Class对象没有在任何地方被引用
- 手动触发回收:System.gc();
-
堆回收:
-
java对象的是否能被回收,是根据对象是否被引用来决定的
-
常见的判断方法:
- 引用计数法:
- 引用计数法为每个对象维护一个引用计数器,初始值为0,当对象被引用时加1,取消引用减1
- 优点是实现简单
- 缺点:
- 加一减一都需要维护计数器,对系统系统有一定影响
- 存在循环引用问题,对象无法回收
- 可达性分析算法:
- 可达性分析将对象分为两类:垃圾回收的根对象和普通对象,对象之间存在引用关系
- GC Root 对象:
- 线程Thread 对象
- 系统类加载器加载的java.lang.Class对象
- 监视器对象,用来保存同步锁synmchornized关键字持有的对象
- 本地方法调用使用的全局对象
- 引用计数法:
-
五种对象引用:
-
强引用 :可达性算法中描述的对象引用就是强引用
-
软引用:
-
如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收
-
软引用常用于缓存中
-
SoftReference类实现软引用
-
把对象包装进软引用对象中:
new SoftReference<对象类型> (对象)
-
-
弱引用:
- 机制与软引用相似,区别是,GC的时候,不管内存够不够都会直接被回收
- WeakReference类实现弱引用
- 弱引用主要在ThreadLocal中使用
-
虚引用:
-
终结器引用:
-
-
GC算法:
-
GC的过程会通过单独的GC线程来完成,不管是用哪种GC算法,都会有部分阶段需要停止所有用户线程,这被称为STW(stop the world)
-
判断GC算法优秀的三个指标:
- 吞吐量:
- 执行用户代码的时间/(执行用户代码的时间 + GC时间)越大越好
- 最大暂停时间:
- STW时间的最大值
- 堆使用效率:
- 吞吐量:
-
三个指标不可兼得,一般来说:
堆内存越大,最大暂停时间越长。想要减少最大暂停时间,就会降低吞吐量
-
不同的GC算法适用不同的场景
-
标记 - 清除算法 —> 复制算法 —> 标记 - 整理算法 —> 分代GC
-
分代GC:
-
结合了前几种算法
-
分代回收时,创建出的对象,首先会被放在Eden区
-
Eden区装满后,就会触发年轻代的GC,称为Young GC 或 Minor GC
-
Minor GC 会把Eden区和From区中需要回收的对象回收,把没有回收的对象放进to区
-
然后S0变成to区,S1变成from区
-
每次Eden区装满后,就会触发 Minor GC,对象的年龄会+1
-
对象的年龄达到阈值(max = 15)会被晋升到老年代
-
当老年代空间不足时,就会触发Full GC,对整个堆进行GC
-
-
垃圾回收器:
-
垃圾回收器是GC算法的具体实现
-
由于垃圾回收器也是分为年轻代和老年代,除G1外其他的垃圾回收器必须成对组合进行使用
-
JDK9后默认的垃圾回收器是G1 垃圾回收器
-
Parallel Scavenge 关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小
-
CMS关注暂停时间,但是吞吐量方面会下降
-
G1 垃圾回收器的设计目标就是将上面两种垃圾回收器的优点融合:
- 支持巨大的堆空间回收,并有较高的吞吐量’
- 支持CPU并行垃圾回收
- 允许用户设置最大暂停时间
-
G1的整个堆被划分为多个大小相等的区域,区域不要求是连续的,区的大小是堆空间大小/2048
-
G1的GC方式有两种:
- Young GC
- Mixed GC
-
GC流程:
- 先创建的对象放在Eden区。
- 当G1判断年轻代区不足(max默认60%),无法分配对象时会执行Young GC
- 标记出Eden和Survivor区中的存活对象
- 根据配置的最大暂停时间选择某些区将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区
- 当某个对象的年龄达到阈值(默认15),会被放到老年代
- 部分对象如果大小超过区的一半,会被直接放到老年代,这类老年代被称为Humongous区
- 对象的总堆占有率达到阈值(默认45%),会触发Mixed GC。回收部分老年代和全部年轻代以及大对象区。采用复制算法完成
- G1对老年代的清理会选择存货度最低的区域来进行回收,这样就可以保证回收效率最高,就是G1(Garbage first)名字的由来
- 最后的清理阶段使用复制算法,不会产生内存碎片
- 如果清理过程中发现没有足够的空区存放转移的对象,会出现Full GC
Survivor区中(年龄+1),清空这些区 - 当某个对象的年龄达到阈值(默认15),会被放到老年代
- 部分对象如果大小超过区的一半,会被直接放到老年代,这类老年代被称为Humongous区
- 对象的总堆占有率达到阈值(默认45%),会触发Mixed GC。回收部分老年代和全部年轻代以及大对象区。采用复制算法完成
- G1对老年代的清理会选择存货度最低的区域来进行回收,这样就可以保证回收效率最高,就是G1(Garbage first)名字的由来
- 最后的清理阶段使用复制算法,不会产生内存碎片
- 如果清理过程中发现没有足够的空区存放转移的对象,会出现Full GC
-
-
-
-