串一串 JVM

Posted 寂静花开

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了串一串 JVM相关的知识,希望对你有一定的参考价值。

目 录

first:你对JVM有什么了解吗?

为什么需要JVM这种东西?像python那种程序不是更好,一边编一边运行。且运行要搞一个虚拟机来运行它呢?

编译型语言和解释型语言

首先要了解编译型语言和解释型语言的区别。

编译型语言是指将源代码一次性全部转化为二进制机器码来进行运行。
比如C语言。

解释型语言是一边执行一边转换,然后需要哪些人代码,哪些原代码就转化为机器码。
比如Python语言。

同时
编译型语言的缺点就是可移植性差、不灵活。
1、是原代码不能跨平台执行
2、是编译后的可执行文件也不能跨平台。
他的优点就是一次编译可以无限次运行。

解释型语言的缺点
1、一边执行一边转换,导致效率很低。
但他的优点是跨平台信号通过不同的解释器将相同的源代码,然后解释成不同平台下的机器码。

但是Java比较奇葩,Java是半编译型半解释型语言。原代码需要先转换成一种字节码文件在Java中,就是.class文件,然后再将这个文件然后在虚拟机中执行,Java中设计了这种机制,然后他的初衷就是在跨平台的基础上,然后提高执行效率。他其中的跨平台就是用.class文件,然后匹配不同的虚拟机就可以在不同的平台上运行。

所以为了实现跨平台。许多语言用的都会有虚拟机。java系的语言都会使用JVM,比如kotlin、scale。

JVM的内存结构


JVM主要由类加载器、类运行时数据区、执行引擎和本地方法接口组成。

其中最重要的部分是运行时数据区。
里面包括:

  • 堆heap
    大部分对象在堆上
  • 方法区(1.8 元空间)
    一般放静态方法之类的
  • 栈stack
  • 本地方法栈Native Method Stack
  • 程序计数器
    记录线程执行到哪里,

其中,堆、方法区、栈这三个是重点

程序计数器

内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

JVM会出现的常见错误

在栈中

  • StackOverFlowError:当JVM的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前JVM虚拟机的最大深度的时候,会报这个错
  • OutOfMemoryError:JVM内存允许动态扩展,如果虚拟机在动态扩展时无法申请到足够的内存空间,会报OOM这个错。(开始一个项目的时候可以配好)

但其实更多时候出问题都在堆中。



其中Eden:S0:S1 = 8:1:1
老年代 :新生代 = 2:1
为什么是这个比例呢?
因为设计的人根据统计学规律得出了,这个参数是可以改的,但一般最好不要动。

java创建一个对象之后,是怎么存的呢?

对象有一个对象头,里面存了一个值代表年龄。
这个时候默认分到伊甸园区。
一段时间后,发现Eden区快满了, JVM有一个守护线程,叫做垃圾回收线程就出现了。
这个垃圾回收线程叫做MiniGC,他会通过垃圾回收算法(引用计数法),把所有存活对象引到S0区,然后这个对象的年龄加1 。每幸存一次,对象的年龄就会加1 。当他的年龄达到15的时候,就会进入老年代中。(一些比较大的对象,会直接进入老年区)
老年代慢了后,触发MajorGC,清理老年区对象,剩下的移到永久区。

问:清理垃圾的时候,还可以继续执行java程序吗?
答:当然不可以啦。不可以一边扫地一边扔垃圾呀。
专业术语叫stopworld(所以程序会有一个小小的停顿)

垃圾回收算法(六种)

引用计数法

引用计数法就是把每个对象都统计一下,看一看这个对象有没有给其他代码或者对象引用到。引用到的话就记为1。否则记为0。如果为0的话,说明这个对象不太可能会被引用到,那么这个对象就是可回收对象。

缺点是:无法检测出循环引用,
如果有一个A对象和一个B对象,其中,A一直引用B对象,然后B引用A对象,那么他俩永远也无法回收。
也正因为这个缺点,所以 Java虚拟机不使用引用计数法。

根可达法/可达性分析算法。

把必要的一定会用到的对象当做根对象。然后看这个根对象会引用到哪些对象,后续我们只保留这些被引用到的对象,对其他对象进行回收。
所以这个算法的关键点就在于我们应该把哪些对象作为根节点对象GC Root。
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
一般来说,根对象包括:

  • 栈帧中的局部变量
  • 已加载类的静态变量
  • JNI handles
  • 已启动且未停止的Java线程

