乐趣区

关于jvm虚拟机:Java-并发编程解析-如何正确理解Java领域中的内存模型主要是解决了什么问题

天穹之边,浩瀚之挚,眰恦之美;悟心悟性,虎头蛇尾,惟善惟道!—— 朝槿《朝槿兮年说》

写在结尾

这些年,随着 CPU、内存、I/O 设施都在一直迭代,一直朝着更快的方向致力。在这个疾速倒退的过程中,有一个外围矛盾始终存在,就是这三者的速度差别。CPU 和内存的速度差别能够形象地形容为:CPU 是天上一天,内存是地上一年(假如 CPU 执行一条一般指令须要一天,那么 CPU 读写内存得期待一年的工夫)。内存和 I/O 设施的速度差别就更大了,内存是天上一天,I/O 设施是地上十年。

咱们都晓得的是,程序里大部分语句都要拜访内存,有些还要拜访 I/O,依据木桶实践(一只水桶能装多少水取决于它最短的那块木板),程序整体的性能取决于最慢的操作——读写 I/O 设施,也就是说单方面进步 CPU 性能是有效的。

为了正当利用 CPU 的高性能,均衡这三者的速度差别,计算机体系结构、操作系统、编译程序都做出了奉献,次要体现为:

  1. 古代计算机在 CPU 减少了缓存,以平衡与内存的速度差别
  2. 操作系统减少了过程、线程,以分时复用 CPU,进而平衡 CPU 与 I/O 设施的速度差别
  3. 编译程序优化指令执行秩序,使得缓存可能失去更加正当地利用

由此可见,尽管当初咱们简直所有的程序都默默地享受着这些成绩,然而理论利用程序设计和开发过程中,还是有很多诡异问题困扰着咱们。

根本概述

每当提起 Java 性能优化,你是否有想过,真正须要咱们优化的是什么?或者说,领导咱们优化的方向和指标是否明确?甚至说,咱们所做的所有,是否曾经达到咱们的冀望了呢?接下来,咱们来具体探讨一下。

性能优化依据优化的方向和指标来说,大抵能够分为业务优化和技术优化。业务优化产生的影响是十分微小的,个别最常见的就是业务需要变更和业务场景适配等,当然这是产品和项目管理的工作领域。而对于咱们开发人员来说,咱们须要关注的和间接与咱们相干的,次要是通过一系列的技术手段,来实现咱们对既定目标的技术优化。其中,从技术手段方向来看,技术优化次要能够从复用优化,后果汇合优化,高效实现优化,算法优化,计算优化,资源抵触优化和 JVM 优化等七个方面着手。

一般来说,技术优化根本都集中在计算机资源和存储资源的布局上,最间接的就是对于服务器和业务应用程序相干的资源做具体的剖析,在关照性能的前提下,同时也兼顾业务需要的要求,从而达到资源利用最优的状态。一味地强调利用空间换工夫的形式,只看计算速度,不思考复杂性和空间的问题,的确有点不可取。特地是在云原生时代下和无服务时代,尽管含糊和缩小了开发对这些问题的间隔,然而咱们更加须要理解和关注这些问题的本质。

特地指出的是,JVM 优化。因为应用 Java 编写的应用程序,自身 Java 是运行在 JVM 虚拟机上的,这就意味着它会受到 JVM 的制约。对于 JVM 虚拟机的优化。肯定水平上会晋升 Java 应用程序的性能。如果参数配置不当,导致内存溢出 (OOM 异样) 等问题,甚至引发比这更重大的结果。

由此可见,正确认识和把握 JVM 构造相干常识,对于咱们何尝不是一个进阶的技术方向。当然,JVM 虚拟机这一部分的内容,绝对编写 Java 程序来说,更加比拟枯燥无味,概念比拟多且形象,须要咱们要有更多的急躁和仔细。咱们都晓得,一颗不塌实的心,做任何事都会播种不一样的精彩。

Java JVM 虚拟机

在开始这一部分内容之前,咱们先来看一下,在 Java 中,Java 程序是如何运行的,最初又是如何交给 JVM 托管的?

1.Java 程序运行过程

作为一名 Java 程序员,你应该晓得,Java 代码有很多种不同的运行形式。比如说能够在开发工具中运行,能够双击执行 jar 文件运行,也能够在命令行中运行,甚至能够在网页中运行。当然,这些执行形式都离不开 JRE,也就是 Java 运行时环境。

实际上,JRE 仅蕴含运行 Java 程序的必须组件,包含 Java 虚拟机以及 Java 外围类库等。咱们 Java 程序员常常接触到的 JDK(Java 开发工具包)同样蕴含了 JRE,并且还附带了一系列开发、诊断工具。

然而,运行 C++ 代码则无需额定的运行时。咱们往往把这些代码间接编译成 CPU 所能了解的代码格局,也就是机器码。

Java 作为一门高级程序语言,它的语法非常复杂,形象水平也很高。因而,间接在硬件上运行这种简单的程序并不事实。所以呢,在运行 Java 程序之前,咱们须要对其进行一番转换。

