2. JVM内存模型

1. JVM内存模型与上下游关系

首先来看JVM与系统之间的关系。
在这里插入图片描述

2. jvm内存分配及参数

下图展示如何通过参数控制各区域的大小。
在这里插入图片描述
控制参数

  • -Xms设置堆的最小空间大小。
  • -Xmx设置堆的最大空间大小。
  • -XX:NewSize设置新生代最小空间大小。
  • -XX:MaxNewSize设置新生代最大空间大小。
  • -XX:PermSize设置元空间(永久代)最小空间大小。
  • -XX:MaxPermSize设置元空间(永久代)最大空间大小。
  • -Xss设置每个线程的堆栈大小。
    没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。
    老年代空间大小=堆空间大小-新生代大空间大小

3. java堆

1. java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此区域唯一目的是存储几乎所有的实例对象和数组,随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不绝对。

  • 逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。
    参考文章:浅谈HotSpot逃逸分析

2. Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代老年代;新生代又分为EdenFrom SurvivorTo Survivor等。
默认情况下内存分配比例:

  • 新生代:老年代=1:2。
  • Eden:From Survivor:To Survivor=8:1:1。

3. 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。

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

3.1 新生代

程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成。

  • 新生代的初始值NewSize默认为1M,最大值需要设置,可以通过参数-XX:NewSize和-XX:MaxNewSize或-Xmn进行设置;
  • 默认比例:Edem : from : to = 8 :1 : 1,可以通过参数-XX:SurvivorRatio 来设定
  • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以总是有一块Survivor 区域是空闲着的。

工作原理

  1. 在未开始GC的时候,对象只会存在于Eden区和From区,To区是空的。
  2. 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
    年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。
  3. 这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
  4. Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

3.2 老年代

用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:

  • 大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
  • 大的数组对象,且数组中无引用外部对象。

4. 方法区

  • 方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
  • 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码,运行时常量池等数据。
  • 和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
  • 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载。

运行时常量池
是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

注意:

  • 在 JDK1.7之前,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;
  • 从 JDK1.7 开始HotSpot 开始移除永久代。其中符号引用(Symbols)被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。
  • 在 JDK1.8 中,永久代已完全被元空间(Meatspace)所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制

5. java栈(虚拟机栈)

  • 线程私有的,它的生命周期与线程相同。
  • 虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈出栈的过程。
  • 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
  • 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  • 该区域可能抛出以下异常:
  1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  2. 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

在这里插入图片描述

6. 程序计数器

  • 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
  • 多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(多核处理器对应一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,
  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。

7. 本地方法栈

本地方法栈 为虚拟机使用到本地方法服务(native)。本地方法栈为线程私有,功能和虚拟机栈非常类似。线程在调用本地方法时,来存储本地方法的局部变量表,本地方法的操作数栈等等信息。

本地方法:是非java语言实现的方法,例如,java调用C语言,来操作某些硬件信息。

参考文章
1.http://www.ityouknow.com/jvm/2017/08/25/jvm-memory-structure.html
2.https://www.jianshu.com/p/0ecf020614cb
3.https://www.jianshu.com/p/76959115d486
4.https://juejin.im/post/5ad5c0216fb9a028e014fb63
5.https://www.cnblogs.com/Java3y/p/9296496.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值