jvm内存布局

若无特殊说明,内存布局是以jdk8的hosport虚拟机展示的。
字节码展示工具:idea种的插件,插件名jclasslib Bytecode Viewer

内存布局图

先对jvm的内存模型做一个大致的了解,等会在详细讲解每一个区域的功能和作用
简略图:其中绿色的代表的是线程私有的,其他颜色的代表的是可以被所有线程所共享
在这里插入图片描述


详细图:以jdk8的hosport虚拟机为例,版本不同,结构不一样
在这里插入图片描述

程序计数器

介绍

保证处理器在切换线程之后,能够回到原来的位置继续执行当前的线程,不会让程序出错。
同时,它也可以保证在程序执行跳转等指令的时候,可以跳转到正确的位置。
程序计数器可以看作是当前字节码的行号指示器。

图示

在这里插入图片描述

从B返回到A之后,处理器继续从50的位置开始执行,保证了切换线程之后的程序仍然是正确的。


其他

  1. 它是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域
  2. 它记录的是虚拟机栈最上面的栈帧中的字节码行号
  3. 它的占用内存很小,只是记录当前的执行位置
  4. 它属于线程私有的,每个线程都有一份
  5. 当前线程执行的一个Java方法,PC计数器才会记录正在执行的虚拟机字节码指令的地址;如果是执行的本地方法(即用native修饰的方法),这个计数器的值为空(Undefined)



虚拟机栈

概述

  1. 栈和堆的区别:栈管运行,堆管存储

在jvm的内存模型中,栈和堆都是放入内存中的,从某种意义上来硕,栈和堆的运行速度是一样的。
但是根据计算机的底层知识,涉及到堆栈的,栈的运行速度都要比堆块,jvm也不例外。
我个人认为,在jvm中栈比堆的运行速度块的原因是:栈负责的事情少,并且占用空间小,从而执行效率快。就像负责管理一个小房间和收拾一个大别墅一样,小房间方便管理,但是大房间能放的东西多

  1. 不会发生GC,但是会报OOM,OOM分为两种情况:

如果通过参数设置了栈的大小,当栈溢出时:StackOverflowError的错误,设置10m参数:-Xss10m 如果不设置参数的,则栈会自动扩展,当栈扩展到内存空间不足的情况下,会报:OutOfMemoryError
但是在Hotspot虚拟机的栈的容量是不支持动态扩展,之前的Classic虚拟机可以

  1. 若是手动设置栈的大小,不能设置的太大,若设置太大,会使得能够创建的线程减少

    一个房间内的空间是固定的,若要放置的物品比较大,那么放这种物品的数量就会减少


栈帧

栈帧是栈中的基本单位,每添加一个栈帧,可以看作是程序执行了一个新的方法。
栈帧相当于栈中的元素,遵循着先进后出的思想
栈帧中存储的数据就是一个方法中的数据,栈帧中含有:局部变量表、操作数栈、动态链接、方法返回地址、一些附件信息等

局部变量表

局部变量表中是按槽数(Slot)来设置每个变量的大小的,除了long和double的类型展两个槽位外,其他的类型在局部变量表中占用了一个槽位。
每一个槽位的大小是由具体的虚拟机来实现的,比如一个槽位占用32位bit或者64位bit。hosport是32位
在一个非静态方法中,会有一个默认的局部变量this
如下表示的是方法中的局部变量表槽的数量,和方法中的局部变量表中的局部变量数量

在这里插入图片描述
在这里插入图片描述


若使用了内部代码块,槽便可以实现复用的功能:可以试着想一下,下面的代码占用多少个槽位

    public void a (){
        int b = 0;
        {
             long c = 0L;
        }
        int d = 0;

    }

在这里插入图片描述

这里可以看到局部变量表中的数据是三个,区别 加起来因该是三个槽位。是四个槽位的原因是:c退出代码块之后它的槽位就不用了,然后的使用c的 槽位,但是d只能用一个,所以还剩下一个槽位没变量用。注意,某些情况下,卡槽的复用会有一些副作用,会直接影响到系统 的垃圾回收行为(《深入理解Java虚拟机第三版》P297:8-2)
扩展