这个转换具体是怎么操作的呢?以后的支流思路是这样子的,设计一个面向 Java 语言个性的虚拟机,并通过编译器将 Java 程序转换成该虚拟机所能辨认的指令序列,也称 Java 字节码。这里顺便说一句,之所以这么取名,是因为 Java 字节码指令的操作码(opcode)被固定为一个字节。

并且,咱们同样能够将其反汇编为人类可读的代码格局(如下图的最右列所示)。不同的是,Java 版本的编译后果绝对精简一些。这是因为 Java 虚拟机绝对于物理机而言,形象水平更高。

Java 虚拟机能够由硬件实现[1],但更为常见的是在各个现有平台(如 Windows_x64、Linux_aarch64)上提供软件实现。这么做的意义在于,一旦一个程序被转换成 Java 字节码,那么它便能够在不同平台上的虚拟机实现里运行。这也就是咱们常常说的“一次编写,到处运行”。

虚拟机的另外一个益处是它带来了一个托管环境(Managed Runtime)。这个托管环境可能代替咱们解决一些代码中简短而且容易出错的局部。其中最广为人知的当属主动内存治理与垃圾回收,这部分内容甚至催生了一波垃圾回收调优的业务。

除此之外,托管环境还提供了诸如数组越界、动静类型、平安权限等等的动静检测,使咱们免于书写这些无关业务逻辑的代码。

2.Java 程序创立过程

从 class 文件到内存中的类,按先后顺序须要通过加载、链接以及初始化三大步骤。其中,链接过程中同样须要验证;而内存中的类没有通过初始化,同样不能应用。那么,是否所有的 Java 类都须要通过这几步呢?

咱们晓得 Java 语言的类型能够分为两大类:根本类型(primitive types)和援用类型(reference types)。在上一篇中,我曾经具体介绍过了 Java 的根本类型,它们是由 Java 虚拟机事后定义好的。

至于另一大类援用类型,Java 将其细分为四种:类、接口、数组类和泛型参数。因为泛型参数会在编译过程中被擦除(我会在专栏的第二局部具体介绍),因而 Java 虚拟机实际上只有前三种。在类、接口和数组类中,数组类是由 Java 虚拟机间接生成的,其余两种则有对应的字节流。

说到字节流,最常见的模式要属由 Java 编译器生成的 class 文件。除此之外,咱们也能够在程序外部间接生成,或者从网络中获取(例如网页中内嵌的小程序 Java applet)字节流。这些不同模式的字节流,都会被加载到 Java 虚拟机中,成为类或接口。为了叙述不便,上面我就用“类”来统称它们。

无论是间接生成的数组类,还是加载的类,Java 虚拟机都须要对其进行链接和初始化。

其实,Java 虚拟机将字节流转化为 Java 类的过程,就是咱们常说的 Java 类的创立过程。这个过程可分为加载、链接以及初始化三大步骤:

  • 加载是指查找字节流,并且据此创立类的过程。加载须要借助类加载器,在 Java 虚拟机中,类加载器应用了双亲委派模型,即接管到加载申请时,会先将申请转发给父类加载器。
  • 链接,是指将创立成的类合并至 Java 虚拟机中,使之可能执行的过程。链接还分验证、筹备和解析三个阶段。其中,解析阶段为非必须的。
  • 初始化,则是为标记为常量值的字段赋值,以及执行 < clinit > 办法的过程。类的初始化仅会被执行一次,这个个性被用来实现单例的提早初始化。
3.Java 程序加载过程

从虚拟机视角来看,执行 Java 代码首先须要将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被寄存于办法区(Method Area)中。理论运行时,虚构机会执行办法区内的代码。

如果你相熟 X86 的话,你会发现这和段式内存治理中的代码段相似。而且,Java 虚拟机同样也在内存中划分出堆和栈来存储运行时数据。

不同的是,Java 虚构机会将栈细分为面向 Java 办法的 Java 办法栈,面向本地办法(用 C++ 写的 native 办法)的本地办法栈,以及寄存各个线程执行地位的 PC 寄存器。

在运行过程中,每当调用进入一个 Java 办法,Java 虚构机会在以后线程的 Java 办法栈中生成一个栈帧,用以寄存局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且 Java 虚拟机不要求栈帧在内存空间里间断散布。

当退出以后执行的办法时,不论是失常返回还是异样返回,Java 虚拟机均会弹出以后线程的以后栈帧,并将之舍弃。

从硬件视角来看,Java 字节码无奈间接执行。因而,Java 虚拟机须要将字节码翻译成机器码。

启动类加载器是由 C++ 实现的,没有对应的 Java 对象,因而在 Java 中只能用 null 来指代。
除了启动类加载器之外,其余的类加载器都是 java.lang.ClassLoader 的子类,因而有对应的 Java 对象。这些类加载器须要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中,方能执行类加载。

在 Java 虚拟机中,这个潜规则有个特地的名字,叫双亲委派模型。每当一个类加载器接管到加载申请时,它会先将申请转发给父类加载器。在父类加载器没有找到所申请的类的状况下,该类加载器才会尝试去加载。

在 Java 9 之前,启动类加载器负责加载最为根底、最为重要的类,比方寄存在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。除了启动类加载器之外,另外两个重要的类加载器是扩大类加载器(extension class loader)和利用类加载器(application class loader),均由 Java 外围类库提供。

