Java篇--JVM二(内存结构)

文章目录

        • 一、运行时数据区(jdk1.7)
          • 1.前言:
          • 2.详细介绍:
            • (1)Java堆(Heap):
            • (2)方法区(Method Area):
            • (3)程序计数器(PC Register或者Program Counter Register):
            • (4)JVM栈(JVM Stacks):
            • (5)本地方法栈(Native Method Stacks):
            • (6)运行时常量池(Runtime Constant Pool):
            • (7)直接内存:
          • 3.相互之间的联系:
          • 4.参数来控制各区域的内存大小:
          • 5.常见内存溢出错误:
        • 二、jdk1.7和jdk1.8的区别
          • 1.前言:
          • 2.jdk1.8元空间讲解:
        • 三、扩展:通过jstat命令进行查看堆内存使用情况
          • 1.查看class加载统计:
          • 2.查看编译统计:
          • 3.垃圾回收统计:

一、运行时数据区(jdk1.7)

1.前言:

  Java开发人员可能会遇到这样的困惑:我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?
  了解JVM内存也是为了服务器出现性能问题的时候可以快速的了解哪块的内存区域出现问题,以便于快速的解决生产故障。Java 虚拟机是中、高级开发人员必须修炼的知识,有着较高的学习门槛,很多人都不情愿去接触它。可能是觉得学习成本较高又或者是感觉没什么实用性,所以干脆懒得“搭理”它了。其实这种想法是错误的。

  方法区和堆是所有执行文件所共享的区域,即一个静态变量被创建之后可以被另一个线程所引用
  而每一个线程都有自己的其他区域,即线程栈、程序计数器和本地方法栈,线程结束后即被销毁

  • 堆(Heap):线程共享。几乎所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
  • 方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。
  • 方法栈(JVM Stack):线程私有。每个线程产生都会有一个独有的线程栈(在栈上分的一小块内存),栈的主要作用是为程序运行提供场所,即程序的执行操作在栈中进行,存储局部变量表、操作栈、动态链接、方法出口,对象指针。
  • 本地方法栈(Native Method Stack):线程私有。存放一些执行本地方法的地址指针,为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
  • 程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器,指向下一条要执行的指令。记录程序运行的位置,如果被打断之后可以接着打断的地方接着运行。

注:Native Interface本地接口:本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通知垃圾收集器进行垃圾回收的代码 System.gc(),就是使用 native 修饰的,还有比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。

2.详细介绍:

Java篇--JVM二(内存结构)_第1张图片
Java篇--JVM二(内存结构)_第2张图片

(1)Java堆(Heap):

  对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
  Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代,空间大小为1:2;再细致一点的有Eden空间、From Survivor空间、To Survivor空间,比例为8:1:1;以上两个比例皆可以修改。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。
  根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
  如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  对象在被创建后放入Eden区中,当Eden区放满之后,会进行一次minor gc,通过可达性算法分析,将垃圾对象进行回收,然后将不是垃圾的对象放入survivor1区中,经过程序执行之后,如果eden区再次放满,则会将Eden区和survivor1区中的对象进行垃圾回收,将仍然存活的对象放入survivor2区中,即每次进行垃圾回收后的对象都放在另一个survivor区中,直到对象的分带年龄达到设定值之后(默认为15,因为对象中的分代年龄指针占四个字节,最大值为15),放入老年代,其中,对象每经历一次minor gc,分代年龄就会加一。老年代经过多次minor gc后,也会放满,如果老年代放满,程序会进行full gc,即,程序会进行STW(stop the world),即终止程序运行,然后通过可达性分析算法,进行一次full gc。如果对象仍然存活,则继续存放在老年代中,如果老年代的对象full gc之后仍然满了,则会出现内存溢出的错误情况。

(2)方法区(Method Area):

  方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存放静态变量、字符串常量池、对象常量池、类元信息等。静态变量:文件在加载过程中,在准备阶段对一些静态变量进行分配空间,并赋予默认值,在初始化阶段再进行赋初始值。常量池:存放相应信息。类元信息:文件在加载过程中,在加载阶段,文件会编译为字节码文件,其中,相应的类中的方法、变量等相应的信息为类元信息。静态变量是对象类型,则存放指向该对象在堆中的地址。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
  对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。在Java8中永生代彻底消失了。
  Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
  根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