局部变量表中的数据和堆中的数据直接或间接的关联,那么这些数据就不会被GC

操作数栈

  • Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
  • 操作数栈的操作遵循栈的操作
  • 一个操作数栈的最大深度是在编译期间就确定的,并且新创建出来的栈帧中的操作数栈是空的
  • 栈中的深度和槽位是一直的,long和double占用两个深度
  • 操作数栈,在方法执行过程中,根据字节码指令,并非采用访问索引的方式来进行数据访问的
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

图示:
在这里插入图片描述

对字节码的解析:

  1. 将long类型的数值为0的数据放入操作数栈中
  2. 将一个long类型的,并且处于操作上栈顶的元素从操作数栈中去除,放入局部变量表的第1位上
  3. 将一个int类型的值为0的数据放入操作数栈中
  4. 同2
  5. 返回

知识扩展:

栈顶缓存技术:将栈顶元素全部缓存到物理CPU的寄存器中,以次降低内存的读写速度,提高执行引擎的执行效率
操作数栈的优化:两个相邻的栈帧出现一部分的重叠。让下面的栈帧中的部分操作数栈和上面栈帧中的部分部分局部变量表重叠在一起,这样不仅节约了空间,更重要的是在方法调用时可以直接公用 一部分数据,无需进行额外的参数复制传递



动态链接

在类加载的阶段或者第一次使用的时候,字节码中的常量此中的符号引用会转换为内存中的具体内存地址的直接引用。但是还有一部分需要在每一次运行的时候转换为直接引用,因为这些数据只有在运行期间才知道其具体的数据类信息。

方法返回地址

退出方法由两种形式:正常返回、异常退出。不管这么返回,都会返回到方法被调用的位置,但是两种也有不同

  • 正常返回:调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
  • 异常退出:返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

附加信息

运行虚拟机在实现的时候添加的一些《java虚拟机规范》中没有定义的信息


本地方法栈

本地方法栈是搭配着本地方法接口来使用的。java代码中用native来修饰便是本地方法,其是用C、C++来实现的。即是java调用C、C++中的方法,来实现对底层操作系统、jvm的操作。
本地方法栈和虚拟机栈一样有异常,报的异常也一样,这里不再叙述

扩展:

并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。



基本概述


  • 《Java虚拟机规范》中是这样定义的:数组和对象实例都应该放在堆中,不可以放在栈中。但是实际情况有所不同,实际是:几乎所有的数组和对象都是放在堆中的。注意,这里是指的几乎,因为编译器优化可以产生栈上分配的效果
  • 几乎所有的线程都是共享堆中的数据的,但是也堆中也可以划分线程私有缓冲区(TLAB)



堆结构

堆中的结构如内存布局图中的结构所示,但是有一点不同,字符串表在jdk6、jdk7的时候都有所变化,这里将的堆结构是:堆中主要结构,即堆中存储对象、数组地方的结构。

概述

堆中放数据的地方分为:

  • 年轻代(新生代)、老年代
  • 新生代有可以分为:伊甸园区、幸存者0区、幸存者1区。两个幸存者区有时又称为from 、to区

Eden-伊甸园区

几乎所有的对象都是从伊甸园区new出来的。

几乎的原因是:栈上分配,和eden区进行了GC后都不足以放下一个对象,将会将对象放入老年代中

据IBM公司的研究表面,新生代中的80%的对象都是朝生暮死的,若一个对象在Eden区被创建,当Eden的内存不足的时候,将会进行GC,将Eden中GC后还活着的对象放入幸存者区


Survivor-幸存者区

接收来自伊甸园区中的数据,在每次进行GC的时候,一个幸存者区中的数据全部都会移动到另外一个幸存者区中,所有又叫from、to区。在一个对象进行移动的时候,会在其对象头中的一个GC编制位的值+1。
如图:
在这里插入图片描述

GC分代年龄是4bit,二进制也就1111转换位10进制也就15,也就是说,一个对象在幸存者区中存活了最多15次之后,就会被放入老年代中。最多的意思是可以通过参数修改最多存活次数的值,但这个值不能超过15.
设置最大分代年领的参数:-XX:MaxTenuringThreshold=x,x是最大存活次数,不能超过15。