扩大类加载器的父类加载器是启动类加载器。它负责加载绝对主要、但又通用的类,比方寄存在 JRE 的 lib/ext 目录下 jar 包中的类(以及由零碎变量 java.ext.dirs 指定的类)。

利用类加载器的父类加载器则是扩大类加载器。它负责加载应用程序门路下的类。(这里的应用程序门路,便是指虚拟机参数 -cp/-classpath、零碎变量 java.class.path 或环境变量 CLASSPATH 所指定的门路。)默认状况下,应用程序中蕴含的类便是由利用类加载器加载的。

Java 9 引入了模块零碎,并且稍微更改了上述的类加载器 1。扩大类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个要害模块,比如说 java.base 是由启动类加载器加载之外,其余的模块均由平台类加载器所加载。

除了由 Java 外围类库提供的类加载器外,咱们还能够退出自定义的类加载器,来实现非凡的加载形式。举例来说,咱们能够对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。

除了加载性能之外,类加载器还提供了命名空间的作用。在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一起确定的。即使是同一串字节流,经由不同的类加载器加载,也会失去两个不同的类。在大型利用中,咱们往往借助这一个性,来运行同一个类的不同版本。

4.Java 程序编译过程

在 HotSpot 外面,上述翻译过程有两种模式:

  • 第一种是解释执行,即逐条将字节码翻译成机器码并执行;
  • 第二种是即时编译(Just-In-Time compilation,JIT),行将一个办法中蕴含的所有字节码编译成机器码后再执行。

前者的劣势在于无需期待编译,而后者的劣势在于理论运行速度更快。HotSpot 默认采纳混合模式,综合了解释执行和即时编译两者的长处。它会先解释执行字节码,而后将其中重复执行的热点代码,以办法为单位进行即时编译。

HotSpot 采纳了多种技术来晋升启动性能以及峰值性能,刚刚提到的即时编译便是其中最重要的技术之一。

即时编译建设在程序合乎二八定律的假如上,也就是百分之二十的代码占据了百分之八十的计算资源。

对于占据大部分的不罕用的代码,咱们无需消耗工夫将其编译成机器码,而是采取解释执行的形式运行;另一方面,对于仅占据小局部的热点代码,咱们则能够将其编译成机器码,以达到现实的运行速度。

实践上讲,即时编译后的 Java 程序的执行效率,是可能超过 C++ 程序的。这是因为与动态编译相比,即时编译领有程序的运行时信息,并且可能依据这个信息做出相应的优化。

举个例子,咱们晓得虚办法是用来实现面向对象语言多态性的。对于一个虚办法调用,只管它有很多个指标办法,但在理论运行过程中它可能只调用其中的一个。这个信息便能够被即时编译器所利用,来躲避虚办法调用的开销,从而达到比动态编译的 C++ 程序更高的性能。

为了满足不同用户场景的须要,HotSpot 内置了多个即时编译器:C1、C2 和 Graal。

  • Graal 是 Java 10 正式引入的实验性即时编译器,在专栏的第四局部我会具体介绍,这里暂不做探讨。之所以引入多个即时编译器,是为了在编译工夫和生成代码的执行效率之间进行取舍。
  • C1 又叫做 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采纳的优化伎俩绝对简略,因而编译工夫较短。
  • C2 又叫做 Server 编译器,面向的是对峰值性能有要求的服务器端程序,采纳的优化伎俩绝对简单,因而编译工夫较长,但同时生成代码的执行效率较高。

从 Java 7 开始,HotSpot 默认采纳分层编译的形式:热点办法首先会被 C1 编译,而后热点办法中的热点会进一步被 C2 编译。
为了不烦扰利用的失常运行,HotSpot 的即时编译是放在额定的编译线程中进行的。HotSpot 会依据 CPU 的数量设置编译线程的数目,并且按 1:2 的比例配置给 C1 及 C2 编译器。

在计算资源短缺的状况下,字节码的解释执行和即时编译可同时进行。编译实现后的机器码会在下次调用该办法时启用,以替换本来的解释执行。

5.Java 虚拟机构造

从组成构造上看,一个 Java 虚拟机(HotSpot 为例),次要包含指令汇合,指令解析器,程序执行指令 等 3 个方面,其中:

  • 指令汇合:指的是咱们常说的字节码 (Byte Code), 次要指将源文件代码(Source File Code) 编译运行生成的, 比方在 Java 中是通过 javac 命令编译(.java) 文件生成,而在 Python 中是通过 jython 命令来编译 (.py) 文件生成。
  • 指令解析器:次要是指字节码解释器 (Byte Code Interpreter) 和即时编译器(JIT Compiler),比方一个 Java 虚拟机(HotSpot 为例),就有一个字节码解释器和两个即时编译器(Server 编译器和 Client 编译器)。
  • 程序执行指令:次要是指操作内存区域,以装载和执行,个别是 JVM 负责 将 字节码 解释成具体的机器指令来执行。

