阅读 355

JVM之Java 内存区域与垃圾回收

前言

一直想要将JVM这块整理一下,权当复习,以后再需要用到只要查看这篇文章就行。
对于Java程序员来说,写代码不需要像C++一样去手动释放内存,把内存控制权利交给 Java 虚拟机,所以学习JVM其实是有必要的,不仅仅是为了面试,是为了更好的理解JAVA语言,而像其他语言如Python实际上也是用到了垃圾回收。

JVM是什么

JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

image.png

JVM包含两个子系统和两个组件

两个子系统:

  • Class loader(类装载)
  • Execution engine(执行引擎)

两个组件:

  • Runtime data area(运行时数据区)
  • Native Interface(本地接口)

运行时数据区域

  • 在了解垃圾收集之前,我们得先知道哪些内存需要回收,哪些不需要。

image.png JVM运行时数据区如上图,其中,程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭,而堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。另外需要知道的是,JDK.1.8之后,方法区被替换成元空间。

  • 程序计数器【线程私有】

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

  • 虚拟机栈【线程私有】

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

  • 本地方法栈【线程私有】

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

  • 堆【线程共享】

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

  • 方法区【线程共享】

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java 虚拟机规范把方法区描述为堆的一个逻辑部分,方法区也被称为永久代。

  • 运行时常量池
  • 直接内存

判断对象是否存活的两种算法

  • 引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。。最主要的缺点是很难解决对象之间相互循环引用的问题。

  • 可达性分析算法

主流商用程序语言都是使用该算法。基本思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

image.png 一般来说,如下情况的对象可以作为GC Roots:
1.虚拟机栈(栈桢中的本地变量表)中的引用的对象
2.方法区中的类静态属性引用的对象
3.方法区中的常量引用的对象
4.本地方法栈中JNI(Native方法)的引用的对象

垃圾回收

JVM的垃圾回收是面试常问的问题,要搞清楚需要有个脉络。首先,垃圾回收是在哪里进行(分代),回收的方式有哪些(回收算法),回收的过程是怎样的(担保机制),我从这三个方面来做笔记。

  • 垃圾回收在哪里进行?

堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。为了提高对象内存分配和垃圾回收的效率,JVM采用分代垃圾收集算法给堆内存分代。

  • 内存分代划分: Java虚拟机将堆内存划分为新生代、老年代和永久代,永久代是HotSpot虚拟机特有的概念,它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且HotSpot也有取消永久代的趋势,在JDK 1.7中HotSpot已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
  • 新生代: 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1,这两个 Survivor 区域按照顺序被命名为 from 和 to。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中【新生代是GC频繁的区域】
  • 老年代: 在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
  • 永久代: 永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
  • 回收的方式
  • 复制算法: 将内存分为两等块,每次使用其中一块。当这一块内存用完后,就将还存活的对象复制到另外一个块上,然后再把已使用过的内存空间一次清理掉
  • 标记-清除算法: 首先标记出所有需要回收的对象,使用可达性分析算法判断一个对象是否为可回收,在标记完成后统一回收所有被标记的对象
  • 标记-整理算法: 标记过程与标记 - 清除算法一样,但之后让所有存活的对象移向一端,然后直接清理掉边界以外的内存。
  • 分代收集算法: 根据对象存活周期将堆分为新生代和老年代,然后根据各年代特点选择适当的回收算法。

1.新生代基本上对象都是朝生暮死的,生存时间很短暂,因此可采用复制算法,只需要复制少量的对象就可以完成垃圾收集。
2.老年代中的对象存活率高,也没有额外的空间进行分配担保,因此必须使用标记 - 整理或者标记 - 清除算法进行回收,只需要清除少量对象即可完成垃圾收集。

  • 回收的过程
  • 新生代的GC: 在新生代,GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
  • 老年代空间分配担保机制: 当新生代被分配了大对象(该对象大小可以通过参数设置),或者经过Minor GC后,存活下来的对象,Survivor区放不下,那么这些对象都会被分配到老年代。
  • Stop the world: 在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。为了保证引用更新的正确性,等待所有用户线程进入安全点后并阻塞,做一些全局性操作的行为,当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。

参考链接

文章分类
后端
文章标签