JVM内存模型、相关参数设置与命令查看

JVM内存模型,你看这一篇就够了 - 知乎 (zhihu.com)

Java虚拟机—Java8内存模型JVM(整理版) - 牧梦者 - 博客园 (cnblogs.com)

JVM入门——JVM内存结构 - 那股泥石流 - 博客园 (cnblogs.com)

JVM参数调优总结 -Xms -Xmx -Xmn -Xss_jakeswang的博客-CSDN博客_xms xmx

JVM的垃圾回收机制——垃圾回收算法 - 知乎 (zhihu.com)

元空间与直接内存的关系_Ethan_199402的博客-CSDN博客_元空间和直接内存有什么区别

jstat 参数详解_netboy_n的博客-CSDN博客_jstat参数分析

为什么设置-Xmx4g但是java进程内存占用达到8g?_斗者_2013的博客-CSDN博客_java占用多少内存

1、JVM简介

1.1 什么是JVM?

​ JVM是Java Virtual Machine(Java虚拟机)的简称,是一种用于计算设备的规范,是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。

1.2 JVM的优点:

  1. 一次编写,到处运行

​ JVM可以让java程序,一次编写,导出运行。让底层代码和运行环境分离开,编写好一份代码后,不用再次修改内容,只用通过安装不同的JVM环境自动进行转换即可运行,在各种系统中无缝连接。

  1. 自动内存管理,垃圾回收机制

​ 在Java诞生之时,C和C++称霸天下,但是这两种语言中没有内存管理机制,是通过手动操作来进行的管理,非常麻烦和繁琐。

​ 此时Java应运而生,为了处理内存管理这个方面,专门设计了垃圾回收机制,来自动进行内存的管理。极大的优化了操作,让程序员们不用正在噼里啪啦在码代的海洋中遨游时,还要操心内存会不会溢出这些“影响我方输出”的问题,顿时获得了成吨的好评。

  1. 数组下标越界检查

​ 在Java诞生之时,还有个让当时C和C++大佬头疼的问题是,数组下标越界是没有检查机制的,这还了得,又是一个影响“我方暴力输出”的罪魁祸首,因此JVM继续抱着暖男的思想,又来了个爱的抱抱。

​ JVM又一次看见了大佬们的烦恼,果断提供了数组下标越界的自动检查机制,在检测到数组下标出现越界后,会在运行时自动抛出“java.lang.ArrayIndexOutOfBoundsException”这个异常,在当时可是感动了很多业界大佬(我猜的)。

  1. 多态

​ JVM还有一个多态功能,是通过相同接口,不同的实例进行实现,完成不同的业务操作,比如:定义了一个动物接口(里面有一个吃的方法),我们就可以通过这个动物创造小猫(吃鱼),再创造一个狗狗(吃肉)。

1.3 JVM、JRE、JDK之间的关系

JVM

​ JVM是Java Virtual Machine的简称,是Java虚拟机,是一种模拟出来的虚拟计算机,它通过在不同的计算机环境当中模拟实现计算功能来实现的。引入Java虚拟机后,Java语言在不同平台上运行时就不需要重新编译。在其中,Java虚拟机屏蔽了与具体平台的相关信息,使得Java源程序在编译完成之后即可在不同的平台运行,达到“一次编译,到处运行”的目的,Java语言重要的特点之一跨平台,也即与平台的无关性,其关键点就是JVM

JRE

​ JRE是Java Runtime Environment的简称,是Java运行环境,是让操作系统运行Java应用程序的环境,其内部包含JVM,也就是说JRE只负责对已经存在的Java源程序进行运行的操作,它不包含开发工具JDK,对JDK内部的编译器、调试器和其它工具均不包含。

  • JRE包括JVM和基础类库

JDK

JDK是Java Development Kit的简称,是Java开发工具包,是整个Java程序开发的核心。其主要包含了JRE、Java的系统类库以及对Java程序进行编译以及运行的工具,例如:javac.exe和java.exe命令工具等。

  • JDK包括JVM、基础类库和编译工具

1.4 JVM的位置地位

  • java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行

  • Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统、硬件无关的关键

    • JVM的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器,JVM 通过移植接口在具体的平台和操作系统上实现
    • JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台
    • Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的跨平台

