美文网首页
JVM内存模型基础

JVM内存模型基础

作者: 笔记本一号 | 来源:发表于2020-04-11 07:42 被阅读0次

    学习了周志明老师的书我们知道java的内存结构可以大体由五部分组成:方法区、堆内存、虚拟机栈、本地方法栈、程序计数器,这五个部分组成的东西就叫做运行时数据区域。其中

 线程独享:虚拟机栈、本地方法栈、程序计数器。

线程共享: 方法区、堆。

jvm结构 jmm内存模型

  方法区和堆:它们都是线程共享的,方法区是堆内存的一个逻辑部分,方法区存储的是加载进运行时数据区域的class文件信息、常量、静态变量、即时编译器编译后的代码等数据,而堆内存主要是存放对象实例,当无法满足内存分配需求时两者都会抛出OutOfMemory Error,设计者为了区分它们给方法区设置了别名叫Non-heap,方法区也有一个称谓叫永久代,jdk8后堆内的方法区迁移到了jvm的本地内存称作元空间(MetaData Space)。

    虚拟机栈和本地方法栈:它们都是线程私有的,对应的职责一样都是方法的执行调用(同时虚拟机栈也会存储方法的局部变量),只不过虚拟机栈主要是执行java方法,而本地方法栈则是为虚拟机调用Native服务(由C++编写的服务方法),有些虚拟机会将这两个部分合二为一

Heap(堆)的结构:

1、年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。

2、堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。

3、非堆内存用途:永久代,也称为方法区,存储程序运行时的元数据和字节码信息,比如类的元数据、方法、常量、属性等。

JVM堆内存的变化:

jdk7和jdk8的内存模型不太一样,经过升级后,jdk到了8版本后性能得到了质的飞跃,堪称是从摩托车变成了F4跑车,即时到了现在的jdk14,jdk8任然是最强王者,性能任然是最强的

在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存

移除永久代原因:

1、HotSpot与JRockit都被Oracle收购了,为融合HotSpot与JRockit而做出的改变,因为JRockit没有永久代。

2、因为永久代中主要是存放类的信息,字节码信息,这些信息基本是不需要回收的,所以把永久代移动到本地内存,jvm就不需要维护永久代的内存,永久代移动到本地内存变成元空间后可以使用的内存就基本没有限制了,因此就不再会出现永久代OOM问题了

JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)也就是方法区。

jdk7的堆内存模型:

jdk7

jdk8的堆内存模型:

jdk8

方法区(非堆内存)

方法区在虚拟机启动的时候创建,方法区是堆的逻辑组成部分。

线程共享,它的职责就是存储类的元数据、加载进运行时数据区域的class文件信息、运行时常量池、静态变量、即时编译器编译后的代码等数据。

