一, 运行时数据区域
1, 程序计数器:
当前线程执行字节码的行号指示器(通过改变计数器的值来选择下条需要执行的字节码指令)
每个线程有独立的程序计数器(线程私有, 为了切换线程时能恢复到挣钱的执行位置)
如果执行 java 方法, 计数器记录正在执行的字节码指令地址. 如果执行的是 Native 方法, 计数器为空.
唯一没规定任何 OutOfMemoryError 情况的区域.
2, 虚拟机栈
为执行 Java 方法服务
线程私有, 声明周期跟线程一致
一个 Java 方法执行到结束的过程: 栈帧从入栈到出栈的过程
栈帧存储局部变量表(包括基本数据类型和对象的引用类型), 操作栈, 动态链接, 方法出口等信息
异常: 线程请求的栈深度大于虚拟机允许的深度, 抛出 StackOverflowError. 虚拟机动态扩展过程中无法申请到足够的内存, 会抛出 OutOfMemoryError 异常.
3, 本地方法栈
为虚拟机用到的 Native 方法服务
也会抛出 StackOverflowError 和 OutOfMemoryError 的异常
4,Java 堆
用来存储对象的实例
所有线程共享的一块内存区域
从内存回收的角度可以分为新生代和老年代
5, 方法区
存放被虚拟机加载的类信息, 常量, 静态变量等
线程共享
6, 运行时常量池
方法区的一部分
存放编译期生成的各种字面量和符号引用
二, 垃圾回收(GC)
哪些内存需要回收
什么时候回收
怎么回收
1, 判断对象是否存活
1, 引用计数法:
给 Java 对象添加一个引用计数器, 每当有一个地方引用它时, 计数器 + 1; 引用失效则 - 1. 当计算器不为 0 时, 判断对象存活
缺点: 如果两个对象相互循环引用时, 因为计算器不为 0, 不能被回收. 实际上对象应该被回收.
2, 可达性分析算法:
(1)原理:
把 "GC Roots" 的对象作为起点, 然后向下搜索, 搜索所走过的路径称为引用链, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的.
(2)可作为 GC Root 对象:
虚拟机栈 (栈帧中的本地变量表) 中引用的对象
方法区中类静态属性引用的对象
方法区常量引用的对象
本地方法栈中 JNI 引用的镀锡
3, 判断一个类可回收:
该类所有实例对象被回收
加载该类的 ClassLoader 已经被回收
该类对应的 Class 对象没被引用, 无法通过反射访问该类的方法
2, 引用类型
通过引用的强度分为强, 软, 弱, 虚四种引用类型
强应用: 一般 Object b=new Object()这类引用, 只要强引用存在, GC 就不会回收被引用的对象.
软引用: 系统在发生内存溢出之前, 会将这些对象二次回收. 如果还没足够内存, 才会抛出内存溢出异常. 通过 SoftReference 来实现.
弱引用: GC 回收时, 无论内存是否足够, 都会收会被弱引用关联的对象. 通过 WeakReference 来实现.
虚引用: 作用是在对象被回收时收到一个系统通知. 通过 PhantomReference 来实现.
3, 垃圾收集算法
1, 标记 - 清除算法: 标记出所有需要回收的对象, 标记完统一清除被标记的对象.
缺点:
标记和清理的效率不高
标记清除后会产生大量不连续的内存碎片
2, 复制算法: 将内存分为大小相等的两块, 每次只用一块, 当这一块内存用完, 将存活对象复制到另一块, 将使用过的内存一次清理.
优缺点:
不会产生内存碎片的问题
缺点是将内存缩小到了之前的一半
在对象存活率高时进行多次复制操作, 效率会低.
3, 标记 - 整理算法: 标记需要回收的对象, 将存活对象向一端移动(整理), 清理掉可回收的对象.
4, 分代收集算法: 根据对象存活周期不同, 将 Java 堆内存分为新生代和老年代.
新生代: 只有少量对象存活, 使用复制算法.
老年代: 大量对象存活, 使用标记清除或者标记整理算法.
三, 类加载机制
1, 类加载时机
1, 定义:
把 Class 文件加载到内存中, 并对数据进行校验, 解析和初始化, 行成可被虚拟机直接使用的 Java 类型. 类从被加载到虚拟机内存中开始, 到卸载出内存结束.
2, 生命周期:
加载
验证
准备
解析
初始化
使用
卸载 加载, 验证, 准备, 初始化, 卸载的顺序确定.
3, 需要对类进行初始化的场景
new 实例化对象, 读取或设置类的静态字段, 调用类的静态方法(被 final 修饰, 已在编译期将结果放入常量池的静态字段除外)
对类进行反射
初始化一个类, 若父类还没初始化, 先触发父类的初始化
需指定一个执行的主类(包含 main 方法的类), 虚拟机先初始化该类
JDK1.7 动态语言, MethodHandle 实例解析结果 REF_getStatis,REF_putStatis,REF_invokeStatis 的方法句柄
4, 不会方法初始化的场景:
所有引用类的方式不会触发初始化, 例如子类引用父类的静态字段, 只触发父类初始化.
5, 接口初始化和类初始化的区别:
接口初始化时, 不要求其父类接口全部完成初始化.
2, 类加载过程
包括加载, 验证, 准备, 解析, 初始化 5 步
1, 加载:
通过类全限定名获取定义该类的二进制字节流
将字节流的静态存储结构转换为方法区的运行时数据结构
内存中生成该类的 Class 对象, 作为访问该类数据的入口
2, 验证:
文件格式验证, 验证字节流是否符合 Class 文件格式规范
元数据验证, 对字节码描述的信息进行语义分析
字节码验证, 确定语义合法
符号引用验证, 对常量池符号引用校验
3, 准备: 为类变量 (static 修饰的变量) 分配内存并设置变量初始值
4, 解析: 将常量池符号引用替换为直接引用 (直接指向目标的指针) 的过程
5, 初始化: 开始执行类中定义的 Java 代码
3, 类加载器
同一个 Class 文件, 被两个不同的类加载器加载, 这两个类不相等. 相等包括 equals,instanceOf,isInstance 方法返回的结果.
1, 类别:
启动类加载器(Bootstrap ClassLoader): 加载 < JAVA_HOME>\lib 目标, 或者被 - Xbootclasspath 参数指定的路径, 可被虚拟机识别的类库
扩展类加载器(Extension ClassLoader): 加载 < JAVA_HOME>\lib\ext 目录, 或被 java.ext.dirs 系统变量指定的路径的类库
应用类加载器(Application ClassLoader): 加载 ClassPath 上指定的类库
2, 双亲委托机制
除了顶层的类加载器外, 其他的类加载器都有自己的父类加载器. 父子之间通过组合来复用父加载器代码.
双亲委托机制的工作流程: 一个类加载器收到类加载的请求, 首先将请求委托给父类加载器去完成, 最终所有加载请求都会传递给顶层的启动加载器中. 当父加载器发现未找到所需的类而无法完成加载请求时, 子加载器才尝试去加载.
- ClassLoader
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException {
- // 检查请求的类是否已经被加载
- Class<?> c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- // 让父类加载器去尝试加载
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // 父类加载器抛异常
- }
- if (c == null) {
- // 然后调用自身的 findClass 方法来进行类加载
- c = findClass(name);
- }
- }
- return c;
- }
先检查是否被加载过, 如果没有则调用父加载类去加载
父加载器为空, 则调用启动类加载器
父加载器加载失败, 则抛出 ClassNotFoundException 异常
然后去调用自身的 findClass 方法去进行类加载
来源: https://juejin.im/post/5bf4d86ce51d45620b4cc727