Java代码的执行过程:

  1. 源码编译:通过Java编译器将.java文件编译成.class字节码文件
  2. 类加载:通过ClassLoader及其子类来完成JVM的类加载
  3. 类执行:字节码(.class文件)被装入内存,进入JVM虚拟机,被解释器解释执行

1.5 JVM的常见实现

三种JVM:① Sun公司的HotSpot ② BEA公司的JRockit ③ IBM公司的J9 JVM

在JDK1.7及其以前我们所使用的都是Sun公司的HotSpot,但由于Sun公司和BEA公司都被oracle收购,jdk1.8将采用Sun公司的HotSpot和BEA公司的JRockit两个JVM中精华形成jdk1.8的JVM。

2、JVM体系结构

  1. Class Loader类加载器

​ 负责加载 .class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定。

① 定位和导入二进制class文件

② 验证导入类的正确性

为类分配初始化内存

④ 帮助解析符号引用.

  1. Native Interface本地接口

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为

native的代码,它的具体作法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见。

因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等。

  1. Execution Engine 执行引擎:执行包在装载类的方法中的指令,也就是方法。

  2. Runtime data area 运行数据区(即:虚拟机内存或者JVM内存 下节介绍)

    从整个计算机内存中开辟一块内存存储Jvm需要用到的对象,变量等,分为:方法区,堆,虚拟机栈,程序计数器,本地方法栈。

2.1 JVM内存结构:

1、程序计数器 PC Register

每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为**“线程私有”的内存**。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

2、本地方法栈 Native Method Stack

Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies

本地方法栈与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务

参数设置:

  1. -Xss 设置每个线程的栈大小。JDK1.5+ 每个线程栈大小为1M,一般来说如果栈不是很深的话,1M是绝对够用的啦。

参数含义解析:

  • 以-X开头的参数是和实现有关的,第一个s表示stack,第二个s表示size;

2、栈 JVM Stack(Java 虚拟机栈)

编译器可知的各种基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(引用指针,并非对象本身),都存储在栈中。

栈是java 方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

(局部变量表:存放了编译器可知的各种基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间)

栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的。

4、方法区 (元空间 Meta Space)—非堆内存

用于存储虚拟机加载的:静态变量+常量+类信息+运行时常量池 (类信息:类的版本、字段、方法、接口、构造函数等描述信息 )

对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

jdk8版本中则把永久代给完全删除了,取而代之的是MetaSpace,如图:

​ 运行时常量池和静态变量都存储到了堆中,MetaSpace存储类的元数据,MetaSpace直接在本地内存(主机的内存,而非分配给Java程序的内存)中(Native memory),这样类的元数据分配只受本地内存大小的限制,OOM问题就不存在了。

参数设置:

JDK1.8以前:

  1. -XX:PermSize设置永久代(方法区)最小空间大小;
  2. -XX:MaxPermSize设置永久代(方法区)最大空间大小;(默认最小值为16MB,最大值为64MB)
  3. -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论

JDK1.8及以后:

  1. -XX:MetaspaceSize:设置元空间的初始空间大小
  2. -XX:MaxMetaspaceSize:设置元空间的最大空间大小,,默认是不限制

注意:

  • -XX:PermSize=256m -XX:MaxPermSize=512m 这两个参数对于1.8就是过期的参数,JDK8没有这个参数设置。

  • 非堆内存不会被Java垃圾回收机制进行处理,在配置之前一定要慎重考虑下自身软件所需要的非堆区内存大小。

5、堆 Java Heap

所有的对象实例以及数组都要在堆上分配,此内存区域的唯一目的就是存放对象实例(即new出来的对象!及数组)。堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的,新生代可以有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

​ 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

堆是理解Java GC机制最重要的区域,没有之一!

JVM内存划分:

结构:新生代(Eden区+2个Survivor区)、 老年代 、 永久代(HotSpot有)

  • 新生代:新创建的对象——>Eden区

GC之后,存活的对象由Eden区 Survivor区0进入Survivor区1

再次GC,存活的对象由Eden区 Survivor区1进入Survivor区0

​ 其中,Eden用来存放JVM放分配的对象,两个Survivor的空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到年老代。Survivor增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。

  • 老年代:对象如果在新生代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到老年代。经过垃圾回收,年轻代中没有回收掉的对象会被Copy到年老代

如果新创建对象比较大(比如长字符串或大数组),新生代空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)

