JVM(一)--JVM的内存区域划分以及相关知识点

JVM(一)–JVM的内存区域划分以及相关知识点

一、JVM的内存区域是怎么划分的?

JVM结构图

在这里插入图片描述
JVM = 类加载器 + 执行引擎 + 运行时数据区

  • 类加载器(Class Loader):把硬盘上的class文件加载到JVM中的运行时数据区域,它不负责这个类文件是否能够执行
  • 执行引擎(Execution Engine):负责这个类文件是否能够执行。执行字节码,或者执行执行本地方法

运行时数据区:

JVM的内存划分中,有部分区域是线程私有的,有部分是属于整个JVM进程;有些区域会抛出OOM异常,有些则不会,了解JVM的内存区域划分以及特征,是定位线上内存问题的基础。那么JVM内存区域是怎么划分的呢?

在这里插入图片描述
1,程序计数器:

首先是程序计数器(Program Counter Register),在JVM规范中,每个线程都有自己的程序计数器。这是一块比较小的内存空间,存储当前线程正在执行的Java方法的JVM指令地址,即字节码的行号(它可以看作是当前线程所执行的字节码的行号指示器)。在JMM中,字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等急促功能都需要一来这个计数器来完成

异常情况:

如果正在执行Native方法,则这个计数器为空。该内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的内存区域。

为什么需要程序计数器?(为什么程序计数器是私有的?)

由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储

2. Java虚拟机栈:

Java虚拟机栈(Java Virtal Machine Stack),同样也是属于线程私有区域每个线程在创建的时候都会创建一个虚拟机栈,生命周期与线程一致,线程退出时,线程的虚拟机栈也回收。虚拟机栈内部保持一个个的栈帧(栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构(在之后的文章中会具体讲解)。这个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。),每次方法调用都会进行压栈,JVM对栈帧的操作只有出栈和压栈两种,方法调用结束时会进行出栈操作。

该区域存储着局部变量表,编译时期可知的各种基本类型数据、对象引用、方法出口等信息。

在这里插入图片描述

  • 局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型
  • 局部变量表所需要的内存空间在编译期间完成分配当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小因为在编译期间已经完成分配了。

异常情况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

3,本地方法栈:

本地方法栈(Native Method Stack)与虚拟机栈类似本地方法栈是在调用本地方法时使用的栈,每个线程都有一个本地方法栈

本地方法栈保存的是native方法信息,当一个jvm创建的线程调用native方法后,jvm不再为其在虚拟机栈中创建栈帧,jvm只是简单地动态连接并直接调用native方法。

本地方法栈与虚拟机栈的区别:

  • 虚拟机栈为虚拟机执行java方法服务(即字节码服务)
  • 本地方法栈则为虚拟机使用到的Native方法服务

异常情况:

与虚拟机栈类似,也会抛出:

将抛出StackOverflowError异常和OutOfMemoryError异常

4,java堆:

  • 堆(Heap),几乎所有创建的Java对象实例,都是被直接分配到堆上的
  • 堆被所有的线程所共享,在堆上的区域,会被垃圾回收器做进一步划分,例如新生代、老年代的划分。
  • 也就是java堆是垃圾收集器管理的主要区域,因此很多时候也被称为”GC堆”
  • Java虚拟机在启动的时候,可以使用“Xmx”之类的参数指定堆区域的大小。

异常情况:

如果堆中没有内存完成实例分配,并且堆已经无法再扩展时,将会抛出OutOfMemoryError异常。

5,方法区:

为什么叫方法区?简单的来说:方法区主要存class,class里面最重要的就是方法,所以称之为方法区。(里面的东西时很难发生变化的)

  • 方法区(Method Area)。方法区与堆一样,也是所有的线程所共享

  • 存储被虚拟机加载的元(Meta)数据,包括类信息、常量、静态变量、即时编译器编译后的代码等数据(也就是class元数据)

  • 里需要注意的是运行时常量池也在方法区中

  • 由于早期HotSpot JVM的实现,将GC分代收集拓展到了方法区,因此很多人会将方法区称为永久代。Oracle JDK8中已永久代移除永久代同时增加了元数据区(Metaspace)

  • 所以,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代名字一样”永久“存在。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载

在这里插入图片描述

异常情况:

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

