JVM内存模型和垃圾回收机制-第八章

系列文章目录

第一章 ArrayList-Java八股面试(一)

第二章 HashMap-Java八股面试(二)

第三章 单例模式-Java八股面试(三)

第四章 线程池和Volatile关键字-Java八股面试(四)

第五章 ConcurrentHashMap-Java八股面试(五)

第六章 spring之refresh流程-Java八股面试(六)

第七章 Spring Bean生命周期七大阶段-Java八股面试(七)


文章目录

  • 系列文章目录
  • 前言
  • 一、JVM
    • 1.1 JVM生命周期
    • 1.2 结束生命周期
    • 1.3 JVM内存
    • 1.4 哪些地方会造成内存溢出
    • 1.5 堆区内存参数
    • 1.6 垃圾回收机制
      • 1.6.1 标记 - 清除算法
      • 1.6.2 标记 - 复制算法
      • 1.6.3 标记 - 整理算法


前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、JVM

JVM的中文名称叫Java虚拟机,它是由软件技术模拟出计算机运行的一个虚拟的计算机。

JVM也充当着一个翻译官的角色,我们编写出的Java程序,是不能够被操作系统所直接识别的,这时候JVM的作用就体现出来了,它负责把我们的程序翻译给系统“听”,告诉它我们的程序需要做什么操作。

我们都知道Java的代码需要经过编译器,生成.Class文件后,JVM才能识别并运行它,JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因。

1.1 JVM生命周期

JVM在Java程序开始运行的时候,它才运行,程序结束的时它就停止。

一个Java程序会开启一个JVM进程,如果一台机器上运行3个Java程序,那么就会有3个运行中的JVM进程。

JVM中的线程分为两种:守护线程和普通线程

守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。

普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。

1.2 结束生命周期

1、执行了System.exit()方法

2、程序正常执行结束

3、程序在执行过程中遇到了异常或错误而终止进程

4、由于操作系统出现错误而导致Java虚拟机进程终止

1.3 JVM内存

方法区(Method Area)

方法区用于存储JVM加载完成的类型信息、常量、静态变量、即时编译器编译后的代码缓存,方法区和 Java 堆区一样,都是线程共享的内存区域。

在JDK8以前,使用永久代的方式来实现方法区,JDK8以后,永久代的概念被废弃了,方法区改用和 JRockit、J9一样的在本地内存中实现的元空间(Meta Space)来代替,好处是元空间会在运行时根据需要动态调整,只要没有超过当前进程可用的内存上限(32位和64位系统各不相同),就不会出现溢出的问题。

方法区也可以被垃圾回收,但条件非常严苛,必须在该类没有任何引用的情况下。

当需要扩展时空间不足,会分别 OutOfMemoryError 异常。

堆(JVM堆、Java heap)

堆区负责存放对象实例,当Java创建一个类的实例对象或者数组时,都会在堆中为新的对象分配内存。

虚拟机中只有一个堆,程序中所有的线程都共享它。

通常情况下,堆占用的内存空间是最多的。

堆的存取方式为管道类型,先进先出。

在程序运行中,可以动态的分配堆的内存大小。

堆的内存资源回收是交给JVM GC进行管理的。

当需要扩展时空间不足,会分别 OutOfMemoryError 异常。

虚拟机栈(JVM栈、VM Stack)

在Java栈中只保存基础数据类型和对象的引用,注意只是对象的引用而不是对象本身哦,对象是保存在堆区中的。

拓展知识:像String、Integer、Byte、Short、Long、Boolean等等包装类型,它们是存放于堆中的。

栈的存取类型为类似于水杯,先进后出。

栈内创建的基本类型数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。而在栈内创建的引用类型实例,则还是由JVM GC管理。

当一个线程创建运行的时候,与之对应的栈就创建了,每个栈中的数据都是私有的,其他线程不能访问。

每个线程都会建立一个栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法的每次调用,栈帧包含了三个部分:

局部变量区(方法内基本类型变量、对象实例的引用)

操作数栈区(存放方法执行过程中产生的中间结果)

运行环境区(动态连接、正确的方法返回相关信息、异常捕捉)

虚拟机栈在深度溢出或扩展失败的时候,会分别抛出StackOverflowError 和 OutOfMemoryError 异常。

本地方法栈(Native Method Stack)

本地方法栈的功能和JVM栈非常类似,区别在于虚拟机栈执行的是Java方法,本地方法栈执行的是本地(Native)方法服务,存储的也是本地方法的局部变量表,本地方法的操作数栈等信息。

栈的存取类型为类似于水杯,先进后出。

栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。

每一个线程都包含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。

本地方法栈是在 程序调用 或 JVM调用 本地方法接口(Native)时候启用。

本地方法都不是使用Java语言编写的,它们可能由C或其他语言编写,本地方法也不由JVM去运行,所以本地方法的运行不受JVM管理。

HotSpot VM将本地方法栈和JVM栈合并了。

本地方法栈也会在深度溢出或扩展失败的时候,分别抛出StackOverflowError 和 OutOfMemoryError 异常。

程序计数器

在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器。

程序计数器仅占很小的一块内存空间。

当线程正在执行一个Java方法,程序计数器记录的是正在执行的JVM字节码指令的地址。如果正在执行的是一个Natvie(本地方法),那么这个计数器的值则为空(Underfined)。

程序计数器不会抛出 OutOfMemoryError(内存不足错误)。

1.4 哪些地方会造成内存溢出

JVM内存模型和垃圾回收机制-第八章_第1张图片

1.5 堆区内存参数

JVM内存模型和垃圾回收机制-第八章_第2张图片

  • -Xmx Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定;
  • -Xms Java Heap初始值,Server端JVM最好将-Xms和-Xmx设为相同值,开发测试机JVM可以保留默认值;
  • -Xmn Java Heap Young区大小,不熟悉最好保留默认值; -Xss 每个线程的Stack大小,不熟悉最好保留默认值;
  • -XX:SurvivorRatio eden和From Survivor 区 (S0区)和 To Survivor 区 (S1区)的内存比例。默认情况下,from和to大小相同各占一份。
    如上图所示,新生代为5G,伊甸园区占三份,from和to各占一份
    即5G/5*3 = 3G 所以eden区的大小为3G

1.6 垃圾回收机制

JVM GC只回收堆区的对象和方法区的废弃常量的回收和对无用类的回收。

栈区的数据(仅指基本类型数据),在超出作用域后会自动出栈释放掉,所以其不在JVM GC的管理范围内。

怎么判断对象可以被回收了?

简单来说就是:对象没有引用了或者对象不可达

怎么判断对象是否存活?常见的有两种算法,分别是 引用计数法 和 可达性分析法

引用计数法

在对象里添加一个被引用的计数器,每当有地方引用了它,计数器就加1,引用失效时,计数器就减1。

在触发回收内存的时候,遍历所有对象,把计数器值等于0的找出来,释放掉即可。

可达性分析法(根搜索算法)

这个算法的基本原理是通过一系列可被作为 GC Roots 的根对象来作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程的就是一条引用链(Reference Chain),没有在这个链条上面的对象,也就是根节点通过引用链不可达到这个对象时,就认为这个对象是可以被回收的。
哪些对象可以作为根节点呢?

  • 在虚拟机栈帧中引用的对象,例如线程调用方法时,使用或产生的参数、局部变量、临时变量等。
  • 在方法区中,类的引用类型静态变量或常量。
  • 在本地方法栈中的JNI引用的对象,
  • 在JVM内部的对象,例如基本数据类型的Class对象,一些常驻的异常对象(NullPointExcepiton),系统类加载器等。
  • 所有synchronized同步锁的持有对象。
  • 反映JVM内部情况的JMXBean、JVMTI注册的回调、本地代码缓存等。

