1、JVM虚拟机常识

1.1 什么是Java虚拟机

所谓虚拟机,就是一台虚拟的计算机。他是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。大名鼎鼎的VisualBox、Mware就属于系统虚拟机。他们完全是对物理计算机的仿真。提供了一个可以运行完整操作系统的软件平台。

程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为java字节码指令。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。

1.2 Java如何做到跨平台

同一个JAVA程序(JAVA字节码的集合),通过JAVA虚拟机(IMM)运行于各大主流操作系统平台比如Windows、CentOs、ubuntu等。程序以虚拟机为中介,来实现跨平台。

JVM介绍 - 图1

1.3 虚拟机基本结构

我们要对MM虚拟机的结构有一个感性的认知。毕竟我们不是编程人员,认知程度达不到那么深入。

JVM介绍 - 图2

1. 类加载子系统

负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字量。

2. Java堆

在虚拟机启动的时候建立,它是Java程序最主要的内存工作区域。几乎所有的java对象实例都放Java堆中。堆空间是所有线程共享的,这是一块与Java应用密切相关的内存空间。

3. Java的NIO库(直接内存)

允许Java程序使用直接内存。直接内存是在Java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能考虑,读写频繁的场合考虑使用直接内存。由于直接内存在Java堆外,因此它的大小不会受限于Xmx指定的最大堆大小。但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

4. 垃圾回收系统

垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、Java堆和直接内存进行回收。

5. Java栈

每一个fava虚拟机线程都有一个私有的Java栈。一个线程的Java栈在线程创建的时候被创建。Java保存着帧信息,Java栈中保存着局部变量、方法参数,同时和Java方法的调用、返回密切相关。每一个偏袒虚拟机线程都有一个私有的虚拟机线程都有一个私有的栈.一个线程的栈在线程创建的时候被创建.JAVA保存着帧信息,JAVA栈中保存着局部变量、方法参数,同时和Java方法的调用、返回密切相关。

6. 本地方法

与ava栈非常类似,最大的不同在于Java栈用于Java方法的调用,而本地方法栈用于本地方法调用。作为Java虚拟机的重要扩展,Java虚拟机运行Java程序直接调用本地方法(通常使用C编写)。

7. PC寄存器

每个线程私有的空间,Java虚拟机会为每一个ava线程创建PC寄存器。在任意时刻,一个fava线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存的值就是undefined.

8. 执行引擎

是ava虚拟机最核心组件之一,它负责执行虚拟机的字节码。使用即时编译技术将方法编译成机器码后再执行。

1.4 虚拟机堆内存结构

JVM介绍 - 图3

JVM中堆空间可以分成三个大区,年轻代、老年代、永久代(方法区)。

1. 年轻代

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集那些生命周期短的对象。年轻代分为三个区域: EDEN、Survivor o(简称S0,也通常称为from区)、Survivor 1(简称S1,也通常称为to区)。其中So与$1的大小是相同等大的,三者所占年轻代的比例大致为8:1:1,$0与S1就像”孪生兄弟”一样,我们大家不必去纠结此比例(可以通过修改MM某些动态参数来调整)的大小.只需谨记三点就好:

1、S0与$1相同大小。

2、EDEN区远比S(S0+S1)区大,EDEN占了整个年轻代的大致70%至80%左右。

3、年轻代分为2个区(EDEN区、Survivor区)、3个板块(EDEN、SQ、S1)。

2. 老年代

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

那一个对象到底要经过多少次垃圾回收才能从年轻代进入老年代呢?

我们通常认矛在新生代中的对象,每经历过一次GC(垃圾回收);如果它没有被回收,它的年龄就会被加1,虚拟机提供了一个参数来可控制新生代对象的最大年龄:MaxTenuringThreshold。默认情况下,这个参数是15。也就是说,在新生代的对象最多经历15次GC,就可以进入老年代。

假如存在一种这样的情况,一个新生代对象,占用新生代空间特别大。在GC时若不回收,新生代空间将不足。但是若要回收,程序还没有使用完。此时就不会依据这个对象的MaxTenuringThreshold参数。而是直接晋升到老年代。所以说MaxTenuringThreshold参数是晋升老年代的充分非必要条件。

3. 永久代(方法区)

也通常被叫做方法区。是一块所有线程共享的内存区域。用于保存系统的类信息,比如类的字段、方法、常量池。

1.5 JVM虚拟机参数类型

1. 标准参数

标准参数中包括功能和输出的结果都是很稳定的,基本上不会随着IMM版本的变化而变化。可以用java -help检索出所有标准参数。

2. X 参数

非标准化的参数,在将来的版本中可能会改变。所有的这类参数都以-X开始,并且可以用java -X来检索。注意,不能保证所有参数都可以被检索出来.

3. XX 参数

非标准化的参数,它们同样不是标准的,随着MM版本的变化可能会发生变化。在实际情况中X参数和XX参数并没有什么不同。X参数的功能是十分稳定的,然而很多XX参数主要用于IMM调优和debug.

所有的XX参数都以”-XX:”开始,但是随后的语法不同,取决于参数的类型:

1、对于布尔类型的参数,我们有”+”或”-“,然后才设置JMM选项的实际名称。例如,-XX:+用于激活选项,而-XX:-用于注销选项。

Example:

开启GC日志的参数:-XX:+PrintGc

2、对于需要非布尔值的参数,如string或者integer,我们先写参数的名称,后面加上”=”,最后赋值。

例如:

-XX:MaxPermSize=2048m

1.6 常用的JVM参数

以上介绍完了lMM的三类参数类型,接下来我们主要聊聊常用的MM参数。

1. 跟踪JAVA虚拟机的垃圾回收

MM的GC的日志是以替换的方式(>)写入的,而不是追加(>>),如果下次写入到同一个文件中的话,以前的GC内容会被清空。这导致我们重启了JAVA服务后,历史的GC日志将会丢失。

  1. -XX:+PrintGc
  2. -XX:+PrintGcDetails
  3. -XX:+PrintGCTimestamps
  4. -X1oggc:filename

Example

此种写法,会导致JAVA服务重启后,GC日志丢失

  1. -XX:+PrintGcDetails -xX:+PrintGCTimestamps -x1oggc:/data0/1ogs/gc.log

在这里GC日志支持%p和%t两个参数:

  • %p将会被替换为对应的进程PID
  • %t将会被替代为时间字符串,格式为: YYYY-MM-DD_HH-MM-SS

此种写法,不管怎么重启,GC历史日志将不会丢失

  1. -xX:+PrintGcDetails -xX:+PrintccDatestamps -xloggc:/data0/1ogs/gc-%t.1og"

2. 配置JAVA虚拟机的堆空间

  1. -xms :初始堆大小
  2. -xmx:最大堆大小
  3. #实际生产环境中,我们通常将初始化堆(-Xms)和最大堆(-Xmx)设置为一样大。以避免程序频繁的申请堆空间。
  4. -Xmn: 设置年轻代大小,至于这个参数则是对–XX:newSize、-XX:MaxnewSize两个参数的同时配置,
  5. -XX:NewRatio= #设置年轻代和老年代的比值
  6. -xX:survivorRatio=eden/from=eden/to #年轻代中Eden区与两个survivor区的比值

Example:

  1. -xmn1m -xx:survivorRatio=2
  2. #这里的eden 于from(to)的比值为2:1,因此在新生代为1m的空间里, eden 区为 512KB,from和 to 分别为256KB.而新生代总大小为512KB + 256KB + 256KB = 1MB
  1. -xms20M -xmx20M -XX:NewRatio=2
  2. #这里老年代和新生代的比值为2:1,因此在堆大小为20MB的区间里,新生代大小为:20NB * 1/3 =6NB左右#老年代为13MB 左右。

3. 配置Java虚拟机的永久代(方法区)

  1. -XX:Permsize=n #设置持久代初始内存分配大小.
  2. -XX:MaxPermsize=n #设置持久代分配的内存的最大上限.

4. 配置Java虚拟机的栈

  1. -Xss128k # 设置每个线程的堆栈大小

1.7 常用垃圾回收算法

1. 引用计数法

引用针数法是最经典的一种垃圾回收算法。其实现很简单,对于一个A对象,只要有任何一个对象引用了A,则A的引用计算器就加1,当引用失效时,引用计数器减1.只要A的引用计数器值为O,则对象A就不可能再被使用。但是该算法却存在严重的问题:

1、无法处理循环引用的问题,因此在Java的垃圾回收器中,没有使用该算法。

由于引用计数器算法存在循环引用以及性能的问题,java虚拟机并未使用此算法作为垃圾回收算法。

1.2 标记清除法

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

缺陷:

①.效率问题: 标记清除过程效率都不高。

②.空间问题: 标记清除之后会产生大量的不连续的内存碎片

JVM介绍 - 图4

3. 标记压缩法

标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

标记-压缩算法的最终效果等同于标记-清除算法执行完成之后,再进行一次内存碎片的整理。基于此,这种算法也解决了内存碎片问题。

JVM介绍 - 图5

4. 复制算法

与标记-清除算法相比,复制算法是一种相对高效的回收方法。但不适用于存活对象较多的场合,如老年代。它将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

缺陷:

空间浪费,浪费了50%的内存空间。

JVM介绍 - 图6

我们前文介绍的MM的新生代,分为Eden及0和S1,其中S0和S1是两个容量相等的区域。其实在MM的垃圾回收中,S0和S1就使用了复制算法作为它们的垃圾回收算法。

5. 分代算法

介绍了复制、标记清除、标记压缩等垃圾回收算法。在所有的算法中,并没有一种算法可以完全取代其他算法,它们都具有自己独特的优势和特点。因此,根据垃圾回收对象的特性,使用合适的算法回收,才是明智的选择。分代算法就是基于这种思想,它将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。

一般来说,Java虚拟机会将所有的新建对象都放到称为新生代的区域中,大约90%的新建对象会被回收,因此新生代比较适合使用复制算法。当一个对象经过几次回收后依然存活,对象就会被放到称为老年代的内存空间。在老年代中,几乎所有对象都经过几次垃圾回收后依然得以存活的。因此可以认为对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。

在极端情况下,老年代对象的存活率可以达到100%。如果依然使用复制算法回收老年代,将需要复制大量对象。根据分代的思想,可以对老年代的回收使用与新生代不通的标记压缩或者标记清除算法,以提高垃圾回收效率。

JVM介绍 - 图7