JVM学习.01 内存模型

Posted 有一只柴犬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习.01 内存模型相关的知识,希望对你有一定的参考价值。

1、前言

对于C、C++程序员来说,在内存管理领域,他们拥有对象的“所有权”。从对象建立到内存分配,不仅需要照顾到对象的生,还得照顾到对象的消亡。背负着每个对象生命开始到结束的维护和管理责任。

对于JAVA程序来说,因为JVM虚拟机的加持,不再需要为每个对象去写配对的delete/free代码。交由虚拟机去管理内存,因而相对来讲不容易出现内存移除和内存泄漏的问题。不过也正是JAVA程序员把内存控制权交给了JVM,一旦出现了内存泄露和溢出的问题,修正起来会比较艰难,如果你不了解虚拟机的化。因而从事JAVA的程序员,多多少少需要了解JVM的内存模型,帮助我们更好应对JAVA内存问题。

2、JVM内存模型

很多Java开发人员会把Java内存区域划分为堆内存(Heap)和栈内存(Stack)。这种划分方式是直接继承C、C++程序的内存布局。在Java中实际内存区域划分会更复杂。

开篇一张图:

线程隔离的数据区,或称为“线程私有的内存”。他们的生命周期与线程相同。线程开辟的时候,会分配该内存空间,当线程被销毁,则这么部分内存空间也会随即释放。

2.1、 程序计数器

程序计数器为当前线程所执行的字节码的行号指示器。由于JVM的多线程是通过时间片轮转切换,依次分配处理器来执行的。因为在任何一个确定的时刻,一个处理器只能执行一条线程指令。当处理器被切换到另一个线程指令执行的时候,处理器需要记住当前指令中断的位置,以便下次执行的时候从当前中断位置恢复。该中断的位置成为指令字节码的行号。程序计数器就是用来存储该行号,因此程序的分支,循环,跳转,异常处理,线程恢复等都需要依赖这个计数器。

如果一个线程正在执行一个JAVA方法,则该计数器记录的是当前正在执行的虚拟机字节码指令的地址;

如果一个线程正在执行的是本地(Native)方法,则该计数器的值为空。

该内存区域也是唯一一个在《Java虚拟机规范》中没有规定任何OOM情况的区域。为线程私有

2.2、虚拟机栈

Java虚拟机以方法作为最基本的执行单元,“栈帧”则是用于支持虚拟机进行方法调用和执行的数据结构,也是虚拟机运行时数据区中的虚拟机栈的栈元素。

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。虚拟机栈也是线程私有的。

例如举个简单的例子,我们同步将虚拟机栈内存放大:

// 有一段代码
double methodA() 
    int quantity = 10;
    double result = methodB(quantity);
    return result;


double methodB(int quantity)
    if(isVip()) 
        return quantity * _basePrice * 0.9;
     else 
        return quantity * _basePrice * 0.98;    
    


boolean isVip()
    retrun _isvip == 1 ? true : false;

处理器在执行该段代码的时候,先执行methodA(),中间发现调用了methodB(),后面发现又调用了isVip()。此时方法methodA,methodB,isVip执行时的数据结构被称为栈帧。

则该线程的虚拟机栈模型如下:

  • 方法执行methodA方法,method方法对应的栈帧(栈帧1)被压入栈底位置,此时methodA为当前活动栈帧;

  • 当方法methodA调用methodB方法,此时methodB方法对应的栈帧(栈帧2)也被压入栈中,此时执行methodB方法;

  • 当方法methodB调用isVip方法,继续将isVip方法对应的栈帧(栈帧3)压入栈中;

  • 当isVip方法执行完毕,对应的isVip栈帧执行出栈操作,并将结果记录下来;

  • 当methodB方法执行完毕,同样对应的栈帧2执行出栈操作;

  • methodA执行完毕,对应的栈帧1执行出栈操作;此时虚拟机栈中没有任何的栈帧;当线程执行结束后,该虚拟机栈也会随即消亡(实际上是在等待被回收)。

试想一下:如果一个递归方法,且没有合适的条件退出。会导致死循环递归,那么最终该虚拟机栈也会被压爆。这时候虚拟机会抛出StackOverflowError异常。
StackOverflowError异常:指线程请求的栈深度大于虚拟机所允许的深度,将抛出该异常。
OutOfMemoryError异常:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,则会抛出该异常。(HotSpot虚拟机的栈容量是不可以动态扩展的,所以在此虚拟机上是不会出现虚拟机栈导致的OutOfMemoryError)。

