深入理解java虚拟机
JVM内存管理
JVM 内存布局
- JVM 堆中的数据是共享的,是占用内存最大的一块区域。
- 可以执行字节码的模块叫作执行引擎。
- 执行引擎在线程切换时怎么恢复?依靠的就是程序计数器。
- JVM 的内存划分与多线程是息息相关的。像我们程序中运行时用到的栈,以及本地方法栈,它们的维度都是线程。
- 本地内存包含元数据区和一些直接内存。
虚拟机栈
在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。
一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。每个栈帧,都包含四个区域:
- 局部变量表
- 操作数栈
- 动态连接
- 返回地址
本地方法栈
是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。
程序计数器
存的,就是当前线程执行的进度
还存储了当前正在运行的流程,包括正在执行的指令、跳转、分支、循环、异常处理等。
堆
是 JVM 上最大的内存区域
存储对象
垃圾回收,操作的对象就是堆。
随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)
对象创建的时候,到底是在堆上分配,还是在栈上分配呢?
对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。
对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。
我们上面提到,每个线程拥有一个虚拟机栈。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。
注意,像 int[] 数组这样的内容,是在堆上分配的。数组并不是基本数据类型。
元空间
方法区
存储类的信息、常量池、方法数据、方法代码
总结
JVM 的运行时区域是栈,而存储区域是堆。
类的加载机制
类加载过程
加载、验证、准备、解析、初始化。
加载
将外部的 .class 文件,加载到 Java 的方法区内
验证
不符合规范的将抛出 java.lang.VerifyError 错误
准备
将为一些类变量分配内存,并将其初始化为默认值。此时,实例对象还没有分配内存,所以这些动作是在方法区上进行的。
解析
将符号引用替换为直接引用的过程
你可以把通讯录里的女友手机号码,类比为符号引用,把面对面和你吃饭的人,类比为直接引用。
解析阶段负责把整个类激活
- 类或接口的解析
- 类方法解析
- 接口方法解析
- 字段解析
初始化
初始化成员变量
开始执行一些字节码
static 语句块,只能访问到定义在 static 语句块之前的变量
JVM 会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕
1 | public class A { |
1 | 1 |
static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。
类信息会被存放在方法区,在同一个类加载器下,static 代码块只会执行一次
类加载器
几个类加载器
- Bootstrap ClassLoader 启动类加载器 任何类的加载行为, 加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。随着 JVM 启动。
- Extention ClassLoader 扩展类加载器 加载 lib/ext 目录下的 jar 包和 .class 文件 是个 Java 类,继承自 URLClassLoader
- App ClassLoader Java 类的默认加载器 加载 classpath 下的其他所有 jar 包和 .class 文件 我们写的代码,会首先尝试使用这个类加载器进行加载。
- Custom ClassLoader 自定义加载器,支持一些个性化的扩展功能
双亲委派机制
除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。
首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。
这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效。
一些自定义加载器
- tomcat
- SPI 机制 JDBC加载驱动类
小结
Java 自带的三个类加载器
main 方法的线程上下文加载器,其实是 Application ClassLoader
一般情况下,类加载是遵循双亲委派机制的
垃圾回收
GC Roots
在发生 GC 的时候,一个对象,JVM 总能够找到引用它的祖先。找到最后,如果发现这个祖先已经名存实亡了,它们都会被清理掉。而能够躲过垃圾回收的那些祖先,比较特殊,它们的名字就叫作 GC Roots。
GC Roots 包括:
- Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用。
- 所有当前被加载的 Java 类。
- Java 类的引用类型静态变量。
- 运行时常量池里的引用类型常量(String 或 Class 类型)。
- JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类。
- 用于同步的监控对象,比如调用了对象的 wait() 方法。
- JNI handles,包括 global handles 和 local handles。
GC Roots 大体可以分为三大类
- 活动线程相关的各种引用。
- 类的静态变量的引用。
- JNI 引用。
典型 OOM 场景
OOM 的全称是 Out Of Memory
最常见的还是发生在堆上
原因
- 内存的容量太小了,需要扩容,或者需要调整堆的空间。
- 错误的引用方式,发生了内存泄漏。没有及时的切断与 GC Roots 的关系。比如线程池里的线程,在复用的情况下忘记清理 ThreadLocal 的内容。
- 接口没有进行范围校验,外部传参超出范围。比如数据库查询时的每页条数等。
- 对堆外内存无限制的使用。这种情况一旦发生更加严重,会造成操作系统内存耗尽。
典型的内存泄漏场景,原因在于对象没有及时的释放自己的引用。比如一个局部变量,被外部的静态集合引用。
垃圾回收(上)
当我们的内存空间达到一定条件时,会自动触发。这个过程就叫作 GC
负责 GC 的组件,就叫作垃圾回收器
垃圾回收器 只需要保证不要把正在使用的对象给回收掉就可以
先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后删除
垃圾回收只与活跃的对象有关,和堆的大小无关
常见的内存回收算法
- 复制算法(Copy)
复制算法是所有算法里面效率最高的,缺点是会造成一定的空间浪费。
- 标记-清除(Mark-Sweep)
效率一般,缺点是会造成内存碎片问题。
- 标记-整理(Mark-Compact)
效率比前两者要差,但没有空间浪费,也消除了内存碎片问题。
所以,没有最优的算法,只有最合适的算法。
大部分对象,可以分为两类:
- 大部分对象的生命周期都很短;
- 其他对象则很可能会存活很长时间。
我们把死的快的对象所占的区域,叫作年轻代(Young generation)。把其他活的长的对象所占的区域,叫作老年代(Old generation)。
年轻代
年轻代使用的垃圾回收算法是复制算法
因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。
老年代
一般使用“标记-清除”、“标记-整理”算法
年轻代垃圾回收器
(1)Serial 垃圾收集器
处理 GC 的只有一条线程,并且在垃圾回收的过程中暂停一切用户线程。
(2)ParNew 垃圾收集器
(3)Parallel Scavenge 垃圾收集器
老年代垃圾收集器
- (1)Serial Old 垃圾收集器
与年轻代的 Serial 垃圾收集器对应,都是单线程版本,同样适合客户端使用。
年轻代的 Serial,使用复制算法。
老年代的 Old Serial,使用标记-整理算法。
- (2)Parallel Old
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
- (3)CMS 垃圾收集器
CMS(Concurrent Mark Sweep)收集器是以获取最短 GC 停顿时间为目标的收集器
Java8 之后,被G1 等垃圾回收器替换掉
查看java默认使用垃圾回收器
java -XX:+PrintCommandLineFlags -version
以下是一些配置参数:
- -XX:+UseSerialGC 年轻代和老年代都用串行收集器
- -XX:+UseParNewGC 年轻代使用 ParNew,老年代使用 Serial Old
- -XX:+UseParallelGC 年轻代使用 ParallerGC,老年代使用 Serial Old
- -XX:+UseParallelOldGC 新生代和老年代都使用并行收集器
- -XX:+UseConcMarkSweepGC,表示年轻代使用 ParNew,老年代的用 CMS
- -XX:+UseG1GC 使用 G1垃圾回收器
- -XX:+UseZGC 使用 ZGC 垃圾回收器
线上使用最多的垃圾回收器,就有 CMS 和 G1,以及 Java8 默认的 Parallel Scavenge。
- CMS 的设置参数:-XX:+UseConcMarkSweepGC。
- Java8 的默认参数:-XX:+UseParallelGC。
- Java13 的默认参数:-XX:+UseG1GC。
STW ( Stop the world)
如果在垃圾回收的时候(不管是标记还是整理复制),又有新的对象进入怎么办?
最好的办法就是暂停用户的一切线程
在这段时间,不能 new 对象的,只能等待。表现在 JVM 上就是短暂的卡顿
这叫stw
垃圾回收(下)
G1
…
实战
…
进阶
…