专栏名称: java一日一条
主要是讲解编程语言java,并且每天都推送一条关于java编程语言的信息
目录
相关文章推荐
芋道源码  ·  分享一次 ShardingJDBC ... ·  12 小时前  
芋道源码  ·  很抱歉,考虑停更了,死磕AI暴利项目! ·  昨天  
芋道源码  ·  Spring-Security ... ·  2 天前  
51好读  ›  专栏  ›  java一日一条

JVM初探 -JVM内存模型

java一日一条  · 公众号  · Java  · 2018-04-19 18:38

正文

请到「今天看啥」查看全文


如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出 OutOfMemoryError:Java Heap Space 异常。

老年代(Old Generation)

老年代 用于存放在年轻代中经多次垃圾回收仍然存活的对象,可以理解为比较老一点的对象,例如缓存对象;新建的对象也有可能在老年代上直接分配内存,这主要有两种情况:一种为大对象,可以通过启动参数设置 -XX:PretenureSizeThreshold=1024 ,表示超过多大时就不在年轻代分配,而是直接在老年代分配。此参数在年轻代采用 Parallel Scavenge GC 时无效,因为其会根据运行情况自己决定什么对象直接在老年代上分配内存;另一种为大的数组对象,且数组对象中无引用外部对象。

当老年代满了的时候就需要对老年代进行垃圾回收,老年代的垃圾回收称作 Major GC (也称作 Full GC )。

老年代所占用的内存大小为-Xmx对应的值减去-Xmn对应的值。

2.3 本地方法栈(Native Method Stack)

本地方法栈 用于支持native方法的执行,存储了每个native方法调用的状态。本地方法栈和虚拟机方法栈运行机制一致,它们唯一的区别就是,虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

2.4 程序计数器(Program Counter Register)

程序计数器 是一个比较小的内存区域,可能是CPU寄存器或者操作系统内存,其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。

2.5 虚拟机栈(JVM Stack)

虚拟机栈 占用的是操作系统内存,每个线程都对应着一个虚拟机栈,它是线程私有的,而且分配非常高效。一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出 StatckOverFlowError (栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出 OutOfMemoryError (内存溢出)。

2.6 Java对象访问方式

一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。以最简单的本地变量引用: Object objRef = new Object() 为例:

  1. Object objRef 表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;

  2. new Object()作为实例对象数据存储在堆中;

  3. 堆中还记录了能够查询到此Object对象的类型数据(接口、方法、field、对象类型等)的地址,实际的数据则存储在方法区中;

在Java虚拟机规范中,只规定了指向对象的引用,对于通过reference类型引用访问具体对象的方式并未做规定,不过目前主流的实现方式主要有两种:

2.6.1 通过句柄访问

通过句柄访问的实现方式中,JVM堆中会划分单独一块内存区域作为句柄池,句柄池中存储了对象实例数据(在堆中)和对象类型数据(在方法区中)的指针。这种实现方法由于用句柄表示地址,因此十分稳定。

2.6.2 通过直接指针访问

通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。

三、JVM内存分配

Java对象所占用的内存主要在堆上实现,因为堆是线程共享的,因此在堆上分配内存时需要进行加锁,这就导致了创建对象的开销比较大。当堆上空间不足时,会出发GC,如果GC后空间仍然不足,则会抛出OutOfMemory异常。

为了提升内存分配效率,在年轻代的Eden区HotSpot虚拟机使用了两种技术来加快内存分配 ,分别是 bump-the-pointer TLAB(Thread-Local Allocation Buffers) 。由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的, 它会为每个新创建的线程在新生代的Eden Space上分配一块独立的空间,这块空间称为TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行情况计算而得。可通过 -XX:TLABWasteTargetPercent 来设置其可占用的Eden Space的百分比,默认是1%。在TLAB上分配内存不需要加锁,一般JVM会优先在TLAB上分配内存,如果对象过大或者TLAB空间已经用完,则仍然在堆上进行分配。因此,在编写程序时,多个小对象比大的对象分配起来效率更高。可在启动参数上增加 -XX:+PrintTLAB 来查看TLAB空间的使用情况。







请到「今天看啥」查看全文