以上两个算法是具体的分析哪对象是可回收的,哪个是不可回收的。他只是分析了哪些对象是要被回收的,但是没有具体的回收过程。

标记清除算法

⾸先标记出所有不需要回收的对象,在标记完成后统⼀回收掉所有没有被标记的对象。
但是他有一个非常显著的缺点就是空间问题。

就比如说你这个空间内存里头总共有100个对象,然后大概有98个都是不用的,但是这98个中间是分散的,所以当你把这98个删除完的时候,你可能第1个位置有一个需要的对象,第10个位置有一个需要的对象,最后又有一个需要的对象,然后导致它的空间被分割非常分散,会产生大量不连续的碎片。

复制算法

它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使用的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进行回收。

用于前面说的Mini GC

标记压缩/标记-整理算法

根据⽼年代的特点提出的⼀种标记算法,标记过程仍然与 标记-清除 算法⼀样,但后续步骤不是
直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的对象。

用于上文提到的Major GC

分代收集算法

根据对象存活周期的不同将内存分为几块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新⽣代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活⼏率是⽐较高的,而且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。

垃圾回收器(了解)

常见的垃圾回收器种类

新生代收集器

  • Serial
  • ParNew
  • parallel

老年代收集器

  • Serial Old
  • Parallel Old

新生代和老年代收集器

  • G1

Serial 收集器

概述: Serial是一类用于新生代的单线程收集器,采用 复制算法 进行垃圾收集。Serial进行垃圾收集时,不仅只用一条单线程执行垃圾收集工作,它还在收集的同时,所用的用户必须暂停。

特点

  • 线程
  • 简单高效(与其他收集器的单线程相比)
  • 对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。
  • 收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。

应用场景:适用于单核服务器、Client模式下的虚拟机。

优势:简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
缺点:会在用户不知道的情况下停止所有工作线程,用户体验感极差,令人难以接受。

ParNew收集器

概述: parNew收集器其实就是Serial的一个多线程版本,其在单核cpu上的表现并不会比Serail收集器更好,在多核机器上,其默认开启的收集线程数与cpu数量相等。

除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。

特点

  • 多线程
  • ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
  • 和Serial收集器一样存在Stop The World问题

优点:随着cpu的有效利用,对于GC时系统资源的有效利用有好处。
缺点:和Serial是一样的。
适用场景:ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器。因为CMS收集器只能与serial或者parNew联合使用,在当下多核系统环境下,首选的是parNew与CMS配合。ParNew收集器也是使用CMS收集器后默认的新生代收集器。

Parallel Scavenge 收集器

概述: Parallel Scavenge也是一款用于新生代的多线程收集器,也是采用复制算法。
与ParNew的不同之处在于 Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,而ParNew收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间).

特点

  • 属于新生代收集器也是采用复制算法的收集器,
  • 又是并行的多线程收集器(与ParNew收集器类似)。

优点追求高吞吐量(运行代码的时间/垃圾回收的时间 的比值最高)。高效利用CPU,是吞吐量优先,且能进行精确控制。

Serial Old 收集器

Serial Old是Serial收集器的老年代版本。

特点

  • 单线程收集器,
  • 采用标记—整理算法。

应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。

Server模式下主要的两大用途(在后续中详细讲解···):

在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
Serial / Serial Old收集器工作过程图(Serial收集器图示相同):

Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本。

特点

  • 多线程
  • 采用标记-整理算法。

应用场景:注重吞吐量与CPU资源敏感的场合,与Parallel Scavenge 收集器搭配使用,jdk7和jdk8默认使用该收集器作为老年代收集器。

CMS收集器

一种以获取最短回收停顿时间为目标的收集器。

特点:基于标记-清除算法实现。并发收集、低停顿。

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。

CMS收集器的运行过程分为下列4步

  • 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
  • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
  • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
  • 并发清除:对标记的对象进行清除回收。

CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器的缺点:

  • 对CPU资源非常敏感。
  • 无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
  • 因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。

G1收集器

一款面向服务端应用的垃圾收集器。

特点如下:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。

  • 分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

  • 空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。

  • 可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

以上是关于串一串 JVM的主要内容,如果未能解决你的问题,请参考以下文章

谈JVM参数GC线程数ParallelGCThreads合理性设置

Qt_C2188-负下标C2027QScopedPointer

JVM笔记-HotSpot的算法细节实现

JVM常见配置

JVM之GI收集器

JVM典型配置和调优举例