Java面试题超详细整理《JVM篇》

JVM由那些部分组成,运行流程是什么?

JVM的由以下几部分组成:

  • 类加载器(ClassLoader): Java的动态类加载功能由ClassLoader子系统处理。它加载,链接。并在运行时(而非编译时)首次引用类时初始化类文件。
  • 运行时数据区(Runtime Data Area): Java虚拟机在执行Java程序的过程中会把它管理的内存分为若干个不同的数据区域。
  • 执行引擎(Execution Engine): 分配给运行时数据区的字节码将由执行引擎执行,执行引擎读取字节码并逐段执行。(字节码执行引擎编译成机器码后才可在物理机上执行)
  • 本地库接口(Native Interface): JNI将与本机方法库进行交互,并提供执行引擎所需的本机库
  • 本地库(Native Libraries): 本机库的集合,执行引擎执行时需要

Java代码执行流程:
-> java源程序通过编译器(javac.exe)对javac文件(.java)进行编译
–> 生成字节码文件(.class)
—> 类加载器把字节码加载到内存,放入运行时数据区的方法区内
-----> 执行引擎读取字节码并逐段执行
------> 解释执行(对字节码指令进行逐行的解释)编译执行(将热点代码编译成机器指令)
-------> 在操作系统(Windows,Linux等)上执行

在这里插入图片描述


简述java类加载机制?

Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。

Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。

当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

  • 加载:通过一个类的全限定名获取定义此类的二进制字节流,并将这个字节流所代表的静态存储结构转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
  • 连接过程:验证-》准备-》解析
    ① 验证(Verify):目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全,主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
    ②准备(Prepare):为类变量分配内存并且设置该类变量的默认初始值,即零值
    ③解析:将常量池内的符号引用转换为直接引用的过程
  • 初始化:对静态变量和静态代码块执行初始化工作

加载class文件的方式:
①从本地系统中直接加载
②通过网络获取,典型场景:Web Applet
③从zip压缩包中读取,成为日后jar、war格式的基础
④运行时计算生成,使用最多的是:动态代理技术

初始化阶段就是执行类构造器方法<clinit>() 的过程
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来(当代码中包含static变量时,<clinit>() 自动生成,如果没有静态代码快则不会生成)。
<clinit>()方法中的指令按语句在源文件中出现的顺序执行
<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>()
若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁

在这里插入图片描述


类加载器的分类

类加载器:通过类的权限定名获取该类的二进制字节流的代码块。

JVM支持两种类型的类加载器 ,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器,所以ExtClassLoader 和 AppClassLoader 都属于自定义加载器。

四者之间是包含关系,不是上层和下层,也不是子父类的继承关系:
在这里插入图片描述

  • 启动类加载器(Bootstrap ClassLoader):用来加载java核心类库,无法被java程序直接引用。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
  • 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
在这里插入图片描述

总结就是: 当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

作用:

  • 双亲机制避免了类的重复加载
  • 保护程序安全,防止核心API被随意篡改

说一下 JVM 运行时数据区

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
在这里插入图片描述
线程私有的:程序计数器、虚拟机栈、本地方法栈
线程共享的:堆、方法区

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;

  • Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;栈帧就是Java虚拟机栈中的下一个单位。

  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码

  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。

  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。虽然 Java 虚拟机规范把⽅法区描述为堆的⼀个逻辑部分,但是它却有⼀个别名叫做 Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来。

运行时常量池:
运⾏时常量池是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息外,还有常量池表(⽤于存放编译期⽣成的各种字⾯量和符号引⽤)既然运⾏时常量池是⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛出 OutOfMemoryError 错误。
JDK1.8 运⾏时常量池在元空间,字符串常量池在堆中。

异常相关:

在这里插入图片描述

  • 程序计数器: 内存区域中唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
  • 虚拟机栈与本地方法栈: 在Java虚拟机栈和本地方法栈中,规定了两个异常状况:如果线程请求的栈深度大于栈所允许的深度,将抛出StackOverflowError异常;如果栈可以动态扩展,并且扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  • 堆: 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时(内存大小超过“-xmx"所指定的最大内存时),将会抛出OutOfMemoryError异常。
  • 方法区: 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类(比如说加载大量第三方jar包),导致方法区溢出,虚拟机会抛出 OutOfMemoryError 异常。

介绍下Java虚拟机栈?

Java虚拟机是线程私有的,它的生命周期和线程相同。 虚拟机栈描述的是Java方法执行的内存模型: 每个方法在执行的同时都会创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息

解析栈帧:

  • 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
  • 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
  • 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
  • 方法返回地址:在方法退出后都返回到该方法被调用的位置,正常的话就是return调用者的pc计数器的值,不正常的话返回异常表中的对应信息

在这里插入图片描述

对于虚拟机栈来说不存在垃圾回收问题

Java 虚拟机栈会出现两种错误: StackOverFlowError 和 OutOfMemoryError 。

  • StackOverFlowError : 若 Java 虚拟机栈的内存⼤⼩不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最⼤深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError : 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也⽆法提供更多内存的话。就会抛出 OutOfMemoryError 错误。

扩展:那么⽅法/函数如何调⽤?
Java 栈可⽤类⽐数据结构中栈,Java 栈中保存的主要内容是栈帧,每⼀次函数调⽤都会有⼀个对应的栈帧被压⼊ Java 栈,每⼀个函数调⽤结束后,都会有⼀个栈帧被弹出。
Java ⽅法有两种返回⽅式: return 语句、抛出异常。不管哪种返回⽅式都会导致栈帧被弹出。

一个方法调用另一个方法,会创建很多栈帧吗?
会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面

递归的调用自己会创建很多栈帧吗?
答:递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去


介绍下Java堆吗?

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

java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老生代,再细致⼀点可分为:Eden 空间、From Survivor、To Survivor 空间等
在这里插入图片描述

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。


为什么要将永久代替换为元空间呢?

JDK1.8以前使用永久代(方法区),JDK1.8以后使用元空间

整个永久代有⼀个 JVM 本身设置固定大小上限,⽆法进⾏调整,而JVM加载的class的总数,方法的大小等都很难确定,因此对永久代大小的指定难以确定。太小的永久代容易导致永久代内存溢出,太大的永久代则容易导致虚拟机内存紧张,空间浪费。⽽元空间使⽤的是直接内存,受本机可⽤内存的限制,虽然元空间仍旧可能溢出,但是⽐原来出现的⼏率会更⼩。

元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace
你可以使⽤ -XX MaxMetaspaceSize 标志设置最⼤元空间⼤⼩,默认值为 unlimited,这意味着它只受系统内存的限制。 -XX MetaspaceSize 调整标志定义元空间的初始⼤⼩如果未指定此标志,则 Metaspace 将根据运⾏时的应⽤程序需求动态地重新调整⼤⼩。


什么是直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。

直接内存可以看成是物理内存和Java虚拟机内存的中间内存,他可以直接使⽤ Native 函数库直接分配堆外内存,然后通过⼀个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样就能在⼀些场景中显著提⾼性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存⼤⼩以及处理器寻址空间的限制。


堆栈的区别是什么?

对比 JVM堆 JVM栈
物理地址方面 堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以使用了各种垃圾回收算法 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存分配方面 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
存放的内容方面 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
程序的可见度 堆对于整个应用程序都是共享、可见的。 栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

堆:主要用来存储对象和数组,物理地址分配不连续、内存大小不确定、线程共享
栈:用来存放操作数栈,物理地址分配连续、内存在编译期确定、线程私有


说⼀下Java对象的创建过程

  • 加载类元信息,判断类元信息(加载、链接、初始化)是否存在
  • 为对象分配内存
  • 处理并发问题
  • 初始化分配到的空间,属性的默认初始化(零值初始化)
  • 设置对象头信息
  • 执行init方法初始化(属性显示初始化、代码块中的初始化、构造器初始化)

在这里插入图片描述

为对象分配内存:
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:

  • 指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
  • 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些

处理并发问题:

  • 采用CAS配上失败重试保证更新的原子性
  • 在Eden区给每个线程分配一块区域TLAB - 通过设置 -XX:+UseTLAB参数来设置(区域加锁机制),

对象的访问定位

  • 句柄访问: 栈的局部变量表中,记录的对象的引用,然后在堆空间中开辟了一块空间,也就是句柄池。
    特点: reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改

    在这里插入图片描述
  • 直接指针(HotSpot采用):
    直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。
    特点: 节省了指针定位的开销,但是在对象被移动时reference本身需要被修改。

    在这里插入图片描述

简述Java垃圾回收机制

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

优点:JVM的垃圾回收器都不需要我们手动处理无引用的对象了,这个就是最大的优点
缺点:程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收

垃圾收集GC(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。


垃圾回收器的原理是什么?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。

有什么办法手动进行垃圾回收?
程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行


JVM中怎么判断对象是可以被回收的?

垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死掉了,需要被回收。一般有两种方法来判断:

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数-1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;(python中使用)
  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。(Java中使用)

可以作为GC Root的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中 Native 方法引用的对象。


JVM 垃圾回收算法有哪些?

  • 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
  • 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
  • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
  • 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

标记-清除算法
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:

  • 标记阶段:标记出可以回收的对象。
    * 清除阶段:回收被标记的对象所占用的空间。

标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。
优点:实现简单,不需要对象进行移动。
缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
在这里插入图片描述

复制算法:
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制
在这里插入图片描述

标记-整理算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。
优点:解决了标记-清理算法存在的内存碎片问题。
缺点:仍需要进行局部对象移动,一定程度上降低了效率。
在这里插入图片描述

分代收集算法:
当前商业虚拟机都采用 分代收集 的垃圾收集算法。分代收集算法,顾名思义是根据对象的 存活周 期 将内存划分为几块。一般包括 年轻代 、 老年代 和 永久代 ,如图所示: (后面有重点讲解)
在这里插入图片描述

分区算法:
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间
在这里插入图片描述


新生代、老年代、永久代的区别

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young )又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

  • 新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法 ,只需要付出少量存活对象的复制成本就可以完成收集。
  • 老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用 “标记-清理”或者“标记-整理” 算法。
  • 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(FullGC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。


Minor GC、Major GC、Full GC是什么?

  • Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾)
  • Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法)
  • Full GC是清理整个堆空间,包括年轻代和老年代,因为Full GC是清理整个堆空间所以Full GC执行速度非常慢,在Java开发中最好保证少触发Full GC