6,运行时常量池:

  • 运行时常量池(Run-Time Constant Pool),这是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时,会抛出OutOfMemoryError异常。
  • Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池常量池存放编译器生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中存放
    • 字面量:文本字符串生命为final的常量值
    • 符号引用:类和接口的完全限定名,字段的名称与描述符,方法的名称与描述符(存放了与编译相关的一些常量,因为Java不像C++那样有连接的过程,因此字段方法这些符号引用在运行期就需要进行转换,以便得到真正的内存入口地址。
  • 运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,java语言并不要求敞亮一定只有编译器才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的敞亮放入池中,这种特性被开发人员用得比较多的便是String类的intern()方法

class文件中的常量池,也称为静态常量池,JVM虚拟机完成类装载操作后,会把静态常量池加载到内存中,存放在运行时常量池

异常情况:

运行时常量池(Run-Time Constant Pool),这是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时,会抛出OutOfMemoryError异常

7. 直接内存:

  • 直接内存(Direct Memory),直接内存并不属于Java规范规定的属于Java虚拟机运行时数据区的一部分
  • 在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作

异常情况:

直接内存并不会受到java堆大小的限制,但是既然是内存,还是会受本机总内存大小以及处理器寻址空间的限制。所以,当各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

下面这张图,反映了运行中的Java进程内存占用情况:

在这里插入图片描述

二、OOM可能发生在哪些区域上?

关于OOM的一个链接:
https://mp.weixin.qq.com/s/MGX_2JyY_DbsdjvPaaMKsw(JVM 发生 OOM 的 8 种原因、及解决办法)

根据javadoc的描述,OOM是指JVM的内存不够用了,同时垃圾收集器也无法提供更多的内存。从描述中可以看出,在JVM抛出OutOfMemoryError之前,垃圾收集器一般会出马先尝试回收内存。

从上面分析的Java数据区来看,除了**程序计数器不会发生OOM**外,哪些区域会发生OOM的情况呢?

第一,堆内存。堆内存不足是最常见的发送OOM的原因之一,如果在堆中没有内存完成对象实例的分配,并且堆无法再扩展时,将抛出OutOfMemoryError异常。当前主流的JVM可以通过**-Xmx和-Xms来控制堆内存的大小**,发生堆上OOM的可能是存在内存泄露,也可能是堆大小分配不合理

第二,Java虚拟机栈和本地方法栈,这两个区域的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务,在内存分配异常上是相同的。在JVM规范中,对Java虚拟机栈规定了两种异常:

  • 如果线程请求的栈大于所分配的栈大小,则抛出StackOverFlowError错误,比如进行了一个不会停止的递归调用;
  • 如果虚拟机栈是可以动态拓展的,拓展时无法申请到足够的内存,则抛出OutOfMemoryError错误。

第三,直接内存。直接内存虽然不是虚拟机运行时数据区的一部分,但既然是内存,就会受到物理内存的限制。在JDK1.4中引入的NIO使用Native函数库在堆外内存上直接分配内存,但直接内存不足时,也会导致OOM。

第四,方法区。随着Metaspace元数据区的引入,方法区的OOM错误信息也变成java.lang.OutOfMemoryError:Metaspace。

对于旧版本的Oracle JDK,由于永久代的大小有限,而JVM对永久代的垃圾回收并不积极,如果往永久代不断写入数据,例如String.Intern()的调用,在永久代占用太多空间导致内存不足,也会出现OOM的问题,对应的错误信为java.lang.OutOfMemoryError:PermGen space

在这里插入图片描述

三、谈谈你对OOM的认识:

主要有:

  • java.lang.StackOverflowError
  • java.lang.OutOfMemoryError:java heap space
  • java.lang.OutOfMemoryError:GC overhead limit exceeded
  • java.lang.OutOfMemoryError:Direct buffer memory
  • java.lang.OutOfMemoryError:unable to create new native thread
  • java.lnag.OutOfMemoryError:Metaspace

类图架构为:
在这里插入图片描述

1,java.lang.StackOverflowError:(是一种错误不是异常)

其组织架构为:

java.lang.Object
	java.lang.Throwable
		java.lang.Error//这里是Error,所以这个是一个错误不是异常
			java.lang.VirtualMachineError
				java.lang.StackOverflowError

从以上的结构中可以看出,这个java.lang.StackOverflowError不是一个异常而是一个错误

这个StackOverflowError:也就是递归调用方法以后,方法特别多把栈空间给撑爆了

2,java.lang.OutOfMemoryError:java heap space(是一种错误不是异常)

java.lang.OutOfMemoryError:java heap space:对象太多,堆给撑爆了。

这里牵涉到一个问题
堆大小30M,数组存在于堆中,25M的数组,为什么会报OOM?

因为堆中分老年代+年轻代(新生代10M(Eden 8M,Survior 1M),老年代 20M),所以这个25M放在新生代还是老年代都放不下的,所以会报OOM

3,java.lang.OutOfMemoryError:GC overhead limit exceeded:

GC回收时间过长时会抛出OutOfMemoryError过长的定义是:超过98%的时间用来做GC,并且回收了不到2%的堆内存,连续多次GC,都回收了不到2%的极端情况下才会抛出。假设不抛出GC overhead limit 错误会发生什么情况呢?那就是GC清理的这么点内存很快就会被再次填满,迫使GC再次执行,这样会形成恶性循环,cpu使用率一直是100%,而GC却没有任何成果。

在这里插入图片描述

好的应该是:GC前空间(较小)–>GC后空间(较大)

4,java.lang.OutOfMemoryError:Direct buffer memory:

导致原因写NIO程序经常会使用ByteBuffer来读取或者写入数据,这是一种基于通道(channe)与缓冲区(buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在java堆和Native堆中来回复制数据

ByteBuffer.allocate(capacity):第一种方式是分配了JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
ByteBuffer.allocateDirect(capacity):这是一种方式分配了os本地内存,不属于GC管辖范围,由于不需要内存拷贝,所以速度较快

但如果不断的分配本地内存,堆内存很少使用,则JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OOM,那程序就直接崩溃了。(==>最大的java本地内存,一般为本地内存的1/4

在这里插入图片描述
5,java.lang.OutOfMemoryError:unable to create new native thread:(这个问的很多)

(高并发请求服务器时经常出现如下异常 java.lang.OutOfMemoryError:unable to create new native thread,准确的说,该native thread异常与对应的平台有关)

导致原因:①你的应用创建了太多的线程了,一个应用线程创建多个线程,超过了系统承载极限了 ②你的服务器并不允许你的应用程序创建这么多个线程,linux系统默认允许单个线程可以创建的线程数是1024个,你的应用创建超过这数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread。

**解决方法:**①想办法降低你的应用程序创建线程的数量,分析应用是否真的需要创建这么多的线程,若不是,改代码将线程数降到最低 ②对于你的应用,确实需要创建很多的线程,远超过linux系统的默认值1024个线程的限制,可以通过修改linux服务器配置,扩大linux默认极限。

6,java.lnag.OutOfMemoryError:Metaspace:

使用java-XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:Metaspacesize为2181036B 大约为20.8M

java8及之后的版本使用Metaspace来替代永久代。Metaspace是方法区HotSpot的实现,它与持久化最大的不同就是Metaspace并不在虚拟机内存而是使用本地内存

四、你平时工作中用过的jvm常用基本配置参数有哪些?

常用参数:

  • -Xms:初始化大小内存,等价于-XX.InitialHeapSize
  • -Xmx:最大分配内存,等价于-XX.MxHeapSize
  • -Xss:设置单个线程栈的大小,等价于-XX.ThreadStackSize
  • -Xmn:设置年轻代的大小(一般为默认)
  • -XX:MetaspaceSize:设置元空间大小:元空间不再虚拟机中,而是使用本地内存(默认为20多兆,为了防止挤爆,有时需要调大一些,比如1024M)
  • -XX:+PrintGcDetails:输出详细GC收集日志信息
  • -XX:SurviorRation:设置新生代中eden和s0/s1空间的比例(默认,Eden:s0:s1=8:1:1(s0与s1相同))
  • -XX:NewRatio:配置年轻代与老年代在堆结构中的占比(NewRation可就是设置老年代的占比,剩下的给新生代)
  • -XX:MaxTenuringThreshold:设置垃圾最大年龄(比如15,则经过15次GC就晋升都老年区)(默认为1~15,一般为15)

五、你说说你做过的jvm调优和参数设置,请问如何盘点查看jvm系统默认值?

  • Xss:初始化栈空间
  • Xms:初始化堆空间
  • Xmx:堆的最大值
    (一般Xms和Xmx这两个最好调成一致)

JVM的参数类型:

  • 1,标配参数:
    • -version和-help:在jdk各个版本之间稳定,很少有大的变化
    • java -showversion
  • 2,X参数(了解):
    • -Xint:解释执行
    • -Xcomp:第一次使用就编译成本地代码
    • -Xmixed:混合模式(也就是先编译再执行)
  • 3,XX参数:
    • ①Boolean类型:

-XX:+/-某个属性值(+表示开启,-表示关闭)

  • ②KV设置类型(键值对):

公式:-XX:属性key=属性值value
比如:-XX:MetaspaceSize=128M
-XX:MaxTenuringThreshold=15

  • ③jinfo举例,如何查看当前运行程序的配置:

公式:jinfo-flag配置项 进程编号

如:jinfo-flag InitialHeapSize 8372(初始化堆大小)
jinfo-flags 5988(找出5988进程所有的能查出的信息)

  • ④题外话(坑提):

两个经典参数:-Xms和-Xmx

-Xms:等价于-XX:InitialHeapSize(初始化堆大小)
-Xmx:等价于-XX:MaxHeapSize(最大堆大小)

如何查看一个正在运行中的java程序,它的某个jvm参数是否开启?具体值是多少?

  • jps:查看java的后台进程
  • jinfo:查看正在运行中的java程序的各组信息

第一种:查看参数盘点家底:
jps:
jinfo -flag 具体参数 java进程编号
jinfo -flags 多个参数 java进程编号

第二种:查看参数盘点家底:

①java -XX:PrintFlagsInitial:(没有运行程序时所有初始化默认的参数)==>也就是出厂默认的参数
②-XX:PrintFlagsFinal:主要查看修改更新

公式:java -XX:+PrintFlagsFinal -version

③-XX:=PrintCommandLineFlags:打印命令行参数

在这里插入图片描述

下面附上对应的链接:

https://mp.weixin.qq.com/s/VEUcsNxJw80YDa_l8D2DfQ(关于OOM的)

感谢并参考:

java知音
https://blog.csdn.net/xiaojie_570/article/details/80094819

展开阅读全文
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值