一般来说,任何一个 Java 虚拟机都会蕴含这三个方面的,然而具体的有各有所不同:

  1. 字节码指令:JVM 具备针对以下工作组的字节码指令标准:加载和存储,算术,类型转换,对象创立和操作,操作数栈治理(push/pop),管制转移(分支),办法调用和返回,抛出异样,基于监视器的并发。被加载到 JVM 后能够被执行,其中字节码是实现跨平台的根底。
  2. 字节码解释器:用于将字节码解析成计算机能执行的语言,一台计算机有了 Java 字节码解释器后,它就能够运行任何 Java 字节码程序。同样的 Java 程序就能够在具备了这种解释器的硬件架构的计算机上运行, 实现了“跨平台”。
  3. JIT 即时编译器:JIT 编译器能够在执行程序时将 Java 字节码翻译老本地机器语言。一般来讲,Java 字节码通过 字节码解释器执行时,执行速度总是比编译成本地机器语言的同一程序的执行速度慢。而 即时编译器 在执行程序时将 Java 字节码翻译老本地机器语言,以显著放慢整体执行工夫。
  4. JVM 操作内存:JVM 有一个堆 (heap) 用于存储对象和数组。垃圾回收器要在这里工作。代码、常量和其余类数据存储在办法区 (method area) 中。每个 JVM 线程也有本人的调用栈 (JVM stack),用于存储“帧”。每次调用办法时都会创立一个新的 帧(放到栈里),并在该办法退出时销毁该帧。每个帧提供一个操作数堆栈 (operand stack) 和一个局部变量数组 (local variables)。操作数栈用于计算操作数和接管被调用办法的 “ 返回值 ”,而局部变量数据用于传递“办法参数”。

除此之外,每个特定的主机操作系统都须要本人的 JVM 和运行时实现。

6.Java GC 垃圾回收

Java 虚拟机提供了一系列的垃圾回收机制(Garbage Collection), 又或者说是垃圾回收器(Garbage Collector), 其中常见的垃圾回收器如下:

  • Serial GC(Serial Garbage Collection):第一代 GC,是 1999 年在 JDK1.3 中公布的串行形式的单线程 GC。个别实用于 最小化地应用内存和并行开销的场景。
  • Parallel GC(Parallel Garbage Collection):第二代 GC,是 2002 年在 JDK1.4.2 中公布的,相比 Serial GC,基于多线程形式减速运行垃圾回收,在 JDK6 版本之后成为 Hotspot VM 的默认 GC。个别是最大化应用程序的吞吐量。
  • CMS GC(Concurrent Mark Sweep Garbage Collection):第二代 GC,是 2002 年在 JDK1.4.2 中公布的,相比 Serial GC,基于多线程形式减速运行垃圾回收,能够让应用程序和 GC 分享处理器资源的 GC。个别是最小化 GC 的中断和进展工夫的场景。
  • G1 GC (Garbage First Garbage Collection):第三代 GC,是 JDK7 版本中诞生的一个并行回收器,次要是针对“垃圾优先”的准则而诞生的 GC,也是时下咱们比拟新的 GC。

在常见的垃圾回收中,咱们个别采纳援用计数法和可达性剖析两种形式来确定垃圾是否产生,其中:

  • 援用计数法:在 Java 中,援用和对象是有关联的。如果要操作对象则必须用援用进行。因而,很显然一个简略的方法是通过援用计数来判断一个对象是否能够回收。简略说,即一个对象如果没有任何与之关联的援用,即他们的援用计数都不为 0,则阐明对象不太可能再被用到,那么这个对象就是可回收对象。
  • 可达性剖析(根搜索算法):为了解决援用计数法的循环援用问题,Java 应用了可达性剖析的办法。通过一系列的“GC roots”对象作为终点搜寻。如果在“GC roots”和一个对象之间没有可达门路,则称该对象是不可达的。要留神的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至多要通过两次标记过程。两次标记后依然是可回收对象,则将面临回收。

一般来说,当胜利辨别出内存中存活对象和死亡对象之后,GC 接着就会执行垃圾回收,开释掉无用对象所占用的内存空间,以便有足够可用的内存空间为新的对象分配内存。

目前,在 JVM 中采纳的垃圾收集算法次要有:

  • 标记 - 革除算法(Mark-Sweep): 最根底的垃圾回收算法,分为两个阶段,标注和革除。标记阶段标记出所有须要回收的对象,革除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化重大,后续可能产生大对象不能找到可利用空间的问题。
  • 复制算法(Copying): 为了解决 Mark-Sweep 算法内存碎片化的缺点而被提出的算法。按内存容量将内存划分为等大小的两块。每次只应用其中一块,当这一块内存满后将尚存活的对象复制到另一块下来,把已应用的内存清掉。这种算法尽管实现简略,内存效率高,不易产生碎片,然而最大的问题是可用内存被压缩到了本来的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
  • 标记 - 压缩算法(Mark-Compact): 为了防止缺点而提出。标记阶段和 Mark-Sweep 算法雷同,标记后不是清理对象,而是将存活对象移向内存的一端,而后革除端边界外的对象。
  • 增量算法(Incremental Collecting): 也能够成为分区收集算法(Region Collenting),将整个堆空间划分为间断的不同小区间, 每个小区间独立应用, 独立回收. 这样做的益处是能够管制一次回收多少个小区间 , 依据指标进展工夫, 每次正当地回收若干个小区间(而不是整个堆), 从而缩小一次 GC 所产生的进展。
  • 分代收集算法 (Generational Collenting): 是目前大部分 JVM 所采纳的办法,其核心思想是依据对象存活的不同生命周期将内存划分为不同的域,个别状况下将 GC 堆划分为老生代(Tenured/Old Generation) 和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有大量对象须要被回收,新生代的特点是每次垃圾回收时都有大量垃圾须要被回收,因而能够依据不同区域抉择不同的算法。