2.2.1、局部变量表

是一组变量值的存储空间,用于存放方法的参数和方法内部定义的局部变量

局部变量是以变量槽(Slot)为最小单位。每个变量槽都应该能存放一个虚拟机基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型或returnAddress类型)的数据。

当一个方法被调用时,JVM会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(非static),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可通过“this”来访问。

2.2.2、操作数栈

操作数栈是方法执行算数运算或调用其他方法进行参数传递时候的媒介。操作数栈也可以称为表达式栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据。

2.2.3、动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个方法的引用是为了支持方法调用过程中的动态链接。

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时被转化为直接引用(称为静态解析)。另一部分将在每次运行期间转化为直接引用,这部分就称为动态连接。

2.2.4、方法出口

当一个方法执行后,要么正常调用完成,将返回值返回给上层调用者;要么异常调用完成,因为异常导致程序退出。

但是不管如何退出,在方法退出之后,程序都必须返回到最初方法调用时的位置,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。

方法退出的过程实际上等同于把当前栈帧出栈,所以退出时可能执行的操作有:

1、恢复上层方法的局部变量表和操作数栈;

2、把返回值(如果有的话)压入调用者栈帧的操作数栈中;

3、调整PC计数器的值以指向方法调用指令后面的一条指令等。

2.2.5、附加信息

其他附加信息。不过一般会把动态连接,方法返回地址,其他附加信息统一称为栈帧信息。

2.3、本地方法栈

本地方法栈与虚拟机栈的作用非常类似。只是虚拟机栈为Java方法服务,而本地方法栈为使用本地方法(Native)服务。HotSpot虚拟机通常直接把本地方法栈和虚拟机栈合二为一,统称为栈。同样本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

2.4、Java堆

对于Java应用程序,Java堆是整个虚拟机内存中最大的一块。是被所有线程共享的一块内存区域。Java中几乎所有的对象实例以及数组都在堆上分配。

因此堆是GC执行垃圾回收的重点关注对象。

堆空间的模型如下:

  • 方法区:详见2.5。

  • 老年代(Tenure / Old Gen):存储长期存活对象,老年代占堆空间的2/3。如果老年代内存满了,会触发Major GC。

  • 新生代(Young Gen):生命周期较短的对象,占对空间的1/3。其中新生代又分为Eden,From Survivor,To Survivor。

  • 伊甸空间(Eden):顾名思义,伊甸园为一切初始的地方。这里指对象的生命周期刚出生便是在这块内存区域。如果Eden空间不足以给新对象分配足够的内存,则会触发Minor GC对Eden进行垃圾回收,将不需要的对象销毁,剩余对象放进S0(From Survivor)区。如果再次触发GC,会将S0复制到S2。如果再次触发GC,存活对象从S2复制到S0。GC过程该空间会重复此步骤,直到对象存活周期经历过15次GC(默认15次,可配置)依然没有被回收,将会转移到老年代。

  • S0空间(From Survivor)/ S2空间(To Survivor):这两个成为幸存空间,Eden、S0、S2的内存占用比例默认为8:1:1。当新生代内存达到一定量时,如果直接进行垃圾回收(清理)会带来空间碎片问题。因此当进行清理之前,会将存活的对象放进S0和S2区域,有助于垃圾回收和清理。

为什么Eden、S0、S2的内存占用比例默认为8:1:1?
IBM公司研究表明,新生代中的对象约98%生命周期都是很短的。8:1:1是基于大量实验和数据收集分析统计之后的比较合理的比例。

Minor GC / Young GC:新生代GC
Major GC:老年代GC,对于高响应要求的系统,需要尽量减少Major GC,会导致响应超时
Full GC:清理整个Heap空间,包括新生代,老年代,永久代

为什么要把堆空间进行分代?不分代不能工作吗?
其实分代的意义是为了优化垃圾回收(GC)的性能,简单理解就是分而治之。分代以后对部分需要清理对象只需要小范围进行回收即可,无需扫描整个堆空间。不过后面的G1垃圾收集器开始,取消了内存分代,取而代之的是每个平等的region。

