vlambda博客
学习文章列表

java虚拟机的内存管理-垃圾回收

本文目的:根据实际的业务场景来调整JVM启动参数,以便更好的发挥服务器的硬件资源性能。

名词解释:

垃圾:在java虚拟机里面何判断一个对象是否已经成为垃圾对象一般是看他是否还在被其对象使用,如果他还在被其他对象使用,则说明他还有利用价值,也就是看是否有利用价值。但是这个算法有个漏洞,就是两个或者一群垃圾对象引用,一群垃圾彼此觉得对方有用。为了解决这个问题,我们一般采用可达性分析。就是从开始通过树形查找,看看是否可以查找到此对象。也就是说是不是垃圾的前提条件是站队正确,找对了派系,有个真正的大佬肯带你飞。这个是根本问题。背后有大佬,自己有价值,那成为垃圾的概率就很低。

JAVA:Java是一门面向对象的编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程。

虚拟机:虚拟机(Virtual Machine)指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。在实体计算机中能够完成的工作在虚拟机中都能够实现。在计算机中创建虚拟机时,需要将实体机的部分硬盘和内存容量作为虚拟机的硬盘和内存容量。每个虚拟机都有独立的CMOS、硬盘和操作系统,可以像使用实体机一样对虚拟机进行操作。

虚拟机分三大类:

1.系统虚拟机:Linux虚拟机、微软虚拟机、Mac虚拟机、BM虚拟机 、HP虚拟机、SWsoft虚拟机 、SUN虚拟机、Intel虚拟机、AMD虚拟机、BB虚拟机等等类型。

2.程序虚拟机:Java虚拟机(也称为: JVM) 等。

3. 操作系统层虚拟化:Docker容器。

JVM:JAVA虚拟机JVM就是在目标操作系统上运行的一个程序,这个程序运行时在目标操作系统上虚构出了一计算机,这台虚构的计算机可以运行java源程序编译后存放在.class文件里的java字节码程序

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言的虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

Java虚拟机它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能模拟来实现的。Java虚拟机有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。

Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。 Java虚拟机由五个部分组成:一组指令集、一组寄存器、一个栈、一个无用单元收集堆(Garbage-collected-heap)、一个方法区域。这五部分是Java虚拟机的逻辑成份,不依赖任何实现技术或组织方式,但它们的功能必须在真实机器上以某种方式实现。

内存: 内存(Memory)是 计算机 的重要部件,也称 内存储器 主存储器 ,它用于暂时存放CPU中的运算数据,以及与 硬盘 外部存储器 交换的数据。它是 外存 CPU 进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。只要计算机开始运行, 操作系统 就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来。
JVM内存模型 :JVM将内存区域分为Method Area(Non-Heap)(方法区)、Heap(堆)、ProgramCounter Register(程序计数器)、VM Stack(虚拟机栈,也有翻译为JAVA方法栈)、Native Method Stack(本地方法栈),其中Method Area与Heap是线程共享的,VM Stack、Native Method Stack、Program Counter Register是非线程共享的。
JVM初始运行的时候都会分配好Method Area(方法区)、Heap(堆),而JVM每遇到一个线程,就会为其分配一个Program Counter Register(程序计数器)、VM Stack 和 Native Stack,当线程终止时,三者(虚拟机栈、本地方法栈、程序计数器)所占用的内存空间也会被释放掉。非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说只发生在Heap上)的原因。
JMM :JMM是Java内存模型,与JVM内存模型是两回事,JMM的主要目标是定义程序中变量的访问规则,所有的共享变量都存储在主内存中共享,每个线程拥有自己的工作内存(相当于高速缓存,有利于提高访问速度),工作内存中保存的是主内存中变量的副本,线程对变量的读写操作是在自己的工作内存中进行,而不是直接读写主内存中的变量。如此多线程进行数据操作时,将可能发生线程安全问题,因此JMM需要提供原子性、可见性、有序性的保证。
今天不展开讨论JMM java内存模型及其相关的多线程安全问题。今天主要讨论下JVM的内存模型里面的堆区垃圾自动回收机制。

java虚拟机的内存分区鸟瞰图如下:


java虚拟机的内存管理-垃圾回收

java虚拟机的内存管理-垃圾回收

线程独享区

只有当前线程能访问数据的区域,线程之间不能共享线程独享区随线程的创建而创建,随线程的销毁而被回收

线程共享区

所有线程都可以访问的区域,当线程被销毁的时候,共享区的数据不会立即回收,需要等待达到垃圾回收的阈(yu)值之后才会进行回收。

为什么这样要划分区域?

随着对象数量的增加,JVM 内存使用率也在增加,如果 JVM 内存使用率达到 100%,则无法继续运行程序。为了让 JVM 内存可以被重复使用,我们需要进行垃圾回收。为了提高垃圾回收的效率,JVM 将内存区域进行了划分。


下面简要说明下这几个区域的大概内容。