(3)程序计数器(PC Register或者Program Counter Register):

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

(4)JVM栈(JVM Stacks):

  与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:栈是程序运行的主要场所,每有一个线程运行时,jvm就会给这个线程在栈上分配一块独立的区域(线程栈)去存放自己的局部变量,线程栈的主要由栈帧组成,每个方法都有一个独立的栈帧,栈帧上主要有:方法出口、动态链接、操作数栈、局部变量表组成。比如执行main方法,则main方法有自己独立的栈和本地方法栈以及程序计数器,根据程序计数器,来进行记录执行的位置,通过字节码执行引擎进行执行,执行main方法时,会给main方法生成一个独立的栈帧,栈帧中本地变量区域放入方法中生成的局部变量,对局部变量进行操作时,将局部变量放入操作数栈中(局部变量做操作时用来中转存放的内存空间),当执行操作时,将操作数栈中的数据顺序出栈,放入外部寄存器中,用运算器进行运算,运算出来结果之后再把值放入操作数栈中,再进行赋值给本地变量。动态链接:程序在加载时,在解析阶段只会进行静态链接(将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用),即只解析静态方法,当程序运行到静态方法中的普通方法时,会进行动态链接。因为程序在加载阶段会把编译为一条条指令文件,然后存放在元空间的常量池中,所以在执行动态链接时,便是找到该方法的在元空间常量池的地址。方法出口:一个方法运行完成之后返回到上层调用方法的哪一个位置。如果方法中引用对象信息,则对应局部变量表中保存的是堆中对象的地址指针。
  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
  其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

(5)本地方法栈(Native Method Stacks):

  也是线程私有的,本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

(6)运行时常量池(Runtime Constant Pool):

  运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
  运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。
  既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常。

(7)直接内存:

  直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
  在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。
  显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
  直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存。

3.相互之间的联系:

Java篇--JVM二(内存结构)_第3张图片
  通过类装载子系统进行装载类,然后通过字节码执行引擎进行执行,执行main()方法时,在栈中划分一小部分空间生成线程栈,在线程栈中为main方法生成独立栈帧,如果类中有其他方法,则生成一个新的栈帧,在栈帧中的局部变量表中存放执行过程中的局部变量,在操作数栈中进行对操作数进栈以及出栈,然后在外部执行运算,在赋值给本地变量,其中执行步骤都是由该线程独有的程序计数器计数执行位置供字节码执行引擎执行。如果栈帧中局部变量为对象类型,则局部变量表中存放的是堆中对象的指针地址。该对象头中存在指针,指向其所属的类,也就是指向方法区中该类的地址。当一个类加载到jvm中会产生类对象,即在堆中存放的class对象,方法区中的类元信息会再次指向堆中的class类对象。如果方法区(元空间)中静态变量是对象类型,则方法区(元空间)的常量池中存放指向该对象在堆中的地址。

4.参数来控制各区域的内存大小:

下面这张图能很清晰的说明JVM内存结构的布局和相应的控制参数:
Java篇--JVM二(内存结构)_第4张图片
堆控制参数:
老年代空间大小=堆空间大小-年轻代大空间大小。
没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。
-Xms设置堆的最小空间大小(最小堆内存 64m)。
-Xmx设置堆的最大空间大小(最大堆内存 128m)。
注:建议将-Xms和-Xmx设为相同值,避免每次垃圾回收完成后JVM重新分配内存。
-XX:NewSize设置新生代最小空间大小(新生代初始化大小为30m)。
-XX:MaxNewSize设置新生代最大空间大小(新生代最大大小为40m)。

方法区控制参数:
-XX:PermSize 设置最小空间
-XX:MaxPermSize 设置最大空间。

方法栈控制参数:
-Xss控制每个线程栈的大小(-Xss=256k)。
注:在Sun JDK中本地方法栈和方法栈是同一个,因此也可以用-Xss控制每个线程的大小。