老年代的空间一般比新生代大,能存放更多的对象,在老年代上发生的GC次数也比年轻代少

  • 永久代:可以简单理解为方法区(本质上两者并不等价)

    用于存放Class、Method元信息,大小与项目的规模、类、方法的量有关。

如上文所说:对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”,本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已,对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了

Jdk1.6及之前:常量池分配在永久代

Jdk1.7:有,但已经逐步“去永久代”

Jdk1.8及之后:没有永久代(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)

参数设置:

  1. -Xms :设置堆的最小空间大小;通常为操作系统可用内存的1/64大小即可。
  2. -Xmx :设置堆的最大空间大小;通常为操作系统可用内存的1/4大小。
  3. -Xmn :设置新生代大小,是对-XX:newSize、-XX:MaxnewSize两个参数的同时配置,这个参数是在JDK1.4版本以后出现的;通常为Xmx的1/3或1/4。新生代 = Eden + 2个Survivor空间。实际可用空间 = Eden + 1个Survivor,即90%。
  4. -Xss: 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
  5. -XX:NewSize :设置新生代最小空间大小;
  6. -XX:MaxNewSize :设置新生代最大空间大小;
  7. -XX:NewRatio :新生代与老年代的比例,如-XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3。
  8. -XX:SurvivorRatio :新生代中 Eden 与 Survivor的比值。默认值为 8 。即Eden占新生代空间的8/10,另外两个Survivor各占1/10。

参数含义解析

  • 以-X开头的参数是和实现有关的,并不是适用于所有的参数;
  • 最开始只有 -Xms的参数,表示‘初始’ memory size,m表示memory,s表示size;
  • 紧接是参数 -Xmx,为了对齐三字符,压缩了其表示形式,采用计算机中约定表示方式:用 x 表示“”大“ (可以联想到衣服的号码大小,S、M、L、XL、XXL),因此 -Xmx中的m应当还是memory。既然有了最大内存的概念,那么一开始的 -Xms所表示的”初始“内存也就有了一个”最小“内存的概念(其实常用的做法中初始内存采用的也就是最小内存)。如果不对齐参数长度的话,其表示应当是-Xmsx。

注意:

​ 开发过程中,通常会将-Xms与-Xmx两个参数的配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源

6、直接内存 Direct Memor(堆外内存)

​ 直接内存(Direct Memory),也叫堆外内存,它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,而是Java虚拟机的堆以外的内存,直接受操作系统管理。比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。使用堆外内存有两个优势,一是减少了垃圾回收,二是提升复制速度,如NIO就是采用堆外内存。可以使用未公开的Unsafe和NIO包下ByteBuffer来创建堆外内存。

​ 在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

​ 显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

参数设置:

  • -XX:MaxDirectMemorySize:设置最大可用直接内存,如果Java虚拟机启动时未设置则默认为最大堆内存大小,即与 -Xmx相同。

    即假如最大堆内存为1G,则默认直接内存也为1G,那么JVM最大需要的内存大小为2G多一些。当直接内存达到最大限制时就会触发GC,如果回收失败则会引起OutOfMemoryError

JDK8 后的元数据区与直接内存的区别:

从上图可以看到,元空间和直接内存都是堆外内存,那么二者的区别是什么呢?

1. 元空间

元空间:JDK1.8以前的HotSpot JVM有方法区,也叫永久代(permanent generation)。(永久代是针对hotspot虚拟机来说的)

方法区:用于存放已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码

​ 方法区是一片连续的堆空间,通过-XX:MaxPermSize来设定永久代最大可分配空间,当JVM加载的类信息容量超过了这个值,会报OOM:PermGen错误。

​ JDK1.7开始了方法区的部分移除:符号引用(Symbols)移至native heap,字面量(interned strings)和静态变量(class statics)移至java heap。

为什么要用Metaspace替代永久代
随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。

默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小

​ 1)-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

​ 2)-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

​ 1)-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
​ 2)-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

​ -verbose参数是为了获取类型加载和卸载的信息

元空间的内存管理:

  • 在metaspace中,类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被回收。
  • 每个加载器有单独的存储空间。
  • 省掉了GC扫描及压缩的时间。
  • 当GC发现某个类加载器不再存活了,会把对应的空间整个回收。
2. 直接内存

