JVM基础-基础知识汇总

JVM基础-基础知识汇总,第1张

JVM基础知识汇总
  • 一、JVM模型概述
    • 1. 类加载器
    • 2. 方法区
    • 3. 堆
    • 4. 虚拟机栈
    • 5. 本地方法栈
    • 6. 程序计数器
  • 二、类的加载
    • 1. 类的加载过程
      • 1.1 加载
      • 1.2 链接
      • 1.3 初始化
      • 1.4 使用
      • 1.5 卸载
    • 2. 类的加载顺序
    • 3. 双亲委派机制
  • 三、堆的详解
  • 四、垃圾回收算法
    • 1. 标记清除算法
    • 2. 复制算法
    • 3. 标记整理算法
    • 4. 分代收集算法
  • 五、对象的死亡判断
    • 1. 两种基础的计算方法
    • 2. Java中可作为GC Roots的对象
    • 3. 如何宣告对象的死亡(两次标记)

一、JVM模型概述

1. 类加载器
  • 如果JVM想要执行.class文件,则需要将其装进一个类加载器中,它就像一个搬运工一样,会把所有的.class文件全部搬进JVM里面来。
2. 方法区
  • 方法区是用于存放元数据信息方面的数据的,比如类信息、常量、静态变量、编译后代码等。类加载器将.class文件搬过来就是存储在此处。
3. 堆
  • 堆主要放了一些存储的数据,比如对象实例、数组等。它和方法区都同属于线程共享区域,也就是说它们都是线程不安全的。
4. 虚拟机栈
  • 栈是代码运行空间。编写的每一个方法都会放到栈里面运行。
5. 本地方法栈
  • 该栈内的方法都带有native关键字修饰,而且不存在方法体。这种用native修饰的方法就是本地方法,是使用C来实现的。
6. 程序计数器
  • 程序计数器类似于一个指针,它始终指向下一行需要执行的代码。和栈一样,都是线程独享的,就是说每一个线程都会有自己对应的一块区域而不会存在并发和多线程的问题。它也是内存区域中唯一一个不会出现OutOfMemoryError的区域,而且占用内存空间小到基本可以忽略不计。这个内存仅代表当前线程所执行的字节码的行号指示器,字节码解析器通过改变这个计数器的值选取下一条需要执行的字节码指令。
二、类的加载 1. 类的加载过程

从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为链接。

1.1 加载
  • 将class文件加载到内存
  • 将静态数据结构转化成方法区中运行时的数据结构
  • 在堆中生成一个代表这个类的 java.lang.Class对象作为数据访问的入口
1.2 链接
  • 验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查
  • 准备:为static变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)
  • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
1.3 初始化
  • 初始化其实就是执行类构造器方法的()的过程,而且要保证执行前父类的()方法执行完毕。这个方法由编译器收集,顺序执行所有类变量(static修饰的成员变量)显式初始化和静态代码块中语句。此时准备阶段时的那个 static int a 由默认初始化的0变成了显式初始化的3。 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。
1.4 使用 1.5 卸载
  • GC将无用对象从内存中卸载
2. 类的加载顺序

3. 双亲委派机制
  • 概念:当一个加载器收到了类的加载请求时,它是不会先自己去尝试加载的,而是委派给父类去完成。
  • 举例:现在要new一个 Person,这个 Person是自定义的类,如果我们要加载它,就会先委派AppClassLoader,反复循环委派,直到委派到最高级的加载器。只有当父类加载器都反馈自己无法完成这个请求(也就是父类加载器都没有找到加载所需的Class)时,子类加载器才会自行尝试加载。
  • 这样做的好处是,加载一个类时不管是哪个加载器加载,最终都会委托到 BootStrapClassLoader 进行加载,这样保证了使用不同的类加载器得到的都是同一个结果。其实这个也是一个隔离的作用,避免了我们的代码影响了 JDK 的代码,防止内存中出现多个相同的类。
