JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机:是指通过软件模拟的具有完整硬件功能、运行在一个完全隔离的环境中完整计算机系统
1.JVM的内存区域划分
jvm是一个java进程 每一个java进程就是一个jvm实例
一个进程运行过程中 就要从操作系统这里申请到一些内存资源 jvm也是如此 所以用一块内存供java代码执行时使用 jvm把这一块内存 有划分几个区域作为不同的用途
栈里面方法的调用关系 局部变量(使用的是递归调用)
栈空间和程序计数器 是每一个线程各有一份
每个线程 都是一个独立的执行逻辑 就能得到单独记录执行到哪里
下面有一段代码
class Test{public int n = 20;public static int a = 10;
}
public static void main(){Test t = new Test();
}
n是普通的成员变量 就是包含在new对象内部 堆上的
a是一个静态成员变量 是包含在类对象中方法区的
t是一个局部变量(引用类型) 栈上
这里栈上的t保存了堆上new Test()的内存地址
2.JVM类加载机制
把类从硬盘加载到内存中 java程序最开始就是写一个java文件编译成.class文件(字节码) 运行java程序JVM就会读取class文件 把文件内容 放到内存中并构成class对象
简单描述下类加载
- 加载找到class文件 打开文件 读取文件内容并尝试解析格式
在java代码就是直接使用类 - 验证 检查当前.class文件格式 是否符合要求
,这一阶段的目的是确保Class文件的字节 流中包含的信息符合《Java虚拟机
规范》的全部约束要求,保证这些信 息被当作代码运行后不会危害虚拟机自身的安全。 - 准备 给类对象分配内存
最终目的是构造出完整的类对象 分配内存+初始化
分配出来的内存空间 内容就是权威0的值(此时此刻 类对象上static成员也就是都是0) - 解析 主要是初始化类对象中涉及到的一些字符串常量
- 初始化对类对象进行更具体的初始化操作初始化静态成员 执行静态代码块 加载父类
双亲委派模型(描述类加载过程中 如何找到.class文件)
JVM加载.class文件的时候需要用到"类加载器"模块
JVM中自带了三个类加载器
Bootstrap ClassLoader 负责加载标准库中的类
Extension ClassLoader 负责加载JVM扩展的库
Application ClassLoader 负责加载第三方库(mysql jdbc driver servlet jackson 等)
- 从Application ClassLoader开始
不会立即就搜索第三方库的目录 而是先把加载任务委派给父亲 让父亲先尝试加载 - 到了Extension ClassLoader
也不会立即搜索扩展库的目录 而是继续把任务派给父亲 让父亲先尝试加载
3)再到Booststrap ClassLoader
同样不会立即搜寻标准库 但是Bootstrap ClassLoadr没有父亲了就只能自己搜索 如果没找到这个类就会交给孩子完成 -> Extension ClassLoader - 任务回到Extension ClassLoader
如果找到了这个类 就进行后续加载
如果没找到这个类就会交给孩子完成
5)任务回到Application ClassLoader
如果找到了类就进行后续加载
如果没找到就会抛出一个异常
双亲委派模型 工作就是找.class文件 每个类加载器都有自己的"父亲"
一个类什么时候才会被加载?
类的加载使用的是懒汉模式 用到了才加载
分为以下三种情况
- 构造类的实例
- 使用了类的静态方法/静态属性
- 子类的加载会触发父类
类加载之后 后续就不用加载了
类卸载 类的卸载
一般来说类卸载是属于特殊情况
类加载之后就不会考虑卸载 一直保持到程序运行结束
但有时我们服务器需要打 热补丁 需要卸载操作
3.JVM的垃圾回收机制(GC)
java的垃圾回收机制 会自动判定 某个内存是否会继续使用(如果不会 就会把内存当初垃圾 自动把垃圾释放掉)
垃圾回收 对于java来说 回收的是对象而不是字节(GC不是判定某个字节是不是垃圾而是判定对象是不是垃圾 进一步去进行回收的)
JVM中有几个内存区域 GC回收是回收的是那里的对象?
JVM的内存区域有栈 堆 方法区 栈空间不需要GC回收栈里面包含很多"栈帧"每一个栈帧对应一个方法 该方法执行结束 此时这个栈帧就销毁了 然后在栈上的局部变量就自然销毁了
程序计数器同理 线程销毁 也自然跟着销毁
方法区 类对象 很少会涉及对象的卸载
所以 堆 GC的主要战场
Java垃圾回收 分两步
- 判定对象是否是"垃圾"
如果一个对象 在后续代码中不再被使用就可以看作是垃圾了 一个对象如果没有任何引用指用它就可以认为是垃圾(Java中要使用一个对象 只有一种途径 就是先创一个引用指向它 然后通过引用访问对象的属性/方法)
例如:
public void test(){T t = new T();t.function();
}
test();
t是一个局部变量此时 test方法执行完毕之后 t 就自然销毁 此时 new T() 对象就没有引用了 此时 这个对象就是垃圾
判定对象是否是垃圾 就是看这个对象是否有引用指向它 那么如何看对象是否有引用指向它呢?
思路一: 引用计数
给这个对象里面安排一个计数器 每次有引用指向它 就把计数器加一 每次引用被销毁 计数器减一 当计数器为0 的时候就说明这个对象是垃圾了
例如:
class Test{int m;int n;
}
Test a = new Test();
Test b = a;
Test c = b;
对象中 会专门留一个空间来存储引用的个数
此处a这个引用指向Test
b指向引用a
c指向引用b
然后Test的就被引用了三次 所以计数器加三
然后我们将
a = null;
此时引用减少一次 计数器减一
当剩下两个引用都置为null时 这个对象就是垃圾了
这就是引用计数方案 但存在两个明显的缺陷
- 空间利用率比较低 浪费更多的内存空间
如果给引用计数分配了2各字节 对象本体才4各字节 引用计数就浪费了50%的空间 代码中都是小对象 并且数量多时浪费就很明显了 - 可能存在循环引用的问题 且对象不能被正确识别为垃圾
例如:
class Test{public Test t;
}
//第一步
Test a = new Test();
Test b = new Test();
//第二步
a.t = b;
b.t = a;
//第三步
a = null;
b = null;
第一步时
第二步时
第三步时
此时这两个对象计算器不为0 不能被当作垃圾 于此同时 想要使用对象1 就要访问 对象2 想使用对象2就要访问道对象1 谁都用不了 谁有无法释放和死锁非常相似
方案二: 可达性分析 (这是java中实际采取的方式)
JVM 首先会从现有代码中能直接访问到的引用出发尝试遍历所有能访问的对象 只要对象能访问到就会标记成可达 完成所有遍历之后 不能访问到的对象就是不可达 就相当于垃圾了
gc root(从那些对象开始出发扫描?)
- 栈上的局部变量
- 常量池的引用
- 方法区中的静态成员
例如:
这样一颗二叉树结构
root = a
root.left = b
root.left.left = d
以此类推可以通过a进行访问到后续任何一个节点
如果此时令root.left.left = null 此时d就会空无法通过root遍历d就是不可达得了 此时d引用的对象就会被垃圾回收
以上就是可达性分析 和引用计数不同的时它消耗的是时间 通过引用计数可以非常快的知道对象是否是垃圾但是会消耗大量的空间
而可达性分析不会引入额外的空间开销 但进行遍历需要消耗时间
可达性分析的扫描是持续的 周期性的
- 释放对象的内存
那我们确定了垃圾 又该如何清理垃圾 释放对象呢?
(1) 标记清除
直接释放对象 但是可能引起内存碎片
这是因为申请内存的时候都是申请连续的内存空间
释放内存就可能会破坏原有的连续性 导致内存申请不了 如果不处理内存碎片会随着程序的运行越来越多 越来越碎内存就会变得很难申请了
(2) 复制算法
复制算法 通过冗余的空间 把有效对象复制到另一个部分空间 避免内存碎片
把一个内存 分成两份用一份 删除一份
把左侧区域中有效的对象复制到右侧
使用右侧区域使用一段时间后存在很多对象然后把有效的对象在复制到左侧然后把左侧的对象全部同意释放
例如:
假设对象2是垃圾对象然后把13复制到右边内存再释放掉左侧内存
这里如果复制的内容多 内存开销很大内存率不高
(3) 标记整理
顺序表删除元素
假设34是
把5对象往前移两个单位内存就相当于删除了
这一方法解决了内存利用率低的问题但是
搬运的元素成本比较高
(4) 分代回收
Java代码中 对象分成两个大类
1.生命周期特别长
2.生命周期特别短
所以可以按照对象的年龄来定制不同的回收策略
GC就是周期性扫描 一个对象每经历一轮GC就看作长了一岁
新生的对象就在伊甸区 这里的对象大部分无法活过第一轮GC 第一轮gc经过之后还剩下的对象 就会被通过复制算法复制到幸存区
幸存区继续使用复制算法 如果这个对象在幸存区经过了许多轮依旧没有清理掉 就进入了老年代了
新生代的扫描频率比较高
老年代的扫描频率比较低
新生代每一轮gc留下的有效对象不过 使用复制算法开销不大
老年代不会有频繁的对象销毁对此 使用标记整理开销不大
还有一个特殊情况如果对象体积特别大就会直接进入老年代