直接内存:直接内存主要被 Java NIO 使用,某种程度上也就是指DirectByteBuffer对象占用的堆外内存。
DirectByteBuffer对象创建时会通过Unsafe类接口直接调用操作系统的malloc分配内存,然后将内存的起始地址和大小保存下来,据此就可以直接操作内存空间。

可以看出,直接内存的大小并不受到java堆大小的限制,甚至不受到JVM进程内存大小的限制。它只受限于本机总内存(RAM及SWAP区或者分页文件)大小以及处理器寻址空间的限制(最常见的就是32位/64位CPU的最大寻址空间限制不同)。

DirectByteBuffer使用直接内存的原因有两点:

1) 这块内存真正的分配并不在 Java 堆中,堆中只有一个很小的对象引用,这种方式能减轻 GC 压力;
2) 对于堆内对象,进行IO操作(Socket、文件读写)时需要先把对象复制一份到堆外内存再写入 Socket 或者文件,而当 DirectByteBuffer 就在堆外分配内存时可以省掉一次从堆内拷贝到堆外的操作,减少用户态到内核态的操作,性能表现会更好。

直接内存的回收

需注意堆外内存并不直接控制于JVM,这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,而 Young GC 的时候只会将年轻代里不可达的DirectByteBuffer对象及其直接内存回收,如果这些对象大部分都晋升到了年老代,那么只能等到Full GC的时候才能彻底地回收DirectByteBuffer对象及其关联的堆外内存。因此,堆外内存的回收依赖于 Full GC

Full GC一般发生在年老代垃圾回收或者代码调用System.gc的时候,依靠年老代垃圾回收触发 Full GC,进而实现堆外内存的回收显然具有太大的不确定性。如果年老代一直不进行垃圾回收,那么堆外内存就得不到回收,机器的物理内存可能就会被慢慢耗光。为了避免这种情况发生,可以通过参数-XX:MaxDirectMemorySize来指定最大的直接内存大小,当其使用达到了阈值的时候将调用System.gc来做一次Full GC,从而完成可控的堆外内存回收。这样做的问题在于,堆外内存的回收依赖于代码调用 System.gc,先捕获到异常,再在Catch块里面通过System.gc()命 令来触发垃圾收集。但如果Java虚拟机再打开了-XX:+DisableExplicitGC开关,禁止了人工触发垃圾 收集的话,根本不会触发Full GC,这样在使用Netty等 NIO 框架时需注意是否会因为这个参数导致直接内存的泄露

直接内存出现OutOfMemoryError的原因是对该区域进行内存分配时,其内存与其他内存加起来超过最大物理内存限制(包括物理的和操作系统级的限制),从而导致OutOfMemoryError。  另外,若我们通过参数“-XX:MaxDirectMemorySize”指定了直接内存的最大值,其超过指定的最大值时,也会抛出内存溢出异常。 
-XX:MaxDirectMemorySize 参数没有指定的话,那么根据directMemory = Runtime.getRuntime().maxMemory()最大直接内存的值和堆内存大小差不多,即与堆内存大小一致。

2.2 JVM的垃圾回收算法

​ 在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

GC 垃圾收集:

​ GC 垃圾收集(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存。不当的回收可能会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。

​ 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。

​ 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。

​ 程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行

如何判断对象是否可以被回收?

​ 垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死亡了,需要被回收。

一般有两种方法来判断:

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。

  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

​ 假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是“abc”,那么这个对象就应该回收。方法区(HotSpot虚拟机中的永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类。比如上述的“abc”就是属于废弃常量,那么哪些类是无用的类呢?

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

JVM 的垃圾回收算法?

1. 标记-清除算法(Mark-Sweep):标记无用对象,然后进行清除回收。

缺陷:

  1. 标记和清理的两个过程效率都不高;
  2. 容易产生内存碎片,碎片空间太多可能导致无法存放大对象。
  • 该算法分为两个阶段,标记和清除。标记阶段标记所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题就是内存碎片严重化,后续可能发生对象不能找到利用空间的问题。

2. 复制算法(Copy):按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉

缺陷:

  1. 内存使用率不高,只有原来的一半。
  • 按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。

3.标记-整理算法(Mark-compact):标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。有效的避免了内存碎片的产生

  • 标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

4. 分代算法(Generational Collection):根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法

  • 当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。jdk8以前分为三代:年轻代、老年代、永久代(方法区)。在jdk8以后取消了永久代的说法,而是元空间取而代之。

4.1 年轻代(复制算法为主)

​ 尽可能快的收集掉声明周期短的对象。整个年轻代占1/3的堆空间,年轻代分为三个区,Eden、Survivor-from、Survivor-to,其内存大小默认比例为8:1:1(可调整),大部分新创建的对象都是在Eden区创建。当回收时,先将Eden区存活对象复制到一个Survivor-from区,然后清空Eden区,存活的对象年龄+1;当这个Survivor-from区也存放满了时,则将Eden区和Survivor-from区存活对象复制到另一个Survivor-to区,然后清空Eden和这个Survivor-from区,存活的对象年龄+1;此时Survivor-from区是空的,然后将Survivor-from区和Survivor-to区交换,即保持Survivor-from区为空(此时的Survivor-from是原来的Survivor-to区), 如此往复。年轻代执行的GC是Minor GC。

​ 年轻代的迭代更新很快,大多数对象的存活时间都比较短,所以对GC的效率和性能要求较高,因此使用复制算法,同时这样划分为三个区域,保证了每次GC仅浪费10%的内存,内存利用率也有所提高。

4.2 老年代(标记-整理算法为主)

​ 在年轻代经过很多次垃圾回收之后仍然存活的对象(默认15岁),就会被放入老年代中,因为老年代中的对象大多数是存活的,所以使用算法是标记-整理算法。老年代执行的GC是Full GC。


具体过程:

新创建的对象首先分配在 eden 区
新生代空间不足时,触发Minor GC ,eden 区 和 from 区存活的对象使用 - copy 复制算法到 to 中,存活的对象年龄加一,然后交换 from to 的位置,空出大量连续内存区,放对象,反复。
minor gc 会引发 stop the world(SWT),暂停其他线程(垃圾回收时会涉及大量关于对象的复制,其他线程会产生不必要的麻烦,如线程不到对象了,因为地址变化了),等垃圾回收结束后,恢复用户线程运行
当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)----多次回收还存在,说明价值较高,放入老年代,不用频繁回收。
当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full gc ,STW停止的时间更长!

4.3 永久代/元空间

jdk8以前:

​ 永久代用于存放静态文件,如Java类、方法等。该区域回收与上述“方法区内存回收”一致。但是永久代是使用的堆内存,如果创建对象太多容易造成内存溢出OOM(OutOfMemory)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZrIiG1Qz-1647701158139)(…/AppData/Roaming/Typora/typora-user-images/image-20220319201843675.png)]

jdk8以后:

​ jdk8以后便取消了永久代的说法,而是用元空间代替,所存内容没有变化,只是存储的地址有所改变,元空间使用的是主机内存,而不是堆内存,元空间的大小限制受主机内存限制,这样有效的避免了创建大量对象时发生内存溢出的情况。

Minor GC和Full GC

之前多次提到Minor GC和Full GC,那么它们有什么区别呢?

  • Minor GC即新生代GC:发生在新生代的垃圾收集动作,因为Java有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • Major GC / Full GC:发生在老年代,经常会伴随至少一次Minor GC。Major GC的速度一般会比Minor GC慢倍以上。

Minor GC发生条件:

  • 当新对象生成,并且在Eden申请空间失败时;

Full GC发生条件:

  • 老年代空间不足
  • 永久带空间不足(jdk8以前)
  • System.gc()被显示调用
  • Minor GC晋升到老年代的平均大小大于老年代的剩余空间
  • 使用RMI来进行RPC或管理的JDK应用,每小时执行1次Full GC

常见的垃圾收集器(jdk8及以前)

一张图即可清除看到不同垃圾收集器之间的关系,连线表示可以配合使用。

  • Serial收集器(复制算法)

新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过 -XX:+UseSerialGC 来强制指定。

  • Serial Old收集器(标记-整理算法)

老年代单线程收集器,Serial收集器的老年代版本。

  • ParNew收集器(复制算法)

新生代收集器,Serial收集器的多线程版本,在多核CPU情况时表现更好。

  • Parallel Scavenge收集器(复制算法)