5.常见内存溢出错误:

  有了对内存结构清晰的认识,就可以帮助我们理解不同的OutOfMemoryErrors,下面列举一些比较常见的内存溢出错误,通过查看冒号“:”后面的提示信息,基本上就能断定是JVM运行时数据的哪个区域出现了问题。关于OutOfMemoryError的更多信息可以查看:https://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/memleaks.html

Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space
原因:对象不能被分配到堆内存中。

Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space
原因:类或者方法不能被加载到老年代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库。

Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
原因:创建的数组大于堆内存的空间。

Exception in thread “main”: java.lang.OutOfMemoryError: request  bytes for . Out of swap space?
原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。

Exception in thread “main”: java.lang.OutOfMemoryError:  (Native method)
原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现。

参考:
https://segmentfault.com/a/1190000023248578
http://www.blogjava.net/nkjava/archive/2012/03/14/371831.html
https://zhuanlan.zhihu.com/p/38348646
https://www.cnblogs.com/ityouknow/p/5610232.html
https://www.cnblogs.com/hexinwei1/p/9406239.html
https://blog.csdn.net/rongtaoup/article/details/89142396
 

二、jdk1.7和jdk1.8的区别

1.前言:

  三种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。官网给出的解释:This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
  JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
  JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。当然方法区和元空间存储内容是一样的。

Jdk1.6及之前:常量池分配在永久代
Jdk1.7:有,但已经逐步“去永久代”
Jdk1.8及之后:没有永久代(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)

2.jdk1.8元空间讲解:

  JDK1.8时,移除了方法区的概念,用一个元数据区代替。元数据区存放的东西和方法区相同,不过元数据区移动到本地内存中。本地内存就是指机器内存中不是JVM管理的那部分内存,由操作系统管理。元数据区移动到本地内存以后,可以避免虚拟机加载类过多而引发的内存溢出:java.lang.OutOfMemoryError: PermGen,但是同样不能无限扩展。
Java篇--JVM二(内存结构)_第5张图片
参考:
https://blog.csdn.net/weixin_41987908/article/details/103904829
https://blog.csdn.net/lovely_girl1126/article/details/106806879
https://blog.csdn.net/qq_43721032/article/details/109999341
https://blog.csdn.net/beishanyingluo/article/details/104972934
 

三、扩展:通过jstat命令进行查看堆内存使用情况

  jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]

1.查看class加载统计:
[root@VM_0_8_centos bin]# jps
29060 Jps
28519 Bootstrap
[root@VM_0_8_centos bin]# jstat -class 28519
Loaded  Bytes  Unloaded  Bytes     Time   
  3270  7119.1        0     0.0     137.50

说明:

  • Loaded:加载class的数量
  • Bytes:所占用空间大小
  • Unloaded:未加载数量
  • Bytes:未加载占用空间
  • Time:时间
2.查看编译统计:
[root@VM_0_8_centos bin]# jstat -compiler 28519
Compiled Failed Invalid   Time   FailedType FailedMethod
    2403      1       0     5.13          1 com/sun/org/apache/xerces/internal/parsers/AbstractDOMParser startElement

说明:

  • Compiled:编译数量。
  • Failed:失败数量
  • Invalid:不可用数量
  • Time:时间
  • FailedType:失败类型
  • FailedMethod:失败的方法
3.垃圾回收统计:
[root@VM_0_8_centos bin]# jstat -gc 28519
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
1280.0 1280.0  0.0   600.5  10688.0   1714.6   26432.0    17306.1   23808.0 23249.7 2560.0 2362.5     17    0.093   1      0.064    0.157

说明:

  • S0C:第一个Survivor区的大小(KB)
  • S1C:第二个Survivor区的大小(KB)
  • S0U:第一个Survivor区的使用大小(KB)
  • S1U:第二个Survivor区的使用大小(KB)
  • EC:Eden区的大小(KB)
  • EU:Eden区的使用大小(KB)
  • OC:Old区大小(KB)
  • OU:Old使用大小(KB)
  • MC:方法区大小(KB)
  • MU:方法区使用大小(KB)
  • CCSC:压缩类空间大小(KB)
  • CCSU:压缩类空间使用大小(KB)
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

你可能感兴趣的:(大数据面试,jvm,java)