JVM
JVM 意为 Java虚拟机
JVM的内存来自操作系统,内存主要划分为一下四个部分:
- 程序计数器
- 栈
- 堆(引用存放在堆中
- 方法区
注意:每个线程都有一份栈和程序计数器,而整个进程有唯一的堆和方法区。加载类得到的类对象是在方法区中。同时static修饰的成员,是类属性(长在类对象身上),也是在方法区中。
代码在创建变量的时候,变量位于那个区域取决于代码,和变量的类型没有关系
- 局部变量,栈上
- 成员变量,堆上
- 静态变量,方法区
内存布局中的异常问题
Java堆溢出
堆的存储空间不够了,-Xms(设置堆的最小值),-Xmx(设置堆的最大值)
启动Java进程的时候,可以设置一些命令选项(JVM调参),这些选项会影响到JVM工作的具体特性
public class Test {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list =
new ArrayList<>();
while(true) {
list.add(new OOMObject()); //这里不会触发CG(垃圾回收机制)
}
}
}
上述代码出现OOM异常,这里很可能是因为代码出现了”内存泄漏“,该释放的内存没有释放,Java的垃圾回收机制没有管住。
Java栈溢出
HotSpot虚拟机将虚拟机栈与本地方法栈合二为一
类加载
类加载就是将编译好的.class文件,给加载到内存中
类加载大致分为5步,其中有3步属于连接
- 加载
- 连接 :1.验证 2.准备 3.解析
- 初始化
1.加载
加载只是类加载过程的一个阶段
- 代码中写的是import了一个类的全限定类名(包名+类名),根据这个全限定类名找到这个.class文件的位置,以字节流的方式读取整个文件的内容到内存中,创建一个类对象,以备接下来解析工作使用。
2.验证
校验.class文件格式是否符合预期(需要遵循Java虚拟机规范的要求),保证这些信息当作代码运行后不会危害虚拟机本身的安全
3.准备
针对类对象进行一些初始化工作,初始化的同时,也需要针对静态变量进行设置值
4.解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
5.初始化
Java虚拟机真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器的过程。
双亲委派模型
- 类加载中的一个环节
面试题:说说JVM的双亲委派模型:
双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载
假设要加载的类是一个java.lang.String(标准库的类)
- JVM调用Application ClassLoader,触发加载
- Application ClassLoader真正去搜索目录之前,会先调用父加载器,ExtClassLoader
- ExtClassLoader在真正去搜索目录之前,就会先调用父加载器,BootStrapClassLoader
- BootStrapClassLoader在搜索目录之前,也会尝试调用自己的父加载器(null),因此就开始真正的搜索目录,在标准库目录中,就找到了java.lang.String
- BootStrapClassLoader就负责读取文件,并进行后续的类加载过程
假设要加载的类是一个自己写的类Java.test
- JVM调用Application ClassLoader,触发加载
- Application ClassLoader真正去搜索目录之前,会先调用父加载器,ExtClassLoader
- ExtClassLoader在真正去搜索目录之前,就会先调用父加载器,BootStrapClassLoader
- BootStrapClassLoader在搜索目录之前,也会尝试调用自己的父加载器(null),因此就开始真正的搜索目录,在标准库目录中,找不到这个类,回到子加载器中
- ExtClassLoader开始真正搜索目录,当前目录仍然找不到这个类,再回到子加载器中
- Application ClassLoader开始真正的搜索目录,搜索到了Java.test就会执行后续的加载过程
如果最后的步骤仍然找不到这个类,就会抛出异常ClassNotFoundException
双亲委派模型是JVM里默认的方式,但是JVM也给我们提供了一些机制,可以让程序员自定义类加载器。自己定义的类加载器可以遵循双亲委派模型也可以不遵守,根据需求灵活控制。(eg:Tomcat)
垃圾回收机制
一个进程的执行,是需要使用到计算机的硬件资源的,而计算机的硬件资源是有限的,尤其是对于内存来说。因此,不能光申请内存,也要释放内存。
内存申请的时候是明确的,而内存释放的时候是不一定明确的
- 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
不同语言缓解内存泄漏的方法
- C++ 智能指针
- Rust 严格的编译校验,特殊的语法规则(代价:让新手很不友好)
- Java,Python,Go,PHP,C# 等 垃圾回收机制
垃圾回收(CG) 自动释放内存
-
程序计数器
-
栈
前两个和线程绑定在一起,创建线程,这里的东西就被申请。线程结束(里面的run方法执行完),相关的内存就可以销毁
-
堆(引用存放在堆中
CG主要负责这里,堆上面创建的内存属于整个进程都可以用到的内存,具体啥时候用到就比较模糊
-
方法区
方法去里主要是类对象,类加载的时候,申请这里的内存。类卸载很少涉及
- STW(stop the world)因为垃圾回收,引起的业务代码暂时不能执行
Java堆中存放着几乎所有对象的实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象那些存活,那些已经死去。将死去的对象进行回收
垃圾回收的基本流程
- 先找到垃圾,判定某个对象是不是垃圾
- 释放这个垃圾
死亡对象的判断算法
判定对象是否是垃圾,判断对象是否有引用指向它
1.引用计数算法
Java没有使用,Python等语言使用
JVM没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题
2.可达性分析算法
JVM中的可达性分析,就是从代码中显式书写出的这些引用变量出发,遍历每一个可以访问的路径(类似于二叉树的遍历,递归进行)把所有可以访问到的对象都标记为可达,而不可达的就是垃圾!接下来JVM就可以将未标记可达的对象清除掉。
可达性分析解决了引用计数中的问题
- 缺点:需要遍历大量的对象,开销比较大
垃圾回收算法
1.标记清除算法
标记-清除分别表示两个不同的阶段,首先标记需要回收的对象,然后对标记的对象进行统一的回收。
不足之处:
- 效率问题:标记与清除的效率都不高
- 空间问题:标记和清除会产生大量的内存碎片,当需要分配较大的对象时会无法完成
2.复制算法
最大优点:充分解决内存碎片问题
标记(可达性分析),这里不直接清除内存碎片,将保留的对象复制到另一半。保证释放后的空间有足够大的空闲空间
缺点:
- 空间利用率低
- 复制成本高
3.标记整理算法
标记-清除的改进版本,删除类似于顺序表删除中间元素,带着一个搬运的操作
分代算法
JVM中不是只使用了一种算法,而是多种算法结合起来一起使用
当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只 是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每 次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没 有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
哪些对象会进入新生代?哪些对象会进入老年代?
- 新生代:一般创建的对象都会进入新生代;
- 老年代:大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代 移动到老年代。
注意:以上都是抽象出来的简化模型,真实的垃圾回收会更加复杂
垃圾收集器
针对垃圾回收算法的具体实现
说说垃圾收集器
《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。