并行收集器,追求高吞吐量,高效利用CPU。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用 -XX:+UseParallelGC 来强制指定,用 -XX:ParallelGCThreads=2 来指定线程数。

  • Parallel Old收集器(复制算法) Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
  • CMS(Concurrent Mark Sweep)收集器(标记-清理算法) 高并发、低停顿,追求最短GC回收停顿时间(Stop The World),cpu占用比较高,响应时间快,停顿时间短,多核cpu追求高响应时间的选择,但是因为使用标记清理算法,容易产生内存碎片。
  • G1收集器

​ G1是一款面向服务端应用的垃圾收集器,支持并行与并发、分代收集、空间整合和可预测停顿的能力,即可适用于年轻代又可适用于老年代。

GC器的使用组合方式:

垃圾收集器GC的参数总结

  • -XX:+UseSerialGC:虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收
  • -XX:+UseParNewGC:打开此开关后,使用ParNew+Serial Old的收集器组合进行内存回收
  • -XX:+UseConcMarkSweepGC:打开此开关后,使用ParNew+CMS+Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用
  • -XX:+UseParallelGC:虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收
  • -XX:+UseParallelOldGC:打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收
  • -XX:+SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8,代表Eden:Survivor=8:1
  • -XX:+PretenureSizeThreshold:直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
  • -XX:+MaxTenuringThreshold:晋升到老年代的对象年龄,每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数时就进入老年代
  • -XX:+UseAdaptiveSizePolicy:动态调整Java堆中各个区域的大小以及进入老年代的年龄
  • -XX:+HandlePromotionFailure:是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况
  • -XX:+ParallelGCThreads:设置并行GC时进行内存回收的线程数
  • -XX:+GCTimeRatio:GC时间占总时间的比率,默认值为99,即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效
  • -XX:+MaxGCPauseMillis:设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效
  • -XX:+CMSInitingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效
  • -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用CMS收集器时生效
  • -XX:+CMSFullGCsBeforeCompaction:设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS收集器时生效

3、检查Java程序内存的相关命令介绍

3.1 jmap命令–查看JVM 内存分配

1)jmap -heap pid:输出堆内存设置和使用情况(JDK11使用jhsdb jmap --heap --pid pid)

2)jmap -histo pid:输出heap的直方图,包括类名,对象数量,对象占用大小

3)jmap -histo:live pid:同上,只输出存活对象信息

4)jmap -clstats pid:输出加载类信息

5)jmap -help:jmap命令帮助信息

# jmap -heap 进程号
(base) [mca@clu04 ~]$ jmap -heap 30454
Attaching to process ID 30454, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.301-b09using thread-local object allocation.
Parallel GC with 8 thread(s)# JVM设置
Heap Configuration:MinHeapFreeRatio         = 0MaxHeapFreeRatio         = 100MaxHeapSize              = 314572800 (300.0MB)NewSize                  = 104857600 (100.0MB)MaxNewSize               = 104857600 (100.0MB)OldSize                  = 209715200 (200.0MB)NewRatio                 = 2SurvivorRatio            = 8MetaspaceSize            = 21807104 (20.796875MB)CompressedClassSpaceSize = 1073741824 (1024.0MB)MaxMetaspaceSize         = 17592186044415 MBG1HeapRegionSize         = 0 (0.0MB)# 具体内存使用情况与GC使用
Heap Usage:
PS Young Generation         # 年轻代使用的GC器是PS
Eden Space:capacity = 62914560 (60.0MB)used     = 62287520 (59.402008056640625MB)free     = 627040 (0.597991943359375MB)99.0033467610677% used
From Space:capacity = 20971520 (20.0MB)used     = 0 (0.0MB)free     = 20971520 (20.0MB)0.0% used
To Space:capacity = 20447232 (19.5MB)used     = 0 (0.0MB)free     = 20447232 (19.5MB)0.0% used
PS Old Generation           # 老年代使用的GC器是PS capacity = 209715200 (200.0MB)used     = 209699552 (199.98507690429688MB)free     = 15648 (0.014923095703125MB)99.99253845214844% used14655 interned Strings occupying 1588976 bytes.

3.2 jstat命令–监视JVM各种堆和非堆内存大小和使用量

jstat 参数详解_netboy_n的博客-CSDN博客_jstat参数分析

1)jstat -class pid:输出加载类的数量及所占空间信息。

2)jstat -gc pid:输出gc信息,包括gc次数和时间,内存使用状况(可带时间和显示条目参数)

