写点什么

JVM 原理与深度调优

用户头像
Java王路飞
关注
发布于: 2021 年 05 月 11 日

什么是 jvm


jvm 是 java 虚拟机 运行在用户态、通过应用程序实现 java 代码跨平台、与平台无关、实际上是"一次编译,到处执行"

1.从微观来说编译出来的是字节码!去到哪个平台都能用,只要有那个平台的 JDK 就可以运行!字码好比是一个人,平台好比为国家,JDK 好比这个国家的语言!只要这个人(字节码)有了这个国家的语言(JDK)就可以在这个国家(平台)生活下去。2.JDK 是整个 Java 的核心,包括了 Java 运行环境(Java Runtime Envirnment),一堆 Java 工具和 Java 基础的类库(rt.jar)。3.Java 虚拟机(JVM)一种用于计算机设备的规范,可用不同的方式(软件或硬件)加以实现。编译虚拟机的指令集与编译微处理器的指令集非常类似。Java 虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。4.java 编译出来的是一种“java 字节码”,由虚拟机去解释执行。而 c 和 c++则编译成了二进制,直接交由操作系统执行。5.所谓的一次编译、到处执行,即只需在一个地方编译,在其他各个平台下都可以执行。6.与平台无关指的是 JAVA 只运行在自己的 JVM 上,不需要依赖任何其他的底层类,所以和操作系统没有任何联系,平台是说运行的系统

内存结构图


class 文件

class 文件径打破了 C 或者 C++等语言所遵循的传统,使用这些传统语言写的程序通常首先被编译,然后被连接成单独的、专门支持特定硬件平台和操作系统的二进制文件。通常情况下,一个平台上的二进制可执行文件不能在其他平台上工作。而 Java class 文件是可以运行在任何支持 Java 虚拟机的硬件平台和操作系统上的二进制文件。


执行过程

执行过程简介

当编译和连接一个 C++程序时,所获得的可执行二进制文件只能在指定的硬件平台和操作系统上运行,因为这个二进制文件包含了对目标处理器的机器语言。而 Java 编译器把 Java 源文件的指令翻译成字节码,这种字节码就是 Java 虚拟机的“机器语言”。

与普通程序不同的是,Java 程序(class 文件)并不是本地的可执行程序。当运行 Java 程序时,首先运行 JVM(Java 虚拟机),然后再把 Java class 加载到 JVM 里头运行,负责加载 Java class 的这部分就叫做 Class Loader。

JVM 中的 ClassLoader

JVM 本身包含了一个 ClassLoader 称为 Bootstrap ClassLoader,和 JVM 一样,BootstrapClassLoader 是用本地代码实现的,它负责加载核心 JavaClass(即所有 java.*开头的类)。

另外 JVM 还会提供两个 ClassLoader,它们都是用 Java 语言编写的,由 BootstrapClassLoader 加载;其中 Extension ClassLoader 负责加载扩展的 Javaclass(例如所有 javax.*开头的类和存放在 JRE 的 ext 目录下的类)ApplicationClassLoader 负责加载应用程序自身的类。

当运行一个程序的时候,JVM 启动,运行 bootstrapclassloader,该 ClassLoader 加载 java 核心 API(ExtClassLoader 和 AppClassLoader 也在此时被加载),然后调用 ExtClassLoader 加载扩展 API,最后 AppClassLoader 加载 CLASSPATH 目录下定义的 Class,这就是一个程序最基本的加载流程。


第一个 Class 文件、通过 javac 编译成字节码、字节码之后有个 ClassLoader 叫类加载器,因为 java.class 文件到 JVM 内部运行起来需要有个装载过程、从物理的文件到内存的结构、比如加载、连接、初始化。

linux 应用程序有个进程地址空间,对进程地址空间的解释:

linux 采用虚拟内存管理技术,每一个进程都有一个 3G 大小的独立的进程地址空间,这个地址空间就是用户空间。每个进程的用户空间都是完全独立、互不相干的。进程访问内核空间的方式:系统调用和中断。创建进程等进程相关操作都需要分配内存给进程。这时进程申请和获得的不是物理地址,仅仅是虚拟地址。 实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,从而进入分配实际页框的程序。该异常是虚拟内存机制赖以存在的基本保证,它会告诉内核去为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在的映射到了物理地址上。

