深入理解JVM虚拟机-内存模型和对象创建

一.内存模型

        运行时数据区域主要分为程序计数器、虚拟机栈、本地方法栈、堆、方法区(方法区中有运行时常量池区别于Class文件常量池)

        程序计数器:当前线程所执行的字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令,是线程私有的,因为多线程在单CPU上是通过线程切换实现并发的,只有线程私有才能保证切换后恢复原有的执行位置,如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的字节码指令的地址,如果正在执行的是Native方法,这个计数器的值为空,该区域是Java运行时数据区唯一不需要考虑OutOfMemoryError情况的区域。

       Java虚拟机栈:线程私有的,生命周期与线程相同,描述的是Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接和方法出口等信息,该内存区域可能会出现StackOverflowError和OutOfMemoryError错误:

       局部变量表:主要是存储方法中的局部变量,包括方法中局部变量的信息和方法的参数。如:各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址),其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

          操作数栈:虚拟机把操作数栈作为它的工作区,程序中的所有计算过程都是在借助于操作数栈来完成的,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

          动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用(指向运行时常量池:在方法执行的过程中有可能需要用到类中的常量),持有这个引用是为了支持方法调用过程中的动态连接

          方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

          附加信息:虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

       本地方法栈:java虚拟机栈为Java方法(字节码)服务,本地方法栈为native方法服务,也可能会抛出StackOverflowError和OutOfMemoryError异常

        Java堆:几乎所有的对象实例都会在堆中分配,但是栈上分配和标量替换使得对象可以不再堆上分配,比如Server端默认开启栈上分配,方法中小的对象在栈中分配,分配不下到eden的TLAB(在堆中,线程私有)分配,然后是eden(这里可能会考虑同步),特别大直接分配再老生带中,从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer),Java堆可以处于物理上不连续的内存,只要逻辑上连续即可,如果堆中没有完成内存分配,并且堆也无法动态扩展时,将会抛出OutOfMemoryError异常。

       方法区:线程共享的区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,这部分的回收主要是针对常量池的回收和类型的卸载,Class文件(常量池)中除了有类的版本、方法、字段、接口等描述信息外,还有一项是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池,不过并非置入Class文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,例如String的intern()方法返回运行时常量池中字符串的地址,有OutOfMemoryError异常

直接内存:直接内存并不是运行时数据区的一部分,有OutOfMemoryError异常,例如JDK 1.4中引入了一种基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了Native堆和Java堆来回复制数据。

二.对象的创建

        推荐查看:https://blog.csdn.net/justloveyou_/article/details/72466416

        从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的

       虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化,如果没有则执行相应的类加载过程,在类加载检查通过后,接下来为对象分配空间,对象空间大小在类加载后就是确定的,主要通过"指针碰撞"和"空闲列表",整理算法一般使用"指针碰撞",清理算法一般使用"空闲列表",为了保证并发情况下内存空间分配的线程安全,一是通过对分配内存空间的动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性,另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Loacl Allocation Bufferr),线程在各自的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定,内存分配后,虚拟机将分配到的内存空间都初始化为零值(除了对象头),接着对象创建刚刚开始-<init>方法还没有执行,所有字段都还为零,执行完<init>方法(实例变量初始化会在<init>中)后,一个真正可用的对象才完全产生出来。

      对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对象填充,Hotspot的对象头包含两部分信息,一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄等,另一部分存储类型指针,即对象指向它的类元数据的指针,实例数据主要存储真实的有效信息,对象填充不是必须存在的,虽然对象头是8字节的整数倍,但是实例数据不一定是,所以对象填充用于补齐实例数据使得整体是8字节的整数倍。

       栈中引用reference访问堆中对象的时候,主要通过句柄或者直接指针访问,如果使用句柄访问的话,Java堆中将会划分出一块内存作为句柄池,reference中存放的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如果使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。使用句柄访问的好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针就是访问更快,节省了一次指针定位的开销。

Java堆的内存溢出、虚拟机栈和本地方法栈的内存溢出、方法区和运行时常量池的溢出、直接内存的溢出

-Xms -Xmx

-Xss -Xoss

-XX:PerSize=xx  -XX:MaxPerSize=xx

-XX:MaxDirectMemorySize

内存泄漏或者内存溢出通过Run或者Debug Cinfiguration配置,然后使用Eclipse Analyzer进行查看饼状图及详细信息

猜你喜欢

转载自blog.csdn.net/qq_27378875/article/details/81180348
今日推荐