// 命令语法结构:
Usage: jstat -help|-optionsjstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]// 参数解释:
Options — 选项,我们一般使用 -gcutil 查看gc情况
vmid    — VM的进程号,即当前运行的java进程号
interval– 间隔时间,单位为秒或者毫秒
count   — 打印次数,如果缺省则打印无数次//示例
jstat -options
# 可以列出当前JVM版本支持的选项,常见的有class (类加载器) compiler (JIT) gc (GC堆状态) gccapacity (各区大小) gccause (最近一次GC统计和原因) gcnew (新区统计)gcnewcapacity (新区大小)gcold (老区统计)gcoldcapacity (老区大小)gcpermcapacity (永久区大小)gcutil (GC统计汇总)printcompilation (HotSpot编译统计)# 列名描述
# Loaded        装载的类的数量
# Bytes         装载类所占用的字节数
# Unloaded      卸载类的数量
# Bytes         卸载类的字节数
# Time          装载和卸载类所花费的时间(base) [mca@clu04 ~]$ jstat -class 30454
Loaded  Bytes  Unloaded  Bytes     Time
267859 540978.3    11697 21477.9    1758.07# 其中30454为进程号,1000是指每1秒(1000毫秒)输出一次
# 查看gc频率的命令(其中O代表老年代占用率,FGC是FullGC次数,FGCT是fullGC时间# S0  — Heap上的 Survivor space 0 区已使用空间的百分比
# S1  — Heap上的 Survivor space 1 区已使用空间的百分比
# E   — Heap上的 Eden space 区已使用空间的百分比
# O   — Heap上的 Old space 区已使用空间的百分比
# M   — Meta space 区已使用空间的百分比
# YGC — 从应用程序启动到采样时发生 Young GC 的次数
# YGCT– 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
# FGC — 从应用程序启动到采样时发生 Full GC 的次数
# FGCT– 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
# GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
(base) [mca@clu04 ~]$ jstat -gcutil 30454 1000 5S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   0.00   0.00  98.95  99.99  98.58  98.22     97    4.039  4787 2851.149 2855.1880.00   0.00  98.95  99.99  98.58  98.22     97    4.039  4787 2851.149 2855.1880.00   0.00  98.95  99.99  98.58  98.22     97    4.039  4787 2851.149 2855.1880.00   0.00  98.95  99.99  98.58  98.22     97    4.039  4787 2851.149 2855.1880.00   0.00  98.95  99.99  98.58  98.22     97    4.039  4787 2851.149 2855.188jstat -gcutil 5940 1000 5
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00  25.00  98.55  15.37  96.94  94.88     21    0.069     7    0.237    0.306
0.00  25.00  99.59  15.37  96.94  94.88     21    0.069     7    0.237    0.306
0.00  25.00  99.59  15.37  96.94  94.88     21    0.069     7    0.237    0.306
0.00  25.00 100.00  15.37  96.94  94.88     21    0.069     7    0.237    0.306
0.00  25.00 100.00  15.37  96.94  94.88     21    0.069     7    0.237    0.306

3.3 jinfo命令–查看指定pid的所有JVM信息

1)jinfo -flags pid 查询虚拟机运行参数信息。

2)jinfo -flag name pid,查询具体参数信息,如jinfo -flag UseSerialGC 42324,查看是否启用UseSerialGC

3.4 jstack-- 进程所包含线程情况查询

1)jstack pid 显示进程详情

3.5 通过NMT分析java进程的内存分配

为什么设置-Xmx4g但是java进程内存占用达到8g?_斗者_2013的博客-CSDN博客_java占用多少内存