Linux 操作系统采用虚拟内存技术,所有进程之间以虚拟方式共享内存。进程地址空间由每个进程中的线性地址区组成,而且更为重要的特点是内核允许进程使用该空间中的地址。通常情况况下,每个进程都有唯一的地址空间,而且进程地址空间之间彼此互不相干。但是进程之间也可以选择共享地址空间,这样的进程就叫做线程。

基本上所有 linux 应用程序都会遵循这个规泛、有栈、有堆、对于 JVM 来说、也是遵循这个规则、只不过在这个规则上做了一些改进

通过类加载器把 Class 文件装载进内存空间、装进来以后只是你的字节码,然后你需要去运行、怎么去运行呢 ?图中类加载器子系统下面都是运行区内存空间里有:1.方法区:被装载的 class 的信息存储在 Methodarea 的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的 class 文件,然后读入这个 class 文件内容并把它传输到虚拟机中。2.Heap(堆):一个 Java 虚拟实例中只存在一个堆空间。3.JavaStack(java 的栈):虚拟机只会直接对栈执行两种操作:以帧为单位的压栈或出栈,java 栈有个核心的数据、先进后出 4.Nativemethodstack(本地方法栈):通过字面意思、基本是调用系统本地的一些方法、一般在底层封装好了、直接调用 5.地址、在这里边是一个指针的概念、比如从变量到对象怎么做引用、就是地址 6.计数器:主要做字节码解析的时候要记住它的位置、可以理解为一个标记 7.执行引擎:数据、字节码做一些业务处理、最终达到想要的结果 8.本地方法接口:基本是底层系统、比如 IO 网络、调用操作系统本身 9.本地方法库:为了兼容、实现跨平台有不同的库 、兼容平台性额外数据信息指的是本地方法接口和本地方法库


JMM

java 的内存模型

大家可能听过一个词、叫线程安全、在写高并发的时候就会有线程安全问题、java 里边为什么会出现线程安全问题呢、因为有 JMM 的存在、它会把内存分为两个区域(一个主内存、一个是工作内存)工作内存是每个 java 栈所私有的因为要运行速度快、需要把主内存的数据放到本地内存中、然后进行计算、计算完以后再把数据回显回去


JMM 有两个区域、主内存和栈内存、java 线程可能不止一个、可能有多个栈、现在需要三个线程同时做个运算、主内存初始值 x=0 需要把 x=0 都要装载在自己的内存里边去、相当于有一个副本、现在初始值和三个栈都是 x=0 现在需要做运算 x=x+1x=x-1x=0 我们的期望值是 x=0,如果是单个线程跑没问题 、取回 x=0、运算 x=+1、回显进来主内存就是 1 、栈 1 是 1,运算 x=-1、回显进来主内存就是 0、栈 1 是 0

如果多个线程同时执行、结果是不可预期的、正因为有这种结构的存在、当执行 x=+1、栈 1 是 x=1 、栈 2 来不及执行、栈 1 就已经把 x=1 写到主内存了 、栈 2 跟栈 3 拿过去之后初始值就不是 0、可能就是 1 了 、这样程序就写乱了

所以在 java 中就出现了很多锁、来确保线程安全


运行时数据区

PC 寄存器----线程私有

PC 寄存器也叫程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。每一条 JVM 线程都有自己的 PC 寄存器在任意时刻,一条 JVM 线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method)如果该方法是 java 方法,那 PC 寄存器保存 JVM 正在执行的字节码指令的地址如果该方法是 native,那 PC 寄存器的值是 undefined。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈 ----线程私有

与 PC 寄存器一样,java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的。每一个 JVM 线程都有自己的 java 虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。JVM stack 可以被实现成固定大小,也可以根据计算动态扩展。如果采用固定大小的 JVM stack 设计,那么每一条线程的 JVM Stack 容量应该在线程创建时独立地选定。JVM 实现应该提供调节 JVM Stack 初始容量的手段。如果采用动态扩展和收缩的 JVM Stack 方式,应该提供调节最大、最小容量的手段。JVM Stack 异常情况:StackOverflowError:当线程请求分配的栈容量超过 JVM 允许的最大容量时抛出 OutOfMemoryError:如果 JVM Stack 可以动态扩展,但是在尝试扩展时无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈时抛出。

本地方法栈----线程私有

Java 虚拟机可能会使用到传统的栈来支持 native 方法(使用 Java 语言以外的其它语言编写的方法)的执行,这个栈就是本地方法栈(Native Method Stack)