7.Java JVM 调优

JVM 调优波及到两个很重要的概念:吞吐量和响应工夫。jvm 调优次要是针对他们进行调整优化,达到一个现实的指标,依据业务确定指标是吞吐量优先还是响应工夫优先。

  • 吞吐量:用户代码执行工夫 /(用户代码执行工夫 +GC 执行工夫)。
  • 响应工夫:整个接口的响应工夫(用户代码执行工夫 +GC 执行工夫),stw 工夫越短,响应工夫越短。

调优的前提是熟悉业务场景,先判断出以后业务场景是吞吐量优先还是响应工夫优先。调优须要建设在监控之上,由压力测试来判断是否达到业务要求和性能要求。调优的步骤大抵能够分为:

  1. 熟悉业务场景,理解以后业务零碎的要求,是吞吐量优先还是响应工夫优先;
  2. 抉择适合的垃圾回收器组合,如果是吞吐量优先,则抉择 ps+po 组合;如果是响应工夫优先,在 1.8 当前抉择 G1,在 1.8 之前抉择 ParNew+CMS 组合;
  3. 布局内存需要,只能进行大抵的布局。
  4. CPU 抉择,在估算之内性能越高越好;
  5. 依据理论状况设置降级年龄,最大年龄为 15;
  6. 依据须要设定相干的 JVM 日志参数:

       -Xloggc:/path/name-gc-%t.log 
         -XX:+UseGCLogFileRotation 
         -XX:NumberOfGCLogs=5
         -XX:GCLogFileSize=20M 
         -XX:+PrintGCDetails
         -XX:+PrintGCDateStamps 
         -XX:+PrintGCCauses

    其中须要留神的是:

       -XX:+UseGCLogFileRotation:GC 文件循环应用
       -XX:NumberOfGCLogs=5:应用 5 个 GC 文件
       -XX:GCLogFileSize=20M:每个 GC 文件的大小

下面这三个参数放在一起代表的含意是:5 个 GC 文件循环应用,每个 GC 文件 20M,总共应用 100M 存储日志文件,当 5 个 GC 文件都应用结束当前,笼罩第一个 GC 日志文件,生成新的 GC 文件。

当 cpu 常常飙升到 100% 的使用率,那么证实有线程长时间占用系统资源不进行开释,须要定位到具体是哪个线程在占用,定位问题的步骤如下(linux 零碎):
1. 应用 top 命令常看以后服务器中所有过程(jps 命令能够查看以后服务器运行 java 过程), 找到以后 cpu 使用率最高的过程,获取到对应的 pid;
2. 而后应用 top -Hp pid,查看该过程中的各个线程信息的 cpu 应用,找到占用 cpu 高的线程 pid
3. 应用 jstack pid 打印它的线程信息,须要留神的是,通过 jstack 命令打印的线程号和通过 top -Hp 打印的线程号进制不一样,须要进行转换能力进行匹配,jstack 中的线程号为 16 进制,而 top -Hp 打印的是 10 进制。

当内存飙高个别都是堆中对象无奈回收造成,因为 java 中的对象大部分存储在堆内存中。其实也就是常见的 oom 问题(Out Of Memory),个别:
1.jinfo pid,能够查看以后进行虚拟机的相干信息列举进去
2.jstat -gc pid ms,多长毫秒打印一次 gc 信息,打印信息如下,外面蕴含 gc 测试,年老代 / 老年带 gc 信息等

  1. jmap -histo pid | head -20,查找以后过程堆中的对象信息,加上管道符前面的信息当前,代表查问对象数量最多的 20 个
  2. jmap -dump:format=b,file=xxx pid,能够生成堆信息的文件,然而这个命令不倡议在生产环境应用,因为当内存较大时,执行该命令会占用大量系统资源,甚至造成卡顿。倡议在我的项目启动时增加上面的命令,在产生 oom 时主动生成堆信息文件:-XX:+HeapDumpOnOutOfMemory。如果须要在线上进行堆信息剖析,如果以后服务存在多个节点,能够下线一个节点,生成堆信息,或者应用第三方工具,阿里的 arthas。

除此之外,咱们还能够应用 jvisualvm 是 jdk 自带的图形化剖析工具,能够对运行过程的线程,堆进行详细分析。然而这种剖析工具能够对本地代码或者测试环境进行监控剖析,不倡议在线上环境应用该工具,因为它会占用系统资源。如果必须要在线上执行,倡议以后服务存在多个节点,而后下线其中一个节点进行问题剖析。也能够应用第三方免费的图形剖析界面 jprofiler。