虚拟机栈
  • 栈是线程私有的

  • 一个方法对应一个栈帧的入栈和出栈

  • 我们可以使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。【防止因为函数递归调用导致的栈溢出】

  • java虚拟机的内存管理-垃圾回收

  • java虚拟机的内存管理-垃圾回收


本地方法栈
  • Java虚拟机栈用于关联java方法的调用,而本地方法栈用于管理本地方法的调用

  • 本地方法栈也是线程私有的

  • 本地方法就是java中常见的native方法,使用C语言实现的

  • 它的具体做法是在本地方法栈中等级native方法,在执行引擎执行时加载本地方法库

  • 并不是所有虚拟机都支持native因为在jvm规范中没有明确要求

  • 在HotspotJVM中,直接将本地方法栈和虚拟机栈合二为一了。


程序计数器

  • PC寄存器用于存储向下一条指令的的地址,也是即将要执行的指令地址。
  • 是一块很小的内存空间,几乎可以忽略不记,也是运行速度最快的存储区域。
  • 在JVM规范中,每个线程都有自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致。
  • 任何一个时间一个线程都只有一个方法在执行,也就是所谓的当前方法,程序计数器会存储当前线程正在执行的java方法的JVM指令地址:如果是在执行native(本地方法),则是未指定。
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个计数器来完成。
  • 字节码解释器工作就是通过改变计数器的值来选取下一跳需要执行的字节码指令。
  • 它是唯一一个在java虚拟机规范中没有规定任何oom的区域。

元数据空间

元空间是方法区在HotSpot JVM 中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存理论上取决于32位/64位系统可虚拟的内存大小,可见也不是无限制的,需要配置参数。


最后剩下的就是这个堆区【垃圾回收工作就在这里展开】:

堆内存用途:存放的是程序运行时生成的各种对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。

堆内存分为年轻代(Young Generation)、老年代(Old Generation)

java虚拟机的内存管理-垃圾回收
java虚拟机的内存管理-垃圾回收

堆内存结构比例分析:Eden:[From:TO]  Tenured  Metaspace。我们拿 配置参数 -server -Xms6G -Xmx6G -Xmn2G 来说。默认情况 元空间不占用堆空间。所以堆空间由新生代和老年代分配。按照堆空间默认比例1:2 则新生代2GB,老年代4GB。新生代默认比例8:1:1 则Eden区1.6G,Survivor区0.4G【From区0.2G+To区0.2G】。

注:在Survivor区熬过一次GC,它们的年龄就加1,当对象年龄达到某个年龄(默认值为15)时,就会把它们移到老年代中。

新生代的垃圾回收称为YGC。老年代的垃圾回收称为FGC。


案例分析:


我们可以使用Linux自带的命令来查看运行中的JVM的内存堆情况:

jmap  -heap PIDxxx

拿手上电信项目的Mqtt Broker的JVM来看,现在大约设备连接数为5W台,我们看看JVM的堆区默认配置如下:

java虚拟机的内存管理-垃圾回收


我们看具体运行情况:


根据上面的数据可知:

当前JVM实例的最大可使用的堆空间为 4G。默认为服务器物理内存的1/4。我们服务器内存为16G的。

NewRatio:2 表示 新旧代比例默认1:2

SurvivorRatio:8 表示eden区和From,To默认比例为8:1:1

根据具体的JVM堆区运行图可见我们的堆内存还是很空闲的!年老代使用66%,不过,eden区压力稍微大一点点。应该是一次性新对象创建的太多太快的原因。后期可以优化。适当增大eden区大小。

不放心的话我们还可以使用另外一个命令来查看JVM的具体运行情况。

jstat -gcutil PIDxxx 5000 每隔5秒打印一次


S0区就是From区使用率 92.5%

S1区也就是To区使用率0%

Eden区使用率82.41%~82.81 25秒增长了4‰。

o区 老年代使用率66.79%。

M 元空间分配占用比:97.06% 动态分配,高效率使用。

CCS 压缩类分配占用比:92.92% 态分配,高效率使用

YGC:Minor GC 新生代垃圾回收次数 至启动以来累计回收6766次。

YGCT:Minor GC 收累计耗时 36.442秒。

FGC:FullGC 老年代回收次数4次。

FGCT:FullGC累计耗时1.534秒。

JVM的垃圾回收累计耗时:37.976秒。

结论就是:由于是默认配置,导致Eden区小了点,Eden区分配一旦过小,就导致了Minor GC次数稍微多了点。不过任然在合理范围之内,平均Minor GC时间为4毫秒。

要GC速度快,那空间必然小,次数就必然多,如果分配的空间大,GC速度就慢,那次数必然少。这是空间是时间的博弈。

但毕竟是Mqtt Broker的专门服务器。

我们可是适当加大Mqtt Broker JVM实例的堆空间大小,比如占用物理内存的一半:配置 -server -Xms9G -Xmx9G。这样算下来Eden区大小可以从1G左右调整到2.4G左右

到时候可以再观察下Minor GC的次数与平均耗时。