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虚拟机的逻辑成份,不依赖任何实现技术或组织方式,但它们的功能必须在真实机器上以某种方式实现。
线程独享区
线程共享区
随着对象数量的增加,JVM 内存使用率也在增加,如果 JVM 内存使用率达到 100%,则无法继续运行程序。为了让 JVM 内存可以被重复使用,我们需要进行垃圾回收。为了提高垃圾回收的效率,JVM 将内存区域进行了划分。
栈是线程私有的
一个方法对应一个栈帧的入栈和出栈
我们可以使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。【防止因为函数递归调用导致的栈溢出】
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)
堆内存结构比例分析: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的堆区默认配置如下:
我们看具体运行情况:
根据上面的数据可知:
当前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的次数与平均耗时。