还有一种情况,幸存者区种的对象在没到达MaxTenuringThreshold 的值的时候也可以进入老年代:

相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。



Old Gen-老年代

将 对象进入老年代之后,对象的生命周期将会延长,只有发生Major GC或Full GC)的时候,才会堆老年代种的垃圾进行回收,如果老年代都放不下要新到的对象,就会报OOM异常


对象分配流程

根据前面堆结构的概述,对创建一个对象进行流程图分析:
对象在堆中的移动

堆大小的设置

默认比例

  • 新生代:老年代 = 1:2
  • Eden:Survivor0:survivor1 = 8:1:1

相关参数

  1. 堆空间大小的设置
  • -Xms100m:初始内存10m (默认为物理内存的1/64);
  • -Xmx100m:最大内存100m(默认为物理内存的1/4)
  • 建议最大内存和初始内存设置一样,因为如果不设置一样,那么jvm会在内存 不够的时候扩展,影响性能
  1. 设置新生代的大小
  • -Xmn10m:新生代最大为10m,新生代的内存不允许动态扩建,所以没有初始内存
  • 这个值一般默认即可
  1. 设置新生代和老年代的比例
  • -XX:NewRatio=2:代表新生代占1,老年代占2
  • -XX:NewRatio=4:代表新生代占1,老年代占4
  1. 设置Eden和Survivor的比例
  • -XX:SurvivorRatio=8:Eden占8,Survivor占1
  1. 设置分代年龄
  • -XX:MaxTenuringThreshold=5:在Survivor区呆5次后进入老年代
  1. -打印GC的详细日志
  • XX:+PrintGCDetails:打印GC的详细日志
  1. 打印所有参数的值
  • -XX:+PrintFlagsFinal:若只进行了修改,那么打印的值就不上初始值
  1. 空间分配担保策略
  • -XX:HandlePromotionFailure=true:启动空间分配担保策略,jdk7后便不再使用

空间分配担保策略

在发现Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间:

  • 如果大于,则此次Minor GC是安全的
  • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
  • 如果HandlePromotionFailure=true,那么会进行检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于则进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。


JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。也就是说:jdk8中这个参数已经失效

总结:对此次GC进行预判,判断是否需要进行full GC。


各种GC的类型

Minor GC

对于年轻代的GC,对年轻代进行垃圾回收
触发机制:

  • Eden区满触发的GC,Survivor满不会引发GC。
  • 因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。



Major GC

老年代的GC,对就是老年代进行回收
触发机制:

  • 老年代空间不足触发
  • 往往伴随着Minor GC的出现。(Parallel Scavenge收集器就不会进行Minor GC)
  • 也就是说,老年代空间不足的时候,先进行Minor GC,若还不足,再进行Major GC
  • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。这也就是为什么进行Minor GC
  • 如果Major GC 后,内存还不足,就报OOM了。



Full GC

全局GC
触发机制

  • 主动调用System.gc()时,系统建议执行Full GC,但是不必然执行。
  • 老年代空间不足
  • 方法区空间不足
  • 年轻代进入老年代的时候,发现老年代的空间不足

说明:

full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。full GC占用的时间是最多的

TLAB-快速分配策略。

  1. TLAB是什么

全称:Thread Local Allocation Buffer,线程本地分配缓冲区。它是jvm为每个线程在Eden区中分配的私有缓存区域。
所有的基于OpenJdk衍生的jvm都含有TLAB的结构

  1. 为什么需要

对象实例的创建非常的频繁,在高并发的条件下,线程在 堆中划分内存空间是不安全的。
为了避免不同的线程操作同一块内存地址,需要实现加锁的机制,但是加锁在高并发下会影响性能
在多线程的条件下,使用TLAB可以避免线程的安全问题,从而 提高内存分配的吞吐量,其原因是每个线程在创建对象的时候,首先会考虑在自己的TLAB中创建,如果不行的话,再去堆中的Eden创建,并加锁。
将这种内存分配方式称之为快速分配策略

  1. 相关使用
  • 若TLAB中的空间不足,对象就不能在TLAB中创建,会加锁保证数据原子性在Eden中创建
  • -XX:+/-UseTLAB来设置是否开启快速分配策略
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,可以通过-XX:TLABWasteTargetPercent来设置占用Eden区域的大小
  • JVM将TLAB作为内存分配的首选。