⚠️[注意事项]:
在日常 JVM 调优罕用参数次要如下:

  • 通用 GC 罕用参数:

    -Xmn:年老代大小

    -Xms:堆初始大小

    -Xmx:堆最大大小

    -Xss:栈大小

    -XX:+UseTlab:应用 tlab,默认关上,波及到对象调配问题

    -XX:+PrintTlab:打印 tlab 应用状况

    -XX:+TlabSize:设置 Tlab 大小

    -XX:+DisabledExplictGC:java 代码中的 System.gc()不再失效,避免代码中误写,导致频繁触动 GC,默认不起用。

    -XX:+PrintGC(+PrintGCDetails/+PrintGCTimeStamps) : 打印 GC 信息(打印 GC 详细信息 / 打印 GC 执行工夫)

    -XX:+PrintHeapAtGC 打印 GC 时的堆信息

    -XX:+PrintGCApplicationConcurrentTime: 打印应用程序的工夫

    -XX:+PrintGCApplicationStopedTime: 打印应用程序暂停工夫

    -XX:+PrintReferenceGC: 打印回收多少种援用类型的援用

    -verboss:class : 类加载具体过程

    -XX:+PrintVMOptions : 打印 JVM 运行参数

    -XX:+PrintFlagsFinal(+PrintFlagsInitial) -version | grep : 查找想要理解的命令

    -X:loggc:/opt/gc/log/path : 输入 gc 信息到文件

    -XX:MaxTenuringThreshold : 设置 gc 升到年龄,最大值为 15

  • Parallel GC 罕用参数:

    -XX:PreTenureSizeThreshold 多大的对象断定为大对象,间接降职老年代

    -XX:+ParallelGCThreads 用于并发垃圾回收的线程

    -XX:+UseAdaptiveSizePolicy 主动抉择各区比例

  • CMS GC 罕用参数:

    -XX:+UseConcMarkSweepGC : 应用 CMS 垃圾回收器

    -XX:parallelCMSThreads : CMS 线程数量

    -XX:CMSInitiatingOccupancyFraction : 占用多少比例的老年代时开始 CMS 回收,默认值 68%,如果频繁产生 serial old,适当调小该比例,升高 FGC 频率

    -XX:+UseCMSCompactAtFullCollection : 进行压缩整顿
    -XX:CMSFullGCBeforeCompaction : 多少次 FGC 当前进行压缩整顿

    -XX:+CMSClassUnloadingEnabled : 回收永恒代

    -XX:+CMSInitiatingPermOccupancyFraction : 达到什么比例时进行永恒代回收

    -XX:GCTimeTatio : 设置 GC 工夫占用程序运行工夫的百分比,该参数只能是尽量达到该百分比,不是必定达到

    -XX:MaxGCPauseMills : GCt 进展工夫,该参数也是尽量达到,而不是必定达到

  • G1 GC 罕用参数:

    -XX:+UseG1 : 应用 G1 垃圾回收器

    -XX:MaxGCPauseMills : GCt 进展工夫,该参数也是尽量达到,G1 会调整 yong 区的块数来达到这个值

    -XX:+G1HeapRegionSize : 分区大小,范畴为 1M~32M,必须是 2 的 n 次幂,size 越大,GC 回收距离越大,然而 GC 所用工夫越长

JVM 内存区域

在 Java 虚拟机中,JVM 内存区域次要分为线程公有、线程共享、间接内存三个区域,具体详情如下:

  • 线程公有(Theard Local Region): 数据区域生命周期与线程雷同, 依赖用户线程的启动 / 完结 而 创立 / 销毁(在 Hotspot VM 内, 每个线程都与操作系统的本地线程间接映射, 因而这部分内存区域的存 / 否追随本地线程的生 / 死对应)。
  • 线程共享(Theard Shared Region): 随虚拟机的启动 / 敞开而创立 / 销毁
  • 间接内存(Direct Memory) : 非 Java 虚拟机中 JVM 运行时数据区的一部分, 但也会被频繁的应用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 形式, 它能够应用 Native 函数库间接调配堆外内存, 而后应用 DirectByteBuffer 对象作为这块内存的援用进行操作(详见: Java I/O 扩大), 这样就防止了在 Java 堆和 Native 堆中来回复制数据, 因而在一些场景中能够显著进步性能

由此可见,在 Java 虚拟机 JVM 运行时数据区中,【程序计数器、虚拟机栈、本地办法区】属于线程公有区域,【JAVA 堆、办法区】属于线程共享区域,都须要 JVM GC 治理的,而间接内存不受 JVM GC 治理的。

首先,对于线程公有区域中的【程序计数器、虚拟机栈、本地办法区】, 次要详情如下:

  • 程序计数器:一块较小的内存空间, 是以后线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程公有”的内存。正在执行 java 办法的话,计数器记录的是虚拟机字节码指令的地址(以后指令的地址)。如果还是 Native 办法,则为空。这个内存区域是惟一一个在虚拟机中没有规定任何 OutOfMemoryError 状况的区域。
  • 虚拟机栈:是形容 java 办法执行的内存模型,每个办法在执行的同时都会创立一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动静链接、办法进口等信息。每一个办法从调用直至执行实现的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧(Frame)是用来存储数据和局部过程后果的数据结构,同时也被用来解决动静链接 (Dynamic Linking)、办法返回值和异样分派(Dispatch Exception)。栈帧随着办法调用而创立,随着办法完结而销毁——无论办法是失常实现还是异样实现(抛出了在办法内未被捕捉的异样)都算作办法完结。
  • 本地办法区:本地办法区和 Java Stack 作用相似, 区别是虚拟机栈为执行 Java 办法服务, 而本地办法栈则为 Native 办法服务, 如果一个 VM 实现应用 C -linkage 模型来反对 Native 调用, 那么该栈将会是一个 C 栈,但 HotSpot VM 间接就把本地办法栈和虚拟机栈合二为一。