三、堆的详解

  • JVM内存会划分为堆内存和非堆内存,堆内存中也会划分为年轻代和老年代,而非堆内存则为永久代。年轻代又会分为Eden和Survivor区。Survivor也会分为FromPlace和ToPlace,toPlace的survivor区域是空的。
  • 堆内存中存放的是对象,垃圾收集收集这些对象然后交给GC算法进行回收。非堆内存其就是方法区。在1.8中已经移除永久代,替代品是一个元空间(MetaSpace),最大区别是元空间是不存在于JVM中的,它使用的是本地内存。
  • 当我们new一个对象后,会先放到Eden划分出来的一块作为存储空间的内存。
  • 当Eden空间满了之后,会触发叫做Minor GC(就是一个发生在年轻代的GC)的 *** 作,存活下来的对象移动到Survivor0区。
  • Survivor0区满后触发Minor GC,就会将存活对象移动到Survivor1区,此时还会把from和to两个指针交换,这样保证了一段时间内总有一个survivor区为空且to所指向的survivor区为空。
  • 经过多次的Minor GC后仍然存活的对象会移动到老年代。
  • 老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。
  • 当老年区执行了full gc之后仍然无法进行对象保存的 *** 作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,或是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。前者可以通过参数-Xms-Xmx来调整。
四、垃圾回收算法 1. 标记清除算法
  • 就分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收。这个套路很简单,也存在不足,后续的算法都是根据这个基础来加以改进的。
  • 其实它就是把已死亡的对象标记为空闲内存,然后记录在一个空闲列表中,当我们需要new一个对象时,内存管理模块会从空闲列表中寻找空闲的内存来分给新的对象。
  • 不足的方面就是标记和清除的效率比较低下。且这种做法会让内存中的碎片非常多。这个导致了如果我们需要使用到较大的内存块时,无法分配到足够的连续内存。
2. 复制算法

  • 为了解决效率问题,复制算法就出现了。它将可用内存按容量划分成两份,每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了,就把存活的对象copy到另一块toPlace上,然后交换指针的内容。
  • 这样就解决了碎片的问题。这个算法的代价就是把内存缩水了,这样堆内存的使用效率就会变得十分低下了。
  • 不过它们分配的时候也不是按照1:1这样进行分配的,就类似于Eden和Survivor也不是等价分配是一个道理。
3. 标记整理算法

  • 复制算法在对象存活率高的时候会有一定的效率问题,标记整理算法标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
4. 分代收集算法
  • 这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
  • 一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
  • 说白了就是八仙过海各显神通,具体问题具体分析了而已。
五、对象的死亡判断 1. 两种基础的计算方法
  • 引用计数器计算:给对象添加一个引用计数器,每次引用这个对象时计数器加一,引用失效时减一,计数器等于0时就是不会再次使用的。不过这个方法有一种情况就是出现对象的循环引用时GC没法回收。
  • 可达性分析计算:这是一种类似于二叉树的实现,将一系列的GC ROOTS作为起始的存活对象集,从这个节点往下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。
2. Java中可作为GC Roots的对象
  • 虚拟机栈(栈帧中的本地方法表)中引用的对象(局部变量)。
  • 方法区中静态变量所引用的对象(静态变量)。
  • 方法区中常量引用的对象。
  • 本地方法栈(即native修饰的方法)中JNI引用的对象(JNI是Java虚拟机调用对应的C函数的方式,通过JNI函数也可以创建新的Java对象。且JNI对于对象的局部引用或者全局引用都会把它们指向的对象都标记为不可回收)。
  • 已启动的且未终止的Java线程。
3. 如何宣告对象的死亡(两次标记)
  • 如果对象进行可达性分析之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行finalize()方法。如果对象有必要执行finalize()方法,则被放入F-Queue队列中。
  • GC对F-Queue队列中的对象进行二次标记。如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/924086.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存