如果 JVM 不支持 native 方法,也不依赖与传统方法栈的话,可以无需支持本地方法栈。

如果支持本地方法栈,则这个栈一般会在线程创建的时候按线程分配。

异常情况:

StackOverflowError:如果线程请求分配的栈容量超过本地方法栈允许的最大容量时抛出

OutOfMemoryError:如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

Jave 堆----线程公用

平时所说的 java 调优就是它在 JVM 中,堆(heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。Java 堆载虚拟机启动的时候就被创建,堆中储存了各种对象,这些对象被自动管理内存系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾回收器)”)所管理。这些对象无需、也无法显示地被销毁。Java 堆的容量可以是固定大小,也可以随着需求动态扩展,并在不需要过多空间时自动收缩。Java 堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。JVM 实现应当提供给程序员调节 Java 堆初始容量的手段,对于可动态扩展和收缩的堆来说,则应当提供调节其最大和最小容量的手段。Java 堆异常:OutOfMemoryError:如果实际所需的堆超过了自动内存管理系统能提供的最大容量时抛出。

方法区----线程公用

方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

方法区在虚拟机启动的时候创建。

方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。

方法区在实际内存空间中可以是不连续的。

Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。

Java 方法区异常:

OutOfMemoryError: 如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常。

JVM 内存分配

内存分配其实真正来讲是有三种的、但对于 JVM 来说只有两种

  • 栈内存分配:

大家在调优的过程中会发现有个参数是-Xss 默认是 1m,这个内存是栈内存分配, 在工作中会发现栈 OutOfMemory Error 内存溢出、就是因为它的内存空间不够了 一般情况下没有那么大的栈、除非你的一个方法里边有几十万行代码、一直往那压、不出,所以导致栈的溢出、栈的内存分配直接决定了你的线程数 、比如说你默认情况下是 1m 、系统一共给你 512m、那最高可以分配 512 个线程,再多系统分配不了啦、因为没有那么多的内存 、像 tomcat、resin、jboss 等、有个最大线程数、要根据这个来调、调个 100 万没有意义、分配不了那么大、调太少整个性能发挥不出来 ,调这个 、跟你的 cpu 有关系、需要找一个折中位置 、根据应用 、是 IO 密集型的还是 CPU 密集型的来调-Xss 的值、它这里边主要保存了一些参数 、还有局部变量 、就比如说写代码、有开始有结束、这里边肯定定义了很多变量、比如:int x=1 y=0 只要在这方法内的都属于局部变量 、因为你要做运算、要把这东西存住、只有等程序结束的时候才能销毁,对于这种参数是不会产生线程安全问题、因为线程是私有的

  • 堆内存分配:

Java 的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过 new、newarray、anewarray 和 multianewarray 等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java 的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢

jvm 堆结构

                     (图一)

1.Young(年轻代)