八种基本数据类型(byte、short、int、long、float、double、char、boolean)的静态变量会在方法区开辟空间,并将对应的值存储在方法方法区,对于引用类型的静态变量如果未用new关键字为引用类型的静态变量分配对象(如:static Object obj;)那么对象的引用obj会存储在方法区中,并为其指定默认值null;若,对于引用类型的静态变量如果用new关键字为引用类型的静态变量分配对象(如:static Person person = new Person();),那么对象的引用person 会存储在方法区中,并且该对象在堆中的地址也会存储在方法区中(注意此时静态变量只存储了对象的堆地址,而对象本身仍在堆内存中

很多程序员称这个区域为永久代,但是两者其实并不等价.永久代只是实现了方法区而已,这样HotSpot可以像管理java的堆内存一样管理这部分的内存,能够省去专门为方法区编写内存代码管理的工作,方法区使用的是JVM的堆内存,但是JVM团队想区分开来,所以重新起名叫做方法区,方法区和永久代的关系如:汽车和

永久代只是为了实现方法区,使得JVM可以通过像管理堆内存一样去实现管理方法区的GC机制。注意这里的永久代在jdk8后,取而代之的是元空间(MetaSpace),元空间并不在虚拟机中而是使用的本地内存

元空间:

    jdk1.8之后已经取消了永久代,改为元空间,类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。这项改造也是有必要的,永久代的调优是很困难的,虽然可以设置永久代的大小,但是很难确定一个合适的大小,因为其中的影响因素很多,比如类数量的多少、常量数量的多少等。永久代中的元数据的位置也会随着一次full GC发生移动,比较消耗虚拟机性能。同时,HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。

  jvm堆内存是java虚拟机中最大的组成部分,它主要是负责存储jvm几乎所有的对象实例和数组,所有线程共享堆,堆可以实现动态内存的分配,但是相对于栈效率较慢,也是jvm垃圾回收最频繁的区域,由于垃圾回收算法基本上用的是分代算法,所以它分为老年代和年轻代,年轻代还可以分为Eden空间,From Survivor空间、to Survivor空间三部分,在物理层面堆内存可以处于不连续的内存空间,只要逻辑上连续即可,这个特性影响了堆内存GC回收算法的选择和效率问题(目前分代算法是GC主流的回收算法)

堆的分代:

新生代的工作过程:

新生代分为三个区域,一个Eden区和两个Survivor区,它们之间的比例为(8:1:1),这个比例也是可以修改的。通常情况下,对象主要分配在新生代的Eden区上,少数情况下(大对象直接进入老年代)也可能会直接分配在老年代中。Java虚拟机每次使用新生代中的Eden和其中一块Survivor(From),在经过一次Minor GC后,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间上(这里使用的复制算法进行GC),最后清理掉Eden和刚才用过的Survivor(From)空间。将此时在Survivor空间存活下来的对象的年龄设置为1,以后这些对象每在Survivor区熬过一次GC,它们的年龄就加1,当对象年龄达到某个年龄(默认值为15)时,就会把它们移到老年代中。

GC的过程:

新生代GC:
新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。

老年代GC:

老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

1、Minor GC : 清理年轻代 

2、Major GC : 清理老年代

3、Full GC : 清理整个堆空间,包括年轻代和永久代

老年代的工作过程:

年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。

老年代新生代大小比例:

新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。

默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

分代的目的:

分代主要为了解决碎片化。如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC。

堆的分代结构图:

JVM堆内存常用参数

虚拟机栈:

  虚拟机栈是线程私有的,它描述的是一个线程和方法执行的内存模型,它里面包含了一个个的栈帧,所以由栈帧组成的栈又称为帧栈,一个线程对应一个栈,一个栈对应着多个栈帧,栈在编译期时就确定了其大小和生命周期,栈帧中又存储了局部变量表(编译期可知的各种数据类型例如:boolean、byte、char、short、int、float、long、double,局部变量表中数据存放是使用插槽(slot)每个插槽可以存放32位的数据,long\double为64位占用两个插槽),操作数栈(是相等于java虚拟机栈的寄存器,用于传递参数)、动态链接、方法出口等信息,栈帧的职责就是方法的执行,每一个方法的执行都对应着一个栈帧的出栈和入栈,虚拟机每次调用方法时间就会创建一个栈帧并压栈,调用完方法退出后就会修改栈顶指针,这样就能销毁栈帧的内容。虚拟机栈的生命周期和线程相同,是java方法执行的内存模型,如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverflowError(这就像给一个杯子倒入了超过杯子容量的水一样,容量不足水就溢出了),当然虚拟机栈是可以向jvm申请内存的,但是如果无法申请到足够的内存,就会抛出OutOfMemory Error异常

本地方法栈

    本地方法栈是是线程私有的,主要职责就是调用Native方法服务,和虚拟机栈一样如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverflowError,法申请到足够的内存,就会抛出OutOfMemory Error异常

程序计数器:

  程序计数器是jvm中很小的一块内存空间,由于工作只是记录一个数值所以它的运行速度是非常快的,并且不会发生内存溢出现象,它是线程私有的,是线程执行字节码的行号指示器,字节码解释工作就是通过改变这个计数器的值来执行下一步该执行的指令,分支、循环、跳转等基础功能都需要依赖这个计数器去实现,值得注意的是在执行Native方法时,这个计数器会被置为空

常量池:

几种常量池:

1、class常量池(静态常量池):class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References),每个class文件都有一个class常量池。

字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;

符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。

2、运行时常量池:虚拟机会将各个class文件中的常量池载入到运行时常量池中,即编译期间生成的字面量、符号引用,总之就是装载class文件。也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String.intern()),符号引用可以被解析为直接引用。

运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法

在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

3、字符串常量池 :字符串常量池可以理解为是分担了部分运行时常量池的工作。加载时,对于class文件的静态常量池,如果是字符串就会被装到字符串常量池中。字符串常量池中的字符串只存在一份。

在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上

在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中

在JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。

JDK8以后也还是放在了Heap空间中,并没有已到元空间。

class文件有常量池存放这个类的信息,占用了大多数空间。但是运行时所有加载进来的class文件的常量池的东西都要放到运行时常量池,这个运行时常量池还可以在运行时添加常量。字符串常量池、Integer等常量池则是分担了运行时常量池的工作,在永久代移除后,字符串常量池也不再放在永久代了,但是也没有放到新的方法区—元空间里,而是留在了堆里,为了方便回收。运行时常量池当然是随着搬家到了元空间里,毕竟它是装类的重要等信息的,有它的地方才称得上是方法区。

jdk8常量池在堆内

GC:

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

相关文章

网友评论

      本文标题:JVM内存模型基础

      本文链接:https://www.haomeiwen.com/subject/qopkmhtx.html