其次,对于线程共享区域中的【JAVA 堆、办法区】, 次要详情如下:

  • Java 堆 (Java Heap): 是 Java 虚拟机 JVM 运行时数据区中,被线程共享的一块内存区域,创立的对象和数组都保留在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。因为古代 VM 采纳分代收集算法, 因而 Java 堆从 GC 的角度还能够细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区) 和老年代。
  • 办法区(Method Area)/ 永恒代(Permanent Generation):咱们常说的永恒代, 用于存储被 JVM 加载的类信息、常量、动态变量、即时编译器编译后的代码等数据. HotSpot VM 把 GC 分代收集扩大至办法区, 即应用 Java 堆的永恒代来实现办法区, 这样 HotSpot 的垃圾收集器就能够像治理 Java 堆一样治理这部分内存, 而不用为办法区开发专门的内存管理器(永恒带的内存回收的次要指标是针对常量池的回收和类型的卸载, 因而收益个别很小)。运行时常量池(Runtime Constant Pool)是办法区的一部分。Class 文件中除了有类的版本、字段、办法、接口等形容等信息外,还有一项信息是常量池(Constant Pool Table),用于寄存编译期生成的各种字面量和符号援用,这部分内容将在类加载后寄存到办法区的运行时常量池中。Java 虚拟机对 Class 文件的每一部分(天然也包含常量池)的格局都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

其中对于 Java 虚拟机 JVM 中的 Java 堆次要分为【新生代、老年代、永恒代、元数据区】:

  1. 新生代(Young Generation):用来寄存新生的对象。个别占据堆的 1 / 3 空间。因为频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
  2. 老年代(Old Generation):次要寄存应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前个别都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无奈找到足够大的间断空间调配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。MajorGC 采纳标记革除算法:首先扫描一次所有老年代,标记出存活的对象,而后回收没有标记的对象。MajorGC 的耗时比拟长,因为要扫描再回收。MajorGC 会产生内存碎片,为了缩小内存损耗,咱们个别须要进行合并或者标记进去不便下次间接调配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异样。
  3. 永恒代(Permanent Generation):指内存的永恒保留区域,次要寄存 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永恒区域,它和和寄存实例的区域不同,GC 不会在主程序运行期对永恒区域进行清理。所以这也导致了永恒代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异样。
  4. 元数据区(Metaspace): 在 Java8 中,永恒代曾经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的实质和永恒代相似,元空间与永恒代之间最大的区别在于:元空间并不在虚拟机中,而是应用本地内存。因而,默认状况下,元空间的大小仅受本地内存限度。类的元数据放入 native memory, 字符串池和类的动态变量放入 java 堆中,这样能够加载多少类的元数据就不再由 MaxPermSize 管制, 而由零碎的理论可用空间来管制。

Java 内存模型

你曾经晓得,导致可见性的起因是缓存,导致有序性的起因是编译优化,那解决可见性、有序性最间接的方法就是禁用缓存和编译优化,然而这样问题尽管解决了,咱们程序的性能可就堪忧了。

正当的计划应该是按需禁用缓存以及编译优化。那么,如何做到“按需禁用”呢?对于并发程序,何时禁用缓存以及编译优化只有程序员晓得,那所谓“按需禁用”其实就是指依照程序员的要求来禁用。所以,为了解决可见性和有序性问题,只须要提供给程序员按需禁用缓存和编译优化的办法即可。

Java 内存模型是个很简单的标准,能够从不同的视角来解读,站在咱们这些程序员的视角,实质上能够了解为,Java 内存模型标准了 JVM 如何提供按需禁用缓存和编译优化的办法。具体来说,这些办法包含 volatile、synchronized 和 final 三个关键字。

Java 的内存模型是并发编程畛域的一次重要翻新,之后 C++、C#、Golang 等高级语言都开始反对内存模型。Java 内存模型外面,最艰涩的局部就是 Happens-Before 规定,接下来咱们具体介绍一下。

Happens-Before 规定

在理解完 Java 内存模型之后,咱们再来具体学习一下针对于这些问题提出的 Happens-Before 规定。如何了解 Happens-Before 呢?如果顾名思义(很多网文也都爱按字面意思翻译成“后行产生”),那就背道而驰了,Happens-Before 并不是说后面一个操作产生在后续操作的后面,它真正要表白的是:后面一个操作的后果对后续操作是可见的。就像有心灵感应的两个人,尽管远隔千里,一个人心之所想,另一个人都看失去。Happens-Before 规定就是要保障线程之间的这种“心灵感应”。所以比拟正式的说法是:Happens-Before 束缚了编译器的优化行为,虽容许编译器优化,然而要求编译器优化后肯定恪守 Happens-Before 规定。

