java内存区域

概述

Java虚拟机在执行Java程序的 过程中会把它所管理的内存划 分为若干个不同的数据区域,通常包括程序计数器、虚拟机栈、本地 方法栈、Java堆、方法区(运行时常量池)、直接内存
在这里插入图片描述

各个区域的作用

-----------------------------线程隔离------------------------------------

程序计数器
  • 占用较小的内存空间,当前线程执行的字节码的行号指示器,各线程之间独立存储,互不影响;
java栈
  • 又称虚拟机栈,线程私有;每个方法在执行时候都会创建一个栈帧用于存储该方法的局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存 放着各种基本数据类型和对象的引用(-Xss) ;
java栈中的存储对象

1.局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大Slot数量)
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了。
虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。
2.操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。
操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
3.动态连接
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
4.方法返回
当一个方法开始执行时,可能有两种方式退出该方法:正常完成出口、异常完成出口
正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。
异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。
无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。
方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。
一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
5.附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧之中,例如和调试相关的信息,这部分信息完全取决于不同的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其他附加信息一起归为一类,称为栈帧信息。

本地方法栈
  • 本地方法栈保存的是native方法的信息, 当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法;

-----------------------------线程共享------------------------------------

  • Java堆是Javaer需要重点关注的一块区域,因为涉及到内存的分配 (new关键字,反射等)与回收(回收算法,收集器等)
方法区
  • 也叫永久区,用于存储已经被虚拟机加载的类信息,常量(“zdy”,"123"等),静态变量(static变量)等数据(-XX:PermSize;- XX:MaxPermSize;-XX:MetaspaceSize; -XX:MaxMetaspaceSize ) 。
运行时常量池
  • 运行时常量池是方法区的一部分,用于存放编译期生成 的各种字面量(“zdy”,"123"等)和符号引用。

各个JDK版本内存区域的变化

  • 在JDK1.6中运行时常量池存在于方法区(永久区)
  • 在JDK1.7中运行时常量池存在于
  • 在JDK1.8中运行时常量池依然存在于堆中,由于永久代(JDK1.7中的方法区)来存储类信息、常量、 静态变量等数据不是个好主意, 很容易遇到内存溢出的问题。而对永久代进行调优是很困难的, 所以将方法区移到了本地内存,更名为元空间,与堆的垃圾回收进行了隔离,避免永久代引发的Full GC和OOM等问题;
    在这里插入图片描述

直接内存

NIO的Buffer提供了一个可以不经过JVM内存直接访问系统物理内存的类——DirectBuffer。 DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制;而DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。
在这里插入图片描述

直接内存
  • 不是虚拟机运行时数据区 的一部分,也不是java虚拟机规范中定义的内存区域;
  • 如果使用了NIO,这块区域会被频繁使用,在java堆内可以用堆栈区directByteBuffer对象直接引用并操作;
  • 这块内存不受java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;

栈和堆的区别

  • 以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
    而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
  • 栈存储的变量是线程私有的,只有当前线程可见;而堆存储的变量是线程共享的,对所有线程均可见
  • 栈的内存远小于堆内存,栈的深度是有限制的,可能发生StackOverFlowError问题

在这里插入图片描述

栈上分配

栈上分配是JVM提供的一种优化技术,其思想可以简单的理解为对于线程私有的对象,将它打散之后在栈上分配内存而不在堆上。好处是对象跟着方法调用自行销毁,不需要进行垃圾回收,可以提高性能。

而栈上分配所依赖的技术是逃逸分析,逃逸分析主要判断对象的作用域是否会会逃出当前方法体,例如函数将对象作为返回值返回,则这个对象是逃逸的。任何可以在多个线程之间共享的对象,一定属于逃逸对象。

启用线上分配
  • -server JVM运行的模式之一, server模式才能进行逃逸分析, JVM运行的模式还有mix/client(client主要用于桌面程序,其加载的速度比server模式快10%,运行速度是server模式的10%)
  • -Xmx10m和-Xms10m:堆的大小
  • -XX:+DoEscapeAnalysis:启用逃逸分析(默认打开)
  • -XX:+EliminateAllocations:标量替换(默认打开)
    对栈上分配发生影响的参数就是三个,-server、-XX:+DoEscapeAnalysis和-XX:+EliminateAllocations,任何一个发生变化都不会发生栈上分配,因为启用逃逸分析和标量替换默认是打开的,所以,一般情况下,JVM的参数只用-server一样可以有栈上替换的效果

使用栈上分配后,假设User对象实例,分配100000000次,启用栈上分配,可能只需5ms,不启用,可能需要需要3S

虚拟机中的对象

对象分配—>当虚拟机遇到一条new指令时

  • 1.先执行相应的类加载过程。
  • 2.接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
    如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞
    如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表
    选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
    除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
    解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
    另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。
    TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。
    TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。
  • 3.内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • 4.接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
  • 5.在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。
如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
如果使用直接指针访问, reference中存储的直接就是对象地址。
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
对Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的。
在这里插入图片描述

JVM内存区域常用的JVM参数

  • -Xss:栈桢大小缺省为1M,可用参数 –Xss调整大小,例如 -Xss256k

  • -Xms 堆的最小值,例如- Xms128m;
    -Xmx 堆的最大值,例如- Xmx256m
    -Xmn 新生代的大小;
    -XX:NewSize 新生代最小值;
    -XX:MaxNewSize 新生代最大值;

方法区/永久代

  • -XX:PermSize jdk1.7及以前用于指定方法区的最小值
    -XX:MaxPermSize jdk1.7及以前用于指定方法区的最大值
    -XX:MetaspaceSize jdk1.8用于指定方法区的最大值,如:-XX:MaxMetaspaceSize=3M
    jdk1.8以后大小就只受本机总内存的限制

直接内存

  • XX:MaxDirectMemorySize 不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;这块内存不受java堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常。

其他:

  • -XX:+PrintGC 打印GC日志
    -XX:+PrintGCDetails 打印GC详细日志
    -XX:+DoEscapeAnalysis 启用逃逸分析(默认打开)
    -XX:+EliminateAllocations 标量替换(默认打开)
    -XX:-UseTLAB 关闭本地线程分配缓冲
    -server 开启server模式
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值