目录
4.Runtime data area 运行数据区详解(栈管运行,堆管存储)
1.JVM简析:
作为一名Java使用者,掌握JVM的体系结构也是很有必要的。
说起Java,我们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。
运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。
它们的关系如下图所示:
Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:
在Java平台的结构中, 可以看出Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。
JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。
下面我们从JVM的基本概念和运过程程这两个方面入手来对它进行深入的研究。
2.JVM基本概念
2.1 基本概念
JVM是可运行Java代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM是运行在操作系统之上的,它与硬件没有直接的交互。
2.2 运行过程
我们都知道Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码 。
也就是如下:
• Java源文件—->编译器—->字节码文件
• 字节码文件—->JVM—->机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
2.3 三种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。
3.JVM的体系结构
3.1 Class Loader类加载器
负责加载 .class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定。
3.2 Native Interface本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体作法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见, 因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等。
3.3 Execution Engine 执行引擎
负责执行来自类加载器子系统(class loader subsystem)中被加载类中在方法区包含的指令集,通俗讲就是类加载器子系统把代码逻辑(什么时候该if,什么时候该相加,相减)都以指令的形式加载到了方法区,执行引擎就负责执行这些指令就行了。
(1) 程序在JVM主要执行的过程是执行引擎与运行时数据区不断交互的过程,可理解为上面“方法区中的动图”
(2)但是执行引擎拿到的方法区中的指令还是人能够看懂的,这里执行引擎的工作就是要把指令转成JVM执行的语言(也可以理解成操作系统的语言),最后操作系统语言再转成计算机机器码。
(3)解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。
即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
3.4 Runtime data area 运行数据区
虚拟机内存或者Jvm内存,从整个计算机内存中开辟一块内存存储Jvm需要用到的对象,变量等,运行区数据有分很多小区,分别为:方法区,虚拟机栈,本地方法栈,堆,程序计数器。
4.Runtime data area 运行数据区详解(栈管运行,堆管存储)
说明:JVM调优主要就是优化 Heap堆 和 Method Area 方法区
4.1 Native Method Stack 本地方法栈
(1)先解释什么是本地方法:jvm中的本地方法是指方法的修饰符是带有native的但是方法体不是用java代码写的一类方法,这类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。
(2)作用同java虚拟机栈类似,区别是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
(3)是线程私有的,它的生命周期与线程相同,每个线程都有一个。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。
4.2 PC Register程序计数器
也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
(1),区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。
(2)当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
(3)程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
(4)此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
4.3 Method Area方法区
(1)在虚拟机启动的时候创建。
(2)所有jvm线程共享。
(3)除了和堆一样不需要不连续的内存空间和可以固定大小或者可扩展外,还可以选择不实现垃圾收集。
(4)用于存放已被虚拟机加载的类信息、常量、静态变量、以及编译后的方法实现的二进制形式的机器指令集等数据。
(5)被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。
(6)运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
静态变量,常量,类信息(构造方法/接口定义),运行时常量池存在方法区中;但是实例变量存在堆内存中,和方法区无关。
4.4 Stack 栈
4.4.1 栈是什么?
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。
基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
4.4.2 栈存储什么?
栈帧中主要保存3类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data):包括类文件、方法等等。
4.4.3 Java虚拟机栈(Java Virtual Machine Stack)
(1)线程私有的,它的生命周期与线程相同,每个线程都有一个。
(2)每个线程创建的同时会创建一个JVM栈,JVM栈中每个栈帧存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double;和reference (32 位以内的数据类型,具体根据JVM位数(64为还是32位)有关,因为一个solt(槽)占用32位的内存空间 )、部分的返回结果,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址;
(3)每一个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
(4)栈运行原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进......F3栈帧,再弹出F2栈帧,再弹出F1栈帧。
(5)JAVA虚拟机栈的最小单位可以理解为一个个栈帧,一个方法对应一个栈帧,一个栈帧可以执行很多指令,如下图:
对上图中的动态链接解释下,比如当出现main方法需要调用method1()方法的时候,操作指令就会触动这个动态链接就会找打方法区中对于的method1(),然后把method1()方法压入虚拟机栈中,执行method1栈帧的指令;此外如果指令表示的代码是个常量,这也是个动态链接,也会到方法区中的运行时常量池找到类加载时就专门存放变量的运行时常量池的数据。
4.5 Heap 堆
(1)是Java虚拟机所管理的内存中最大的一块。
(2)不同于虚拟机栈,本地方法栈和pc寄存器,堆是jvm所有线程共享的。
(3)在虚拟机启动的时候创建。
(4)唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
(5)Java堆是垃圾收集器管理的主要区域。
(6)因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
(7)java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
(8)如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
4.5.1 新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1去也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullGCC),进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
a.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
4.5.2 养老区
养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。
4.5.3 永久区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。 原因有二:
a. 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
b. 大量动态反射生成的类不断被加载,最终导致Perm区被占满。
说明:
1.堆的分配参数
-Xmn
- 设置新生代大小
-XX:NewRatio
- 新生代(eden+2*s)和老年代(不包含永久区)的比值
- 4 表示 新生代:老年代=1:4,即年轻代占堆的1/5
-XX:SurvivorRatio
- 设置两个Survivor区和eden的比
- 8表示 两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10
2.堆的分配参数 – 总结
根据实际事情调整新生代和幸存代的大小
官方推荐新生代占堆的3/8
幸存代占新生代的1/10
在OOM时,记得Dump出堆,确保可以排查现场问题