Happens-Before 规定应该是 Java 内存模型外面最艰涩的内容了,和程序员相干的规定一共有如下六项,都是对于可见性的,具体如下:

  1. 程序的程序性规定:指在一个线程中,依照程序程序,后面的操作 Happens-Before 于后续的任意操作。
  2. volatile 变量规定:指对一个 volatile 变量的写操作,Happens-Before 于后续对这个 volatile 变量的读操作。
  3. 传递性规定:指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
  4. 管程中锁的规定:指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。管程中的锁在 Java 里是隐式实现的,在进入同步块之前,会主动加锁,而在代码块执行完会主动开释锁,加锁以及开释锁都是编译器帮咱们实现的。
  5. 线程 start() 规定:对于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 可能看到主线程在启动子线程 B 前的操作。换句话说就是,如果线程 A 调用线程 B 的 start() 办法(即在线程 A 中启动线程 B),那么该 start() 操作 Happens-Before 于线程 B 中的任意操作。
  6. 线程 join() 规定:对于线程期待的。它是指主线程 A 期待子线程 B 实现(主线程 A 通过调用子线程 B 的 join() 办法实现),当子线程 B 实现后(主线程 A 中 join() 办法返回),主线程可能看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。换句话说就是,如果在线程 A 中,调用线程 B 的 join() 并胜利返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回。

在 Java 语言外面,Happens-Before 的语义实质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否产生在同一个线程里。例如 A 事件产生在线程 1 上,B 事件产生在线程 2 上,Happens-Before 规定保障线程 2 上也能看到 A 事件的产生。

Java 内存模型次要分为两局部,一部分面向你我这种编写并发程序的利用开发人员,另一部分是面向 JVM 的实现人员的,咱们能够重点关注前者,也就是和编写并发程序相干的局部,这部分内容的外围就是 Happens-Before 规定。

代码设计准则

对于一个开发人员来说,理解上述常识只是一个开始,更多的是咱们在理论工作中如何使用。集体感觉,理解一些设计准则,并把握这些设计准则,能力帮忙咱们写出高质量的代码。

当然,设计准则是代码设计时的一些经验总结。最大的一问题就就是:设计准则看起来比拟形象,其定义也比拟含糊,不同的人对于同一个设计准则都会有不同的感悟。如果,咱们只是单纯的形象记忆这些定义,对于咱们编程技术和代码设计的能力来说,并不会有什么实质性的帮忙。

针对于每一个设计准则,咱们须要把握它能帮忙咱们解决什么问题和能够适宜什么样的利用场景。能够这样说,设计准则是心法,设计模式是招式,而编程是实实在在的使用。常见的设计准则有:

  • 繁多职责准则 (Single Responsibility Principle, SRP 准则):一个类(Class) 和模块(Module) 只负责实现一个职责 (Principle) 或者性能(Funtion).
  • 开闭准则(Open Closed Principle, OCP 准则):软件实体,比方模块,类,办法等须要撑持 “ 对扩大开发,对批改敞开 ” 的准则。
  • 里氏代替准则(Liskov Substitution Principle, LSP 准则):子类对象可能代替程序中的父类对象呈现的任何中央,并且保障原有逻辑行为不变和正确性不被毁坏。
  • 接口隔离准则(Interface Segregation Principle, ISP 准则):接口调用方和使用者只关怀本人相干的,不必依赖于本人不须要的接口。
  • 依赖反转准则(Dependency Inversion Principle,DIP 准则):高模块不必依赖低模块,不必关注其细节,须要通过形象来相互依赖。
  • KISS 准则(Keep it Simple and Stupid Principle, KISS 准则):放弃代码可读和可保护的准则。
  • YAGNI 准则(You Ai Not Gonna Need It Principle,YAGNI 准则):防止适度设计的准则,不必去设计用不到的性能和不必去编写用不到的代码。
  • DRY 准则(Do Not Repeat Yourself Principle,DRY 准则):缩小编写反复的代码的准则,进步代码复用。
  • 迪米特准则(Law of Demeter Principle, LoD 准则):就是咱们常说的“高内聚,低耦合”的最佳参考准则,不应该存在间接依赖关系的类之间不要有依赖。

综上所述,后面五种准则就是咱们常说的 SOLID 准则,其余四种准则也是咱们最罕用的准则,这些设计准则都是咱们的编程方法论。

写在最初

Java 内存模型通过定义了一系列的 Happens-Before 操作,让应用程序开发者可能轻易地表白不同线程的操作之间的内存可见性。

在恪守 Java 内存模型的前提下,即时编译器以及底层体系架构可能调整内存拜访操作,以达到性能优化的成果。如果开发者没有正确地利用 Happens-Before 规定,那么将可能导致数据竞争。

Java 内存模型是通过内存屏障来禁止重排序的。对于即时编译器来说,内存屏障将限度它所能做的重排序优化。对于处理器来说,内存屏障会导致缓存的刷新操作。

在设计 Java 代码的时候,遵循一些必要的设计准则,也能更好地帮忙咱们写出好的代码,缩小内存开销,对于咱们自我晋升也有更好的帮忙。

版权申明:本文为博主原创文章,遵循相干版权协定,如若转载或者分享请附上原文出处链接和链接起源。

退出移动版