年轻代分三个区。一个 Eden 区,两个 Survivor 区。大部分对象在 Eden 区中生成。当 Eden 区满时,还存活的对象将被复制到 Survivor 区(两个中的一个),当这个 Survivor 区满时,此区的存活对象将被复制到另外一个 Survivor 区,当这个 Survivor 区也满了的时候,从第一个 Survivor 区复制过来的并且此时还存活的对象,将被复制年老区(Old。需要注意,Survivor 的两个区是对称的,没先后关系,所以同一个区中可能同时存在从 Eden 复制过来对象,和从前一个 Survivor 复制过来的对象,而复制到年老区的只有从第一个 Survivor 区过来的对象。而且,Survivor 区总有一个是空的。

2.Old(年老代)

年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

3.Permanent:(持久代)

也叫方法区、用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至 Survivor 的 from 区,经过多次回收以后如果 from 区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至 to 区。等到 to 区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。

通常我们说的 JVM 内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的 JVM 的 Heap 空间,而持久代则是值指 MethodArea,不属于 Heap。

java 堆结构和垃圾回收

                    图(二)

Direct Momery 严格意义来说也算堆,它是一块物理内存、可以分为操作系统内存、是比较快的、不会走 JVM 在 java 里边实现了内存映射、这样速度更快 CodeCache 放一些字节码、类的信息会放在里边 Permanent Generation space 方法区、严格意义来说也属于堆 Eden Space 区 Survivor Space 区 Tenured Generation Old 区(年老代)

JVM GC 管理调优大部分调优的是怎么回收,Minor GC 回收 Eden Space 和 Survivor Space , Full GC 回收所有区域 不管什么 GC,回收过程中会出现暂停、回收过程中用户线程是不会工作的、这样就造成程序卡了 这是无法改变不了的事实、避免不了、不过可以优化暂停时间的长短原则上不能出现 Full GC 、所有区域都要跑一遍 、出现 Full GC 应用就不可用


Jvm 堆配置参数

1、-Xms 初始堆大小默认物理内存的 64/1(<1GB),建议小于 1G、可根据应用业务调节

2、-Xmx 最大堆大小默认物理内存的 4/1(<1GB)、建议小于 1G、实际中建议不大于 4GB(否则会出现很多问题)

3、一般建议设置 -Xms= -Xmx 好处是避免每次在 gc 后、调整堆的大小、减少系统内存分配开销

4、整个堆大小=年轻代大小+年老代大小+持久代大小(Permanent Generation space 区、也会被 Full GC 回收)

jvm 新生代(young generation

图(三)


1、新生代=1 个 eden 区和 2 个 Survivor 区 2、-Xmn 年轻代大小设置年轻代大小、比如-Xmn=100m 那么新生代就是 100m,然后共享 3、-XX:NewRatio 年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代)Xms=Xmx 并且设置了 Xmn 的情况下,该参数不需要进行设置。4、-XX:SurvivorRatioEden 区与 Survivor 区的大小比值,设置为 8(默认是 8) ,则两个 Survivor 区与一个 Eden 区的比值为 2:8,一个 Survivor 区占整个年轻代的 1/10 比如新生代=100m,设置-XX:SurvivorRatio 为 8,那 E =80m S0 =10m S1=10m(1/10)5、用来存放 JVM 刚分配的 Java 对象

java 老年代(tenured generation)

           图(四)

1、老年代=整个堆-年轻代大小-持久代大小年轻代就是上面讲的-xmn 配置的参数、持久代参数默认是 02、年轻代中经过垃圾回收没有回收掉的对象被复制到年老代。就是这个对象收集完一次、发现被引用了、某个地方使用了、回收不掉才放进去,一般是多次回收、从 E 区回收过程中、先进 S0 或者 S1、S0 或者 S1 再回收一次、回收不掉再放到年老区 3、老年代存储对象比年轻代年龄大的多,而且不乏大对象。对互联网企业来说、最常用的是"缓存"的对象比较多、缓存一般会用弱引用、但弱引用也不会轻易被回收的、除非是在整个堆的内存不够的情况下、防止你的内存宕机、强引用是和垃圾回收机制相关的。一般的,如果一个对象可以通过一系列的强引用引用到,那么就 说明它是不会被垃圾回收机制(Garbage Collection)回收的,刚才说了缓存对象一般是弱引用、有些数据丢了是没关系的、只是提高你的系统性能才放到缓存里边去、但是如果有一天内存不够了 、缓存占了很大一部分对象、你不回收的话、你整个系统都不可用了、整个服务都不能用了、如果回收掉、我可以从数据库去取、可 能速 度慢点、但是我的服务可用性不会降低比如说刚开始分配的对象 、这个对象暂定是 OLD 区、刚开始一部分内存区域被缓存占据了、一般情况下对于一个缓存的设计都有初始值、对于 java 来说、比较通用的缓存是可以自动伸缩的、如图(四)整个 OLD 区 50M 有 45M 是被缓存占据了、不会被回收掉、那整个 OLD 区只有 5M 可以用了 、假如 E 区有 40M 、S0 分配 10M 、S1 分配也是 10M 、理想情况下、经过 E 区到 S0、S1 到老年代的大小不到 1M、 那 5M 就够了、不会出现 FULL GC 、也不会出现 内存溢出、一旦你的对象大于 5M、比如 10M 的数据、 放不进去了、就会出现 FULL gc 、FULL gc 会把整个缓存全都收掉、瞬间缓存数据就没了、然后把 10M 的数据放进去、这就是弱引用、可以理解为这是一种服务降级、如果是强引用那就直接挂了 4、新建的对象也有可能直接进入老年代  4.1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为 0、也就是说所有的默认都在新生代)来代表超过多大时就不再新生代分配,而是直接在老年代分配  4.2、大的数组对象,切数组中无引用外部对象。5、老年代大小无配置参数


java 持久代(perm generation)

