JVM 运行流程
javac 命令将 java 文件编译成 class 文件,jvm 通过类加载器(ClassLoader)将 class 文件从磁盘中加载到运行时数据区,命令解释器 执行引擎 将字节码翻译成底层操作指令交给 CPU 执行,这个过程需要调用其他语言的接口(C++)本地库接口
内存区域划分
虚拟机栈(线程私有)
每个方法在执行的同时创建一个栈帧(Stack Frame),存储局部变量表、操作栈等
记录方法之间的调用关系,不记录方法的具体指令
本地方法栈(线程私有)
跟虚拟机栈类似,给本地方法使用,Java1.8 之后跟 虚拟机栈合并了
栈容量由参数 Xss(栈大小)设置,
- 当栈帧深度超过限度是会报错,StackOverFlow异常
- 当内存超过限制会报 OMM异常 OOM(out of memory)
堆(线程共享)
- 占用内存最大的区域
堆的大小可以设置,Xms(最小启动内存)memory start、Xmx(最大运行内存)memory max
超出限制会报异常 OOM(out of memory)
方法区(线程共享)
- 存放类信息,继承关系,实现哪些接口
- 被final修饰的常量(成员属性),静态变量
- 方法的内部指令、访问权限,返回值,参数
JDK7 使用永久代实现方法区,JDK8 使用 元空间
对于 HotSpot 来说JDK8 的元空间内存属于本地内存,并且将字符串常量池移动到了堆上
程序计数器(线程私有)
- 占用内存最小
存放执行到了哪条指令(地址)
类加载机制
图来源于《深入理解Java虚拟机》
加载
在方法区中生成类对象
连接
验证: 验证格式释放符合规范要求
准备: 为类中的静态变量分配内存,并初始化值,例如 有这样一个成员变量 private static int num = 111,初始化的值是 0,不是 111
解析:初始化常量,将常量池内的符号引用替换为直接引用
初始化
初始化静态变量,执行静态代码块
双亲委派模型
对于Java虚拟机来说只有两中加载器
- 启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机自身的一部分
- 其他所有的类加载器,使用Java实现,并且全部继承自抽象类 java.lang.ClassLoader
对于Java开发者来说有三层加载器
- 启动类加载器(BootstrapClassLoader)加载标准库中的类(String ArrayList)
- 扩展类加载器(ExtensionClassLoader)加载扩展的类
- 应用程序类加载器(ApplicationClassLoader)加载自己写的类,第三方库中的类
- 自定义类加载器
前三层类加载器是JVM 自带的,这三种类加载器的搜索目录不同,约定了父子关系,依次向上询问是否已加载
一、加载自己写的类
先从ApplicationClassLoader开始,再到 ExtensionClassLoader,再到 BootstrapClassLoader,到了顶层就开始从这一层查找,因为是自己写的类,当前层的类加载器管理的目录中没有这个类,所以就返回到下一层ExtensionClassLoader,继续找,发现还是找不到,继续返回下一层,找到了就加载,找不到就会报 ClassNotFoundException 异常
二、加载标准库中的类
先从ApplicationClassLoader开始,再到 ExtensionClassLoader,再到 BootstrapClassLoader,到了顶层就开始从这一层查找,找到了就加载
双亲委派模型的优点
- 防止程序员自己写的类将标准库中的类覆盖了,例如,写了一个完全限定名是 java.lang.String 的类,那么加载的时候,到了 BootstrapClassLoader 时会加载标准库中的String类,这样就避免了标准库的类被覆盖
垃圾回收机制(GC)
对于虚拟机栈、本地方法栈、程序计数器这个三部分而言,当线程结束或者方法结束他们的生命也就结束了,对于方法区来说,里面存放的是类对象,对于GC 不迫切,所以,我们垃圾回收的关注点在 堆
垃圾回收就是对象的回收
死亡对象的判断算法
1、引用技术算法
Java 未使用这种方法,Python 使用的就是这种算法
每当有一个引用指向这个对象,这个对象的引用计数器就+1,当计数器为 0 时,就代表是死亡对象了
缺陷:无法解决 循环引用 的问题,当多个线程对同一个引用计数器修改时,需要考虑线程安全问题
2、可达性分析
以一些特殊的变量(对象)为起点,称为 GC Roots,只有当对象可以从起点出发访问到的,我们才称为可
达,而那些不可达的对象就会被回收
可以作为GC Roots 的对象
1、虚拟机栈中的引用的对象
2、方法区中静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中 JNI(Native 方法)引用的对象
关于可达,就拿二叉树来说,只要得到根节点就能访问到这颗树的每个节点
垃圾回收算法
1、标记-清除算法
先标记出哪些对象需要回收,在标记完成后统一进行标记对象的回收,直接在原地回收,对于还存活的对象不做处理,所以会带来内存碎片,效率也不高
2、复制算法
将内存空间分成大小相等的两部分,一次只使用一部分,当需要进行垃圾回收的时候,会将此块区域还存活的对象复制到另一块上,最后把原本那块直接全部清理掉
优点:算法实现简单,运行高效,并且不会带来内存碎片的问题
3、标记-整理算法
整理前:
假如2,4 是需要回收的对象,则把3,5复制到1的后面,这样就不会产生内存碎片
缺点:效率不高
4、分代算法
将内存分区域,不同区域使用上述算法中的某种算法
将 Java 堆分为新生代 和 老年代,新生代分为 Eden(伊甸区)、Survivor From、Survivor To,其中这三种空间的内存容量比值为 Eden:Survivor From:Survivor To = 8:1:1
每经历一轮 GC,存活的对象的年龄就加1,多大的年龄会进入老年区,不同的JVM有不同的参数,所以我们不必关注这点,而是关注策略本身即可
- 新生成的对象放在 Eden 里面
- 一般来说 Eden 里的对象活不过一轮 GC,所以适用于 复制算法
- 经过一轮GC的对象就会被放在 Survivor To 区,在经过一轮 GC,Eden 和 Survivor To 中存活的对象就会被搬到 Survivor From 中,所以都有些对象就会反复在 Survivor From 和 Survivor To 中,最后经过若干次的GC,Survivor 中的达到一定年龄的对象就会进入 老年代
- 老年代中的对象一般存活率比较高,GC 的频率也会降低,所以不适合复制算法,而是选择标记-整理算法
注意:如果有一个很大的对象,这个对象会直接存在于老年代,因为很大的对象不适合复制算法