一个对象创建中堆空间的内存申请和分配流程大致如下:

此外JVM提供了一些操作对空间的参数选项,常见的有:

参数

描述

-Xms

堆内存初始大小

-Xmx

堆内存最大允许大小

-Xns

新生代内存初始大小

-Xmn

新生代最大允许大小

-XX:SurvivorRatio=8

年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1

-Xss

线程栈内存大小。JDK1.5后默认每个为1M,减少该值能生成更多线程

2.5、方法区

方法区也是线程共享的内存区域,用于存储已被JVM加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。别名也叫“非堆”,目的是与Java堆区分开。

1、类型信息:类class,接口interface,枚举enum,注解annotation

2、字段信息(域信息):域名称,信息,类型的修饰特征符public, abstract,final......

3、方法信息:返回类型void等,参数列表,方法修饰特征public, protected

/**
 * Student:方法区
 * stuInstance: 栈区
 * new Student(): 堆区
 */
Student stuInstance = new Student();

说到这里,很多人会把方法区称为“永久代”,或者进行等价。本质上不是的,起初HotSpot设计团队选择把分代设计扩展至方法区,或者说用永久代来实现方法区,这样做的目的是HotSpot的GC回收器能够像Java堆一样管理这部分内存,就不用单独为方法区编写一个专门的内存管理工作。

JDK8之后废弃了永久代,改为元空间(Meta Space)。元空间与永久代类似,最大的区别是元空间直接使用本地内存,而不是JVM。因此JDK8过后,元空间就不再会出现OOM问题。

2.6、运行时常量池

运行时常量池是方法区的一部分。class文件中除了有类的版本,字段,方法,接口等描述信息以外,还有常量池表,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

常量池是方法区的一部分,当然如果无法申请到内存时,也会抛出OutOfMemoryError。

3、直接内存

直接内存并不是JVM的内存区域,属于操作系统本身的内存。JDK1.4加入的NIO类,引入了Channel与缓冲区Buffer。它可以直接使用Native函数库直接分配直接内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用来进行操作,可以显著提高性能。

为什么这里要讲直接内存?直接内存虽然不受到Java堆的限制,但是收到了操作系统总内存大小以及处理器寻址空间的限制。 通常我们在用-Xmx设值堆大小信息时,会经常忽略直接内存;有可能使得内存区域大于物理内存限制,而导致动态扩展时出现OOM异常。

直接内存既然不属于Java内存,那么自然也JVM GC也无法回收他。如果需要回收,需要主动调用Unsafe的freeMemory方法。

可以通过-XX:MaxDirectMemorySize来指定直接内存的容量大小,如果不指定,默认与Java堆的最大值一致。

直接内存导致内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常情况,如果发现内存溢出之后产生的Dumo文件很小,而程序中又直接或间接使用了Directmemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因。

4、小结

JVM专栏第一篇。明白了JVM的内存模型,对于JVM内存的一些问题处理应该会更加得心应手(面试唬人)。

参考资料:《深入理解Java虚拟机》 - 第三版

JVM相关之JVM运行参数和内存模型

Jvm优化
了解下我们为什么要学习 JVM优化
掌握 jvm的运行参数以及参数的设置
掌握 jvm的内存模型(堆内存)
掌握 jamp命令的使用以及通过MAT工具进行分析
掌握定位分析内存溢出的方法


为什么要学习JVM优化
JVM被称为Java虚拟机,所有Java程序的运行都依赖于JVM
1.应用运行一段时间后突然停止,程序没有响应
2.服务器的CPU突然升高
3.在多线程应用下,如何去分配线程数量
。。。。。。。。。。。。。。。。。。。。。。。
优化JVM目的是为了让程序运行的更快

JVM运行参数

合理的调整JVM参数,也能够加快运行速度
标准参数

无论如何更新迭代版本,标准参数不会进行更改
-version
-help
-X参数,非标准参数:在更新迭代过程中,有一些参数信息是变化的
-Xms 初始内存
-Xmx 最大内存
-XX参数,非标准参数:专门做优化和debug用的,使用率比较高
-XX:+UseSerialGC

标准参数:一般来说标准参数比较稳定,我们可以使用java -help来查看所有的标准参数
java -help查看所有的标准参数

技术图片

 

 

 


java -version查看版本

技术图片

 

 

 