1、持久代=整个堆-年轻代大小-老年代大小 2、-XX:PermSize 最小 -XX:MaxPermSize 最大设置持久代的大小,一般情况推荐把-XX:PermSize 设置成 -XX:MaxPermSize 的值为相同的值,因为永久代大小的调整也会导致堆内存需要触发 fgc。3、存放 Class、Method 元信息,其大小与项目的规模、类、方法的数量有关。一般设置为 128M 就足够,设置原则是预留 30%的空间刚开始设置了 128M、随着程序的运行、java 有一个叫 lib 的地方放了很多类库、这个类库并不是所有的都加载的、只有在用的时候或者系统初始化的时候会加载一部分、比如已经占了 100M 了、但是随着业务的运行会动态去类库里加、把一些 Class 文件通过反射的方式装进去、这样你的内存不断增大、达到 128M 以后就挂了、就会报方法区溢出、怎么做?调大到 256M、然后监控、超过阈值再调大、简单方式是调大、另外 JDK 里边有一个 GC 可以回收如果能接受停机、就调大,简单、快速、已解决问题为主 4、永久代的回收方式 4.1、常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收比如一个常量=5 它的意义就是个值、如果回收、发现它没被引用就被回收了 4.2、对于无用的类进行回收,必须保证 3 点:类跟常量不一样、一个类里边可能有好多东西、比如这个类引用那个类、

  • 类的所有实例都已经被回收

  • 加载类的 ClassLoader 已经被回收

  • 类对象的 Class 对象没有被引用(即没有通过反射引用该类的地方)

################################################################

jvm 垃圾收集算法

1、引用计数算法每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减 1,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。还有一个问题是如何解决精准计数。这种方法现在已经不用了 2、根搜索算法从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在 java 语言中,GC Roots 包括:虚拟机栈中引用的对象。方法区中类静态属性实体引用的对象。方法区中常量引用的对象。本地方法栈中 JNI 引用的对象。

jvm 垃圾回收算法

1、复制算法(Copying)


  • 复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。

  • 此算法用于新生代内存回收,从 E 区回收到 S0 或者 S1

从根集合扫描、就是刚才说的 GC-Roots 收集算法、从它开始查你的引用、如果没有被引用、开始执行算法、并将存活对象复制到一块新的、(S0 或者 S1)

2、标记清除算法


  • 标记-清除算法采用从根集合进行扫描,对存活的对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如图所示。

  • 标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!

适合老生代去回收

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

名词解释

1、串行回收 gc 单线程内存回收、会暂停使有用户线程 2、并行回收收集是指多个 GC 线程并行工作,但此时用户线程是暂停的;所以,Seral 是串行的,Paralle 收集器是并行的,而 CMS 收集器是并发的。3、并发回收是指用户线程与 GC 线程同时执行(不一定是并行,可能交替,但总体上是在同时执行的),不需要停顿用户线程(其实在 CMS 中用户线程还是需要停顿的,只是非常短,GC 线程在另一个 CPU 上执行)

串行回收要区分好并行回收和并发回收的区别,这地方非常关键、在选择 GC 的过程中根据应用场景来选择

JVM 常见垃圾回收器

上图是 HotSpot 里的收集器,中间的横线表示分代,有连线表示可以组合使用。

年轻代区域有 Serial 串行 ParNew 并发 Parallel Scavenge 并行年老代区域有 CMSSerial Old Parallel OldG1 目前还不成熟 、适合年轻代和年老代

Serial 回收器(串行回收器)


是一个单线程的收集器,只能使用一个 CPU 或一条线程区完成垃圾收集;在进行垃圾收集时,必须暂停所有其它工作线程,直到收集完成。缺点:Stop-The-World 优势:简单。对于单 CPU 的情况,由于没有多线程交互开销,反而可以更高效。是 Client 模式下默认的新生代收集器。

新生代 Serial 回收器 1、通过-XX:+UseSerialGC 来开启 Serial New+Serial Old 的收集器组合进行内存回收 2、使用复制算法。3、独占式的垃圾回收。一个线程进行 GC,串行。其它工作线程暂停。老年代 Serial 回收器 1、-XX:UseSerialGC 来开启 Serial New+Serial Old 的收集器组合进行内存回收 2、使用标记-压缩算法 3、串行的、独占式的垃圾回收器。因为内存比较大的原因,回收比新生代慢

ParNew 回收器(并行回收器)


