文章目录
- JVM简介
- JVM运行时数据区
- 堆(线程共享)
- 方法区/元空间/元数据区(线程共享)
- 栈
- 程序计数器
- JVM类加载
- 类加载过程
- 双亲委派模型
- 垃圾回收机制(GC)
- 判断对象是否为垃圾
- 判断是否被引用指向
- 如何清理垃圾, 释放对象?
JVM简介
JVM 是 Java Virtual Machine 的简称, 意为Java虚拟机.
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
JVM运行时数据区
也就是JVM的内存布局
堆(线程共享)
保存程序中创建的对象
方法区/元空间/元数据区(线程共享)
存放被JVM加载的类信息(类对象), 常量, 静态变量(static), 即时编译器编译后的代码等数据
栈
存放方法的调用关系, 局部变量
程序计数器
记录了当前线程执行的下一条指令的内存地址
class Test {public int n = 20;public static int a = 10;
}
public class Main() {public static void main(String[] arg) {Test t = new Test();}
}
n是普通的成员变量, 就包含在new的对象的内部, 存放在堆上
a是一个静态成员变量, 包含在类对象中, 存放在方法区中
t是一个局部变量, 存放在栈上
new Test()这个对象是保存在堆上的
栈上的t保存了堆上的new Test()的内存地址
JVM类加载
Java程序一开始是一个.java文件, 通过javac编译成.class文件, 运行java程序, JVM就会读取.class文件, 把文件的内容加载到内存中, 并构造成一个.class对象
类加载就是: 把类从硬盘文件加载到内存中.
类加载过程
流程:
-
加载: 找到.class文件, 打开文件, 并读取文件内容, 并且尝试解析格式
-
验证: 检查当前.class文件是否符合标准格式
-
准备: 给类对象分配内存. 分配出来的内存空间, 内容就是全0的值.
-
解析: 将常量池内的符号引用替换为直接引用的过程, 也就是初始化常量的过程. 初始化类对象中涉及到的一些字符串常量, 这些字符串常量在.class文件中已经存在, 直接读到内存中就行.
- 符号引用: 偏移量. 在.class文件中不知道字符串真实的内存地址在哪, 只知道一个相对的偏移量, 知道字符串的内容在.class文件的哪个地方.
- 直接引用: 真实的内存地址
-
初始化: 对类对象进行更具体的初始化操作. 初始化金泰城园, 执行静态代码块, 加载父类…
双亲委派模型
实则单亲.
JVM加载.class文件的时候, 需要用到"类加载器"模块.
JVM中自带了三个类加载器:
- Bootstrap ClassLoader: 负责加载标准库中的类
- Extension ClassLoader(父亲是Bootstrap ClassLoader): 负责加载JVM扩展的库
- Application ClassLoader(父亲是Extension ClassLoader): 负责加载第三方库.
不是父类子类的继承关系, 而是对象里有一个parent引用指向 父 类加载器 实例
当接收到类加载请求时:
- Application ClassLoader是类加载的入口, 不会立即就搜索第三方库的目录, 而是先把任务委派给父亲, 让父亲先尝试加载.
- 到了Extension ClassLoader, 也不会立即搜索扩展库的目录, 把任务委派给父亲, 让父亲先尝试加载.
- 到了Bootstrap ClassLoader, 也不会立即搜索扩展库的目录, 把任务委派给父亲, 但是他没有父亲, 就只能自己来搜索了. 如果找到了这个类, 就会进行后续的加载流程; 如果没有, 那么任务就会交付给孩子来解决.
- 如果任务回到了Extension ClassLoader, 他就要搜索扩展库的目录, 如果找到了这个类, 就会进行后续的加载流程; 如果没有, 那么任务就会交付给孩子来解决.
- 如果任务回到了Application ClassLoader, 他就要搜索第三方库的目录, 如果找到了这个类, 就会进行后续的加载流程; 如果没有, 那么就抛出异常.
这样做的目的: 明确类的优先级(标准库的类最优先加载, 扩展库其次, 第三方库最低)
标准库中有一个类java.lang.String, 如果我们自己也写了一个java.lang.String类
JVM始终是加载标准库里的类, 而不会加载到我们写的类, 这样便可以避免程序员的代码对标准库的代码产生负面影响
类加载的时机?(类似于懒汉模式, 用到了才加载)
- 构造了这个类的实例
- 使用了该类的静态方法, 静态属性
- 子类的加载会触发父类加载.
垃圾回收机制(GC)
对于程序计数器, 栈而言, 它的生命周期与相关的线程有关, 随线程生, 随线程灭, 并且这两个区域的内存分配和回收具有确定性, 因为当方法或者线程结束了, 内存就自然跟着线程回收了. 所以垃圾回收主要是回收堆和方法区这两个区域.
缺点: 消耗额外的系统资源, 消耗一定的时间, 可能有STW问题
垃圾回收是以对象为单位进行回收
垃圾回收的流程分为两步: 1. 判定对象是否为垃圾. 2. 释放对象的内存
判断对象是否为垃圾
一个对象, 如果在后续代码中不会被继续使用到了, 就可以视作是垃圾了
-
不会被继续使用: 没有被任何引用指向
public void test() {T t = new T();t.func(); } test();
调用方法test的时候, 局部变量t被创建, 当test方法执行完了之后, t自然销毁, 此时
new T()
就没有被引用指向了, 这个对象也就是垃圾了.
判断是否被引用指向
-
引用计数
给这个对象里面安排一个计数器, 每次有引用指向它, 就把计数器+1, 每次引用被销毁, 计数器-1, 当计数器为0, 意味着该对象就是垃圾了
class Test {// } Test t = new Test();//new Test()里的计数器为1 Test t2 = t;//new Test()里的计数器为2 Test t3 = t2;//new Test()里的计数器为3 t = null;//new Test()里的计数器为2 t2 = null;//new Test()里的计数器为1 t3 = null;//new Test()里的计数器为0, 此时该对象为垃圾了
该方法并非是JVM中使用的方案, Python和PHP的虚拟机GC采用的是此方案
缺陷:
-
空间利用率低, 浪费更多的内存空间
-
可能存在循环引用的问题, 导致对象不能正确识别为垃圾.
class Test {public Test t; } Test a = new Test();//计数器为1 Test b = new Test();//计数器为1 a.t = b;//计数器为2 b.t = a;//计数器为2 a = null;//计数器为1 b = null;//计数器为1
此时这两个对象的引用计数不为0, 不能被当做垃圾. 与此同时, 一想要使用对象1, 就需要访问对象2. 要想使用对象2, 就得先访问到对象1…这就是循环引用
-
-
可达性分析(JVM使用的方案)
JVM首先会从现有代码中的能直接访问到的对象出发, 尝试便利所有能访问的对象, 只要对象能访问到, 就会标记成"可达", 完成整个遍历之后, 不可达的对象, 就相当于是垃圾了.
这样的操作没有额外的空间开销, 但是消耗了更多的时间.
那些是能直接访问到的对象呢?
这些对象又被称为gc roots.
- 栈上的局部变量.
- 常量池里的引用
- 方法区中类静态属性引用的对象
- 本地方法栈中 JNI(Native方法)引用的对象
代码执行过程中, 一个对象是否是垃圾, 往往是动态变化的(之前不是垃圾, 现在是垃圾了), 所以可达性分析的扫描是持续的周期性的
如何清理垃圾, 释放对象?
-
标记清除: 被标记为垃圾的对象直接清除.
弊端: 申请内存的时候, 都是申请的连续的空间, 直接释放会导致内存的碎片化, 会破环原有的连续性, 可能会导致有内存, 但是申请不了.
-
复制算法: 通过冗余的内存空间, 把有效的对象复制到另一部分空间, 避免内存碎片.
把一个内存分成两份, 一份使用, 一份等待有效对象被复制过来.
弊端: 如果复制的对象多, 开销会很大, 而且内存利用率也不高, 相当于浪费了一般的内存.
-
标记整理: 类似于顺序表的删除元素操作.
弊端: 这样的方式虽然解决了复制算法内存利用率低的问题, 但是搬运对象的成本也比较高.
-
分代回收: 采用分治的思想, 进行代的划分, 把不同生命周期的对象放在不同代上, 不同代上采用最适合它的垃圾回收方式进行回收.
Java的对象大体分为两类: 1. 生命周期很长的; 2.生命周期很短的
不同的对象的生命周期是不一样的. 因此, 不同生命周期的对象可以采取不同的收集方式, 以便提高回收效率.
如何分代?
JVM根据对象存活的周期(GC周期性扫描)不同, 把对内存划分了2块, 为新生代和老年代.
新生代又分为伊甸区(Eden)和幸存区(Survivor).
伊甸区占大部分内存, 这里存储的对象生命周期都很短. 经过一次GC后, 存活下来的对象就会通过复制算法复制到幸存区.
幸存区是两块大小相等的内存区域, 每次只用一块, 如果这里的对象经过GC后存货, 会继续被复制到另一块幸存区, 如此往复.如果一个对象在幸存区里经过了很多轮GC还存活, 证明它的生命周期很长, 那么它就会被复制到老年代.
老年代的GC频率比新生代要低. 这里清理垃圾的策略是标记整理
如果一个对象的体积很大, 那么他会直接进入老年代, 因为这样的对象不适合进行复制