JAVA基础之JVM+Tomcat

四、Tomcat 运维实战

1、JVM 虚拟机常识

两个常识问题

作为了解JVM 虚拟机的开始。我们很有必要弄明白以下两个问题。

1、什么是JAVA虚拟机
所谓虚拟机,就是一台虚拟的计算机。他是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。大名鼎鼎的VisualBox、VMware就属于系统虚拟机。他们完全是对物理计算机的仿真。提供了一个可以运行完整操作系统的软件平台。
程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都呗限制于虚拟机提供的资源中。
2、JAVA 如何做到跨平台
同一个JAVA程序(JAVA字节码的集合),通过JAVA虚拟机(JVM)运行于各大主流操作系统平台
比如Windows、CentOS、Ubuntu等。程序以虚拟机为中介,来实现跨平台。

JAVA基础之JVM+Tomcat_第1张图片

3、虚拟机基本结构

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

JAVA基础之JVM+Tomcat_第2张图片

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

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

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

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

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

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

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

8、执行引擎
是Java虚拟机最核心组件之一,它负责执行虚拟机的字节码。现代虚拟机为了提高执行效率。会使用即时编译技术将方法编译成机器码后再执行。
4、虚拟机堆内存结构

JAVA基础之JVM+Tomcat_第3张图片

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

1、年轻代
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分为三个区域:EDEN、Survivor0(简称S0,也通常称为from区)、Survivor1(简称S1,也通常称为to区)。其中S0与S1的大小是相同等大的,三者所占年轻代的比例大致为8:1:1,S0与S1就像"孪生兄弟"一样,我们大家不必去纠结此比例(可以通过修改JVM某些动态参数来调整)的大小.只需谨记三点就好:
1.S0与S1相同大小。
2.EDEN区远比S(S0+S1)区大,EDEN占了整个年轻代的大致70%至80%左右。 
3.年轻代分为2个区(EDEN区、Survivor区)、3个板块(EDEN、S0、S1)。
2、老年代
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
那一个对象到底要经过多少次垃圾回收才能从年轻代进入老年代呢?
我们通常认为在新生代中的对象,每经历过一次GC,如果它没有被回收,它的年龄就会被加1, 虚拟机提供了一个参数来可控制新生代对象的最大年龄:MaxTenuringThreshold。默认情况下,这个参数是15。 也就是说,在新生代的对象最多经历15次GC,就可以进入老年代。
假如存在一种这样的情况,一个新生代对象,占用新生代空间特别大。在GC时若不回收,新生代空间将不足。但是若要回收,程序还没有使用完。此时就不会依据这个对象的 MaxTenuringThreshold 参数。而是直接晋升到老年代。所以说
MaxTenuringThreshold 参数是晋升老年代的充分非必要条件。
3、永久代(方法区)
也通常被叫做方法区。是一块所有线程共享的内存区域。用于保存系统的类信息,比如类的字段、方法、常量池。
5、常用虚拟机参数

JVM 虚拟机提供了三种类型参数

1、标准参数
标准参数中包括功能和输出的参数都是很稳定的,很可能在将来的JVM版本中不会改变。你可以用 java 命令(或者是用 java -help)检索出所有标准参数。
2、X 类型参数
非标准化的参数,在将来的版本中可能会改变。所有的这类参数都以 -X 开始,并且可以用 java -X 来检索。
注意,不能保证所有参数都可以被检索出来,其中就没有 -Xcomp 。
3、XX 类型参数
非标准化的参数(到目前为止最多的),它们同样不是标准的,甚至很长一段时间内不被列出来。然而,在实际情况中 X 参数和 XX 参数并没有什么不同。X 参数的功能是十分稳定的,然而很多 XX 参数仍在实验当中(主要是 JVM 的开发者用于 debugging 和调优 JVM 自身的实现)。

用一句话来说明 XX 参数的语法。所有的 XX 参数都以"-XX:"开始,但是随后的语法不同,取决于参数的类型:
1)对于布尔类型的参数,我们有"+"或"-",然后才设置 JVM 选项的实际名称。
   例如,-XX:+ 用于激活选项,而 -XX:- 用于注销选项。
   Example:
   开启GC日志的参数: -XX:+PrintGC
2) 对于需要非布尔值的参数,如 string 或者 integer,我们先写参数的名称,后面加上"=",最后赋值。
   例如: -XX:MaxPermSize=2048m
6、常用的JVM参数

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

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

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

-XX:+PrintGC 
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

Example

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

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/data0/logs/gc.log

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

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

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

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/data0/logs/gc-%t.log"
2、配置JAVA虚拟机的堆空间
-Xms:初始堆大小
-Xmx:最大堆大小
实际生产环境中, 我们通常将初始化堆(-Xms) 和 最大堆(-Xmx) 设置为一样大。以避免程序频繁的申请堆空间。

-Xmn: 设置年轻代大小
-XX:NewRatio=老年代/新生代 //设置年轻代和年老代的比值
-XX:SurvivorRatio=eden/from=eden/to //年轻代中Eden区与两个Survivor区的比值

Example:

-Xmn1m -XX:SurvivorRatio=2
// 这里的eden 于from(to) 的比值为2:1 ,因此在新生代为1m的区间里, eden 区为 512KB, from 和 to 分别为 256KB. 而新生代总大小为 512KB + 256KB + 256KB = 1MB
-Xms20M -Xmx20M -XX:NewRatio=2
// 这里 老年代和新生代的比值为2:1 , 因此在堆大小为20MB的区间里, 新生代大小为: 20MB * 1/3 = 6MB左右
// 老年代为 13MB 左右。
3、配置JAVA虚拟机的永久区(方法区)
-XX:PermSize=n    //设置初始化值
-XX:MaxPermSize=n //设置持久代大小
4、配置JAVA虚拟机的栈
-Xss128k 设置每个线程的堆栈大小
一个常用的JAVA服务启动参数:
      nohup /usr/java/$SERVER/bin/java
      -Dspring.cloud.config.profile=$ENV  -jar /app/$SERVER/$SERVER.jar --log.path=/log/$SERVER/all/ --log.level=info  2>&1  >/dev/null &

-Xmx:最大堆大小
-Xms:初始堆大小
-Xmn: 设置年轻代大小
-XX:MetaspaceSize  初始空间大小,达到该值就会触发垃圾收集进行类型卸载(metadata元数据)
-XX:MaxMetaspaceSize  最大空间,默认是没有限制的
7、常用垃圾回收算法
1、引用计数法
引用计数法是最经典的一种垃圾回收算法。其实现很简单,对于一个A对象,只要有任何一个对象引用了A,则A的引用计算器就加1,当引用失效时,引用计数器减1.只要A的引用计数器值为0,则对象A就不可能再被使用。
       
虽然其思想实现都很简单(为每一个对象配备一个整型的计数器),但是该算法却存在两个严重的问题:
1)无法处理循环引用的问题,因此在Java的垃圾回收器中,没有使用该算法。
2)引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

一个简单的循环引用问题描述:
对象A和对象B,对象A中含有对象B的引用,对象B中含有对象A的引用。此时对象A和B的引用计数器都不为0,但是系统中却不存在任何第三个对象引用A和B。也就是说A和B是应该被回收的垃圾对象,但由于垃圾对象间的互相引用使得垃圾回收器无法识别,从而引起内存泄漏(由于某种原因不能回收垃圾对象占用的内存空间)。

如下图:不可达对象出现循环引用,它的引用计数器不为0

JAVA基础之JVM+Tomcat_第4张图片

注意:由于引用计数器算法存在循环引用以及性能的问题,java虚拟机并未使用此算法作为垃圾回收算法。
【可达对象】通过根对象的进行引用搜索,最终可以到达的对象。
【不可达对象】通过根对象进行引用搜索,最终没有被引用到的对象。
2、标记清除法
标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
缺陷:
①.效率问题:标记清除过程效率都不高。 
②.空间问题:标记清除之后会产生大量的不连续的内存碎片

JAVA基础之JVM+Tomcat_第5张图片

3、 标记压缩法
标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成之后,再进行一次内存碎片的整理。基于此,这种算法也解决了内存碎片问题。

JAVA基础之JVM+Tomcat_第6张图片

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

JAVA基础之JVM+Tomcat_第7张图片

我们前文介绍的JVM 的新生代,分为Eden 及 S0 和 S1 , 其中S0 和 S1 是两个容量相等的区域。其实在JVM 的垃圾回收中, S0 和 S1 就使用了复制算法作为它们的垃圾回收算法。
# 使用JDK 自带的jmap 工具,打印一个JVM 虚拟机的堆信息. 
# 其中 187136 为JVM 的PID

[root@java00 ~]# jmap -heap 187136
Attaching to process ID 187136, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 24.80-b11

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 4294967296 (4096.0MB)
   NewSize          = 1431633920 (1365.3125MB)
   MaxNewSize       = 1431633920 (1365.3125MB)
   OldSize          = 2863267840 (2730.625MB)
   NewRatio         = 2
   SurvivorRatio    = 8
   PermSize         = 1073741824 (1024.0MB)
   MaxPermSize      = 2147483648 (2048.0MB)
   G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 1288503296 (1228.8125MB)
   used     = 570069688 (543.6608200073242MB)
   free     = 718433608 (685.1516799926758MB)
   44.242780734027704% used
Eden Space:
   capacity = 1145372672 (1092.3125MB)
   used     = 561190696 (535.1931533813477MB)
   free     = 584181976 (557.1193466186523MB)
   48.99634064256773% used
From Space:
   capacity = 143130624 (136.5MB)
   used     = 8878992 (8.467666625976562MB)
   free     = 134251632 (128.03233337402344MB)
   6.2034187736092035% used
To Space:
   capacity = 143130624 (136.5MB)
   used     = 0 (0.0MB)
   free     = 143130624 (136.5MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 2863333376 (2730.6875MB)
   used     = 46529976 (44.37444305419922MB)
   free     = 2816803400 (2686.313056945801MB)
   1.6250282412102892% used
Perm Generation:
   capacity = 1073741824 (1024.0MB)
   used     = 47940496 (45.71961975097656MB)
   free     = 1025801328 (978.2803802490234MB)
   4.464806616306305% used

20796 interned Strings occupying 2411488 bytes.
# 截取部分信息如下: From(S0) 和 To(S1) 区间采用了复制算法,不论什么时候执行jmap 区间,都会发现
# 有一个区间的使用用于都是 0.0% used
From Space:
   capacity = 143130624 (136.5MB)
   used     = 8878992 (8.467666625976562MB)
   free     = 134251632 (128.03233337402344MB)
   6.2034187736092035% used
To Space:
   capacity = 143130624 (136.5MB)
  

你可能感兴趣的:(JAVA基础)