并行回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,因此,在并发能力比较强的 CPU 上,它产生的停顿时间要短于串行回收器,而在单 CPU 或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,由于多线程的压力,它的实际表现很可能比串行回收器差。

新生代 ParNew 回收器

1、-XX:+UseParNewGC 开启新生代使用并行回收收集器,老年代使用串行收集器 2、-XX:ParallelGCThreads 指定线程数默认最好与 CPU 数理相当,避免过多的线程数影响垃圾收集性能 3、使用复制算法。4、并行的、独占式的垃圾回收器。

新生代 Parallel Scavenge 回收器

1、吞吐量优先回收器关注 CPU 吞吐量,即运行用户代码的时间/总时间。比如:JVM 运行 100 分钟,其中运行用户代码 99 分钟,垃圾回收 1 分钟。则吞吐量是 99%,这种收集器能最高效率的利用 CPU,适合运行后台运算 2、-XX:+UseParallelGC 开启使用 Parallel Scavenge+Serial Old 收集器组合回收垃圾,这也是 Server 模式下的默认值 3、-XX:GCTimeRation 来设置用户执行时间占总时间的比例,默认 99,即 1%的时间用来进行垃圾回收 4、-XX:MaxGCPauseMillis 设置 GC 的最大停顿时间 5、使用复制算法

老生代 Parallel Old 回收器

1、-XX:+UseParallelOldGC 开启使用 Parallel Scavenge +Parallel Old 组合收集器进行收集 2、使用标记整理算法。3、并行的、独占式的垃圾回收器。

CMS(并发标记清除)回收器


运作过程分为 4 个阶段:初始标记(CMS inital mark):值标记 GC Roots 能直接关联到的对象。并发标记(CMS concurrent mark):进行 GC RootsTracing 的过程。重新标记(CMS remark):修正并发标记期间用户程序继续运行而导致标记发生改变的那一部分对象的标记.

并发清除(CMS concurrent sweep):其中标记和重新标记两个阶段仍然需要 Stop-The-World,整个过程中耗时最长的并发标记和并发清除过程中收集器都可以和用户线程一起工作

CMS(并发标记清除)回收器

1、标记-清除算法同时它又是一个使用多线程并发回收的垃圾收集器 2、-XX:ParalleCMSThreads 手工设定 CMS 的线程数量,CMS 默认启动的线程数是(ParallelGCTherads+3)+3/4)这是它的公式,一般情况下、对于 IO 密集型的 cpu 的核数乘以 2+1 ,CPU 密集型的一般 CPU 的核数+13、-XX+UseConcMarkSweepGC 开启使用 ParNew+CMS+Serial Old 的收集器组合进行内存回收,Serial Old 作为 CMS 出现“Concurrent Mode Failure” 失败后的后备收集器使用.

失败以后就会触发 Full GC 、位了避免这种情况发生、就要对它进行配置、触发 Full GC 有两种情况、promotion failed 和 concurrent mode failure 对于采用 CMS 进行老年代 GC 的程序而言,尤其要注意 GC 日志中是否有 promotion failed 和 concurrent mode failure 两种状况,当这两种状况出现时可能会触发 Full GC。promotion failed 是在进行 Minor GC 时,survivor space 放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC)。对应措施为:增大 survivor space、老年代空间或调低触发并发 GC 的比率。

4、-XX:CMSInitiatingOccupancyFraction 设置 CMS 收集器在老年代空间被使用多少后触发垃圾回收器,默认值为 68%,仅在 CMS 收集器时有效,-XX:CMSInitiatingOccupancyFraction=70

(一般情况为 70%,设太高了可能会出现失败,设太低了、频繁, 只能去找一个比值、可以分析 GC log、看是否符合你的要求 )

5、-XX:+UseCMSCompactAtFullCollection 由于 CMS 收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在 CMS 收集器时有效

6、-XX:+CMSFullGCBeforeCompaction 设置 CMS 收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与 UseCMSCompactAtFullCollection 参数一起使用 7、-XX:CMSInitiatingPermOccupancyFraction 设置 Perm Gen 使用到达多少比率时触发,默认 92%


如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份 Java 核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!


发布于: 2021 年 05 月 11 日阅读数: 57
用户头像

Java王路飞

关注

需要资料添加小助理vx:17375779923 即可 2021.01.29 加入

Java领域;架构知识;面试心得;互联网行业最新资讯

评论

发布
暂无评论
JVM原理与深度调优