JVM内存模型、相关参数设置与命令查看相关推荐

  1. IDEA JVM 性能优化 相关参数设置

    文章目录 IDEA JVM 性能优化 相关参数设置 IDEA JVM 性能优化 相关参数设置 点击 Help - Edit Custom VM Options -Xms256m -Xmx2048m - ...

  2. JVM 内存区域大小参数设置

    需要提前了解的知识点: 1. JVM内存模型 2. JVM垃圾回收算法 下图是JVM内存区域划分的逻辑图 从图中我们大概了解JVM相关的内存区域. JVM内存包括区域 Heap(堆区) New Gen ...

  3. jvm调优二:jvm内存模型剖析和参数设置

    简述 在开始学习java的时候,我们知道java是一个跨平台的语言,为什么java能够跨平台,主要是因为jvm屏蔽了操作系统底层的差异.下面重点来研究下jvm.本次研究的jvm都是jdk1.8的jvm ...

  4. 二、JVM内存模型及内存参数设置

    二.JVM内存模型 1.Java语言跨平台特性 java程序主要通过JVM来实现跨平台的,JVM编译器将Java源代码文件编译成字节码文件(一次编译,随处运行),然后不同的操作系统生成的机器码不同,但 ...

  5. 深入详解JVM内存模型与JVM参数详细配置

    本系列会持续更新. JVM基本是BAT面试必考的内容,今天我们先从JVM内存模型开启详解整个JVM系列,希望看完整个系列后,可以轻松通过BAT关于JVM的考核. BAT必考JVM系列专题 1.JVM内 ...

  6. 直通BAT必考题系列:深入详解JVM内存模型与JVM参数详细配置

    JVM基本是BAT面试必考的内容,今天我们先从JVM内存模型开启详解整个JVM系列,希望看完整个系列后,可以轻松通过BAT关于JVM的考核. BAT必考JVM系列专题 1.JVM内存模型 2.JVM垃 ...

  7. jvm 参数_一文带你深入了解JVM内存模型与JVM参数详细配置

    JVM基本是BAT面试必考的内容,今天我们先从JVM内存模型开启详解整个JVM系列,希望看完整个系列后,可以轻松通过BAT关于JVM的考核. 一.JVM内存结构 由上图可以清楚的看到JVM的内存空间分 ...

  8. Java编程题修院子,了解java虚拟机—JVM相关参数设置(2)

    1.   JVM相关参数设置 JVM相关配置 -XX:+PrintGC 两次次YoungGC,两次FullGC. -XX:+PrintGCDetails 打印GC时的内存,并且在程序结束时打印堆内存使 ...

  9. MySQL-DB参数、内存、I/O、安全等相关参数设置

    文章目录 生猛干货 官方文档 服务器参数介绍 MySQL获取信息配置领 MySQL配置参数的作用域 内存配置相关参数 I/O相关配置参数 Innodb I/O 相关的配置 MyISAM I/O 相关的 ...

最新文章

  1. 初学python还是swift-零基础如何选择编程语言 小白的我研究了3个月得出一个结论...
  2. TypeScript 2.1中的类型运算 一个递归的Readonly泛型
  3. IBASE的hierarchy结构
  4. ROS2学习(十一).ROS概念 - 命令行工具的使用
  5. python调用报表制作工具_使用Python轻松制作漂亮的表格
  6. 1024程序员节 | 我敢____,你敢么?Are you ready?
  7. ipython怎么安装_ipython的两种安装方式
  8. 物体检测 | 29篇顶会(刊)论文集
  9. @async 没有异步_javascript之异步函数
  10. 细说php一些常见的知识点
  11. 向服务器上传本地大文件的方法xshell
  12. spring的自动装配(default-autowire=byName)
  13. c++ primer plus 内存模型和名称空间
  14. U盘修复工具哪个好?7款U盘低格工具详解
  15. 敏感词过滤算法DFA
  16. Volley 源码解析(一)
  17. 国产系统-Ubuntu Kylin优麒麟图文(VIP典藏2022版)
  18. 什么是d.ts文件,它是不是typescript文件?
  19. Exception Triggered - Qt-Creator The inferior stopped because it triggered an exception.
  20. 华为交换机接口绑定mac案例

热门文章

  1. 和尚挑水安排(回溯问题)
  2. SCAU 2018新生赛 初出茅庐 全题解
  3. nag在逆向中是什么意思_CrackMe003-如何理解透VB逆向中的4C大法(图文+视频)-重在思维方法...
  4. 0002 c语言 字母排序
  5. 金士顿固态硬盘不认盘修复_上海金士顿固态硬盘维修 上海台电固态硬盘不识别数据恢复中心...
  6. oracle数据库期末上机题,oracle数据库期末考试试题及复习资料
  7. VUE项目学习系列博文
  8. 【论文阅读】Color Constancy by Learning to Predict Chromaticity from Luminance
  9. 中层管理者,你到底该管理什么?
  10. php ziparchive 损坏,通过ZipArchive php获取损坏或空拉链