图示:来自宋红康老师的jvm的TLAB
在这里插入图片描述

方法区

方法区在jdk6、jdk7、jdk8中都有所变化,若无特殊说明,那么就是6,7,8都没变的地方。

位置

《Java虚拟机规范》中说到,尽管方法区在逻辑上属于堆的一部分,但是一般不会对方法区进行垃圾回收或者压缩。
对于HotSpotJVM而言,方法区和堆是分开的,属于两个部分。


概念

方法区和堆的概念大多都是一样的:线程共享、逻辑上连续的内存空间、内存空间可固定可扩展、会报OOM
但是OOM根据jdk的不一样而报不同的错误:

  • jdk7之前(包括7):java.lang.OutOfMemoryError: PermGen space (永久代)
  • jdk8: java.lang.OutOfMemoryError: Metaspace(元空间)

Hosport虚拟机的演进

jdk6

在这里插入图片描述

jdk7

在这里插入图片描述

在7中将字符串表和静态变量从永久代中移动到了堆中


jdk8

在这里插入图片描述

各种调整的原因

永久代和元空间

将原来的永久代变成了元空间,永久代和元空间的区别:

  • 永久代使用的是Java虚拟机中的内存,可以通过-XX:MaxPermSize来设置,超过报OOM
  • 元空间使用的是本地内存,其最大可分配空间就是系统可用内存空间,不受jvm内存的影响


将永久代变为元空间的原因:

  • 为永久代设置空间大小是很难确定的。因为web项目中可能有很多需要动态加载的类,若这些类不断增加,那么永久代就会报OOM,而元空间和系统的内存挂钩,不受jvm影响。
  • 对永久代进行调优是很困难的。



字符串常量表

全称为StringTable,在开发中,通常会生成很多的字符串,而这些字符串通常使用后就不再用了。
若放在永久代中,那么就会导致永久代中有大量的字符串,若有大量的无用字符使得永久代空间不足,就会触发full GC,而full GC是全局GC,会导致回收效率极低,进行大量的full GC。
若放在堆中,若堆中 空间不足的时候,可以进行Major GC,要比full GC的时间要长一些。
在7之后,放入的是堆中的老年代中,如果放在Eden中,因为字符串表中始终有一些数据是不会收的,而不回收的数据最后都会到到老年代,所以不如直接存入老年代。


存储数据

  1. 类型信息

存储的信息包括:

  • 类型的全限类名(包名+类名)
  • 这个类型的父类,但是对于接口或者object是没有父类的
  • 类型的修饰符(public、final、abstract)
  • 这个类型的直接接口的一个有序列表
  1. 属性信息

JVM必须在方法区中保存类型的所有属性的相关信息以及属性的声明顺序。包括属性名称、属性类型、属性修饰符(public, private, protected, static, final, volatile, transient的某个子集)

  1. 方法信息

和属性信息一样,包括方法名称、方法返回类型、方法参数类型和数量(按顺序)、方法的修饰符、方法的字节码、操作数栈局部变量表的大小(abstract和native方法除外),异常表(abstract和native方法除外)

  1. 静态变量

这里的静态变量是non-final的类变量,不包含常量。

  1. 运行时常量池

几种在常量池内存储的数据类型包括:数量值、字符串值、类引用、字段引用、方法引用。常量也就方法这里面的


方法区的GC

方法去中是由GC的,虽然GC的效果不明显,但是GC又是有必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量不再使用的类型
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。


方法区、堆、栈的关系

图示:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

直接内存

概念

直接内存不是《Java虚拟机规范》的一部分,在Java堆外的、直接向系统申请的内存区间。
通过存在堆中的DirectByteBuffer操作Native内存,访问直接内存的速度会优于Java堆。即读写性能高。
使用场合:

  • 因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
  • Java的NIO库允许Java程序使用直接内存,用于数据缓冲区



相关信息:

  • 也可能导致OutOfMemoryError异常
  • 如果不指定,默认与堆的最大值-Xmx参数值一致

Java内存 = Java堆内存+本地内存


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值