1.6.1 标记 - 清除算法

分两个步骤:标记 和 清除。首先标记出所有存活的对象,再扫描整个空间中未被标记的对象直接回收。

并没有规定标记阶段一定要标记“存活”的对象,也可以标记“可回收”的对象
标记“存活”的,还是标记“可回收”的,网上各种说法都有,我个人理解,是标记存活的。这样效率高些。
首先,Java使用的是可达性分析算法来判断对象是否存活,上面有详细说这个算法,这里就不重复了。
我们假设要标记“可回收”的对象,再进行清除,那么需要三个步骤:

  1. 先通过可达性分析法,通过根对象(GC Roots)顺着引用链先把这些存活对象都标出来
  2. 遍历这个区域所有对象,把没标记存活的对象,打上一个“可回收”的标记
  3. 遍历这个区域所有对象,把标记了“可回收”的对象,释放掉。

但标记的是“存活”的对象,再进行清除,只需要两个步骤即可:

  1. 先通过可达性分析法,通过根对象(GC Roots)顺着引用链先把这些存活对象都标出来
  2. 遍历这个区域所有对象,把没标记存活的对象,直接清理掉即可。
    JVM内存模型和垃圾回收机制-第八章_第3张图片

标记 - 清除算法由于回收后没有进行整理的操作,所以会存在内存空间碎片化的问题,这个确实是缺点,但也是这个算法的特点,正因为它不进行整理,所以效率才高。

1.6.2 标记 - 复制算法

常规的复制算法,是把内存分成两块大小相同的空间(1 : 1),每次只使用其中一块,当使用中的这块内存用完了,就把存活的对象移动到另一块内存中,再把使用过的这块内存空间一次性清理掉。这个做法虽然效率极高,但也浪费了一半的内存空间。

所以不需要按照 1 : 1 的比例来实现复制算法,而是可以按照 8 : 1 : 1 的比例来分配内存空间,也就是一个80%的Eden空间和两个10%的Survivor空间。

JVM内存模型和垃圾回收机制-第八章_第4张图片

为什么要两块Survivor空间?

  • 因为复制算法,必须要有一块空间是空闲的。想象一下,如果只有一块Eden空间 + 一块Survivor空间
  • 当GC回收完成后,Eden中存活的对象会移动到Survivor空间。程序继续运行,新的对象又会进入Eden空间,此时就会出现 Eden 和
    Survivor 空间里都有对象,复制算法也就进行不下去了。
  • 每次分配内存,只使用Eden和其中一块Survivor空间,发生GC回收时,把Eden和其中一块Survivor空间中存活的对象,复制到另一块空闲的Survivor空间,然后直接把Eden和使用过的那块Survivor空间清理掉。

目前主流的使用分代回收机制的Java虚拟机,都是使用标记-复制算法来作为新生代的回收算法。它非常适合用在新生代这种回收率极高的场景,这样的场景下,复制算法浪费的空间几乎可以忽略不计。效率高,且内存不会有碎片化的问题。但对于老年代这种存活率很高的场景,就不适合了。

1.6.3 标记 - 整理算法

标记-清除算法会产生内存碎片,不适合哪些需要大量连续内存空间的场景,而标记-整理算法,就是在其基础之上,增加了整理这个操作,去解决这些内存空间碎片化的问题。
JVM内存模型和垃圾回收机制-第八章_第5张图片

和标记-清除算法一样,先标记,但清除之前,会先进行整理,把所有存活的对象往内存空间的左边移动,然后清理掉存活对象边界以外的内存,即完成了清除的操作。标记-整理 算法是在 标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

老年代里的对象存活率很高,不适合使用标记-复制的算法。而且老年代存储大对象的概率要比新生代大很多,这些大对象需要连续的内存空间来存储,标记-清除这个算法也不适合。所以大多数的老年代都采用标记-整理来作为这个区域的回收算法。

你可能感兴趣的:(java八股面试,java,jvm,java,面试)