Minor GC、Major GC、Full GC的触发条件

Minor GC 触发条件一般为:

  • eden区满时,触发MinorGC。
  • 新创建的对象大小 > Eden所剩空间时触发Minor GC

Major GC和Full GC 触发条件一般为: Major GC通常是跟full GC是等价的 1. 每次晋升到老年代的对象平均大小>老年代剩余空间

  • MinorGC后存活的对象超过了老年代剩余空间
  • 永久代空间不足
  • 执行System.gc()
  • CMS GC异常
  • 堆内存分配很大的对象

为什么新生代要分Eden和两个 Survivor 区域?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。

Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生。设置两个Survivor区解决了碎片化问题(使用了复制算法),刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor spaceS1。(每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,无论什么时候,总是有一块 Survivor 区域是空闲着的。)
在这里插入图片描述
Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。
在这里插入图片描述

Java堆老年代( Old ) 和新生代 ( Young ) 的默认比例?
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio来指定 )

新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,Edem 和俩个Survivor 区域比例是 = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),


说一下 JVM 有哪些垃圾回收器?

在这里插入图片描述
7种经典的垃圾收集器:

按新生代老年代来分:

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、CMS、Parallel Old
  • 整堆回收器:G1

按串行、并行来分:

  • 串行回收器:Serial、Serial old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel old
  • 并发回收器:CMS、G1
垃圾回收器 工作区域 回收算法 工作线程 用户线程并行 描述
Serial 新生带 复制算法 单线程 Client模式下默认新生代收集器。
Serial Old 老年带 标记-整理 单线程 Serial老年代版本,给Client模式下的虚拟机使用
ParNew 新生带 复制算法 多线程 Serial的多线程版本,Server模式下首选, 可搭配CMS的新生代收集器
Parallel Scavenge 新生带 复制算法 多线程 目标是达到可控制的吞吐量
Parallel Old 老年带 标记-整理 多线程 Parallel Scavenge老年代版本,吞吐量优先
CMS 老年代 标记-清除 多线程 以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器
G1 新生带 +老年带 标记-整理 + 复制算法 多线程 JDK1.9默认垃圾收集器
  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量= 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
  • G1(Garbage First)收集器 ( 标记整理 + 复制算法来回收垃圾 ): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

在这里插入图片描述

JVM系类学习笔记

猜你喜欢

转载自blog.csdn.net/Lzy410992/article/details/119448063