java -D设置系统参数
技术图片

 

 

 

设置系统参数

1).使用idea中的application中vm options设置参数

技术图片

 

 

 

2).使用命令创建参数

技术图片

 

 

 


-server和-client有什么区别
ServerVM模式当中:

初始堆空间会大一些,

默认使用的是并行垃圾回收器,

启动慢,运行快


ClientVM模式当中:

初始堆空间相对小一些,使用串行垃圾回收期,

目的是为了让JVM启动速度更快一些,运行就比ServerVM慢一些

JVM运行的时候是根据操作系统选择运行的模式
如果是32位的window操作系统,默认使用的client
如果在其他系统上,只要有2G以上的内存以及至少两个以上的CPU那么默认使用Server模式
在64位操作系统当中只有Server模式,不支持Client模式


-X非标准参数
在不同版本的JVM当中,参数可能有所不同,通过java -X查看非标准参数列表

1.非标准参数列表的三种模式
-Xint:解释模式(interpreted model),强制要求JVM执行所有的字节码,效率低
-Xcomp:编译模式,与-Xint相反,第一次会将所有的字节码编译成本地的代码,第一次运行慢,后续运行快
万一我们有一些字节码只需要编译一次
-Xmixed:混合模式,就是解释模式和编译模式混合使用,有jvm自身决定应该采用哪种模式,jvm默认的默认

-XX参数:也是非标准参数,主要用于JVM调优和Debug操作
-XX参数有两种模式:
1.boolean类型 +代表true,代表应用该参数 -代表false代表不应用该参数
2.非boolean模式
-XX:NewRatio=1
3.默认情况下没有禁用手动调用GC,也就是说程序员可以手动调用,手动回收
禁用手动调用垃圾回收器,程序员手动调用gc是无效的 java -showversion -XX:+DisableExplicitGC xxx

-Xms -Xmx参数调整堆内存大小
-Xms代表初始的堆内存大小 -Xmx代表最大堆内存大小
java -Xms512m -Xmx1024m JVMTest


-Xms512m等价于-XX:InitialHeapSize
-Xmx2018m等价于-XX:MaxHeapSize

查看Java程序运行时使用的参数
java -XX:+PrintFlagsFinal JVMTest,查看过程中发现有=和:=,=代表默认值 :=代表修改过的值
更改参数信息:java -XX:+PrintFlagsFinal -XX:+VerifySharedSpaces -version


查看当前正在运行的Java的参数
jps命令查看当前所有正在运行Java进程
jps -l 查看当前所有正在运行Java进程详细信息
查看正在运行的Java进程信息jinfo -flags 进程id
查看进程的参数信息:jinfo -flag MaxHeapSize 6752

 

内存模型
jdk1.7的内存模型
1.年轻代:新创建的对象,比如new的对象,在对象比较小会放在年轻代当中存储
2.年老代:如果当前创建对象比较大,不经过年轻代直接放在年老代当中,年轻代通过多次垃圾回收期没有回收掉的对象也会放在年老代当中
3.永久代:比如我们部署一个程序到Tomcat当中,Tomcat启动的时候要去加载所有的Class,以及Method,以及Field都会存放在永久代当中

技术图片

 

 

 

jdk1.8的内存模型
1.年轻代:新创建的对象,比如new的对象,在对象比较小会放在年轻代当中存储
2.年老代:如果当前创建对象比较大,不经过年轻代直接放在年老代当中,年轻代通过多次垃圾回收期没有回收掉的对象也会放在年老代当中
3.MetaSpace元数据空间

技术图片

 

 



1.8版本以后废除了永久代,因为要融合JRocket VM虚拟机使用,JRocket VM虚拟机不支持永久代,所以废除了

现实使用中,由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。

基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。

通过jstat命令进行查看堆内存使用情况

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

查看class加载统计

技术图片

 

 技术图片

 

 查看编译统计

技术图片

 

 技术图片

 

 

垃圾回收统计

技术图片

 

 技术图片

 

以上是关于JVM学习.01 内存模型的主要内容,如果未能解决你的问题,请参考以下文章

学习笔记JVM之内存模型

JVM学习1.0

Jvm内存模型

JVM相关之JVM运行参数和内存模型

性能测试之jvm内存模型

JVM内存结构 VS Java内存模型 VS Java对象模型