浅谈JVM(一):内存与内存溢出

一、java内存区域

在这里插入图片描述
java内存区域即JVM内存区域,主要分为以上几块,我们一一进行解释:
1、程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。可以说程序计数器是用来指示执行哪条指令的。在多线程的情况下,因为线程是并发执行的,即线程在不断的切换中,为了保证每次线程切换能恢复到之前的执行位置,每个线程都需要有自己的程序计数器,即是线程私有的。
2、虚拟机栈
虚拟机是线程私有的,每个线程在创建时都会创建属于自己的栈帧空间,用于存储局部变量表、操作数栈、动态链接,方法返回地址等信息。事实上,虚拟机栈可以看作时方法执行的内存模型,一次方法的调用就是入栈和出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(64位长度的long和double会占用2个局部变量空间,其他类型变量占用一个),对象引用(并非对象本身,对象本身是存储在堆中,这可能是指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和返回地址类型。注意:局部变量表所需内存空间在编译期间完成分配,是不会改变的。
栈不可能无限存放数据,当请求的栈深度超出栈的容量,就报栈溢出(StackOverflowError)异常。
3、本地方法栈
本地方法栈(Native Method Stack)和虚拟机栈最大的区别就在于,本地方法栈为Native方法服务的。
本地(Native)方法:本地方法是用非java语言写的,但用java调用的接口。因此本地方法它是与平台有关的。
4、堆
一般来说,java堆是JVM所管理的内存中最大的一块。java堆创建的目的就是为了存储对象实例,它也是gc(Garbage Collection)所管理的主要区域。java堆是线程共享的,JVM只有一个堆。
5、方法区
方法区和堆一样,也是线程共享的区域。在方法区中,存储了已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JVM的解释中,方法区被看做是堆的一个逻辑部分,因用分代收集算法可将堆分为新生代和老年代,所以方法区也经常被称作“永久代”。但事实上,方法区和堆是不一样的,应该被区分开来。
运行时常量池:方法区的一部分,用于存放编译期生成的各种字面量和符号引用,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。运行时常量池一个重要的特征就是具备动态性,Java语言并不要求常量一定在编译期产生,在运行时也可产生,如String类的intern()方法。

二、HotSpot虚拟机对象

1、对象的创建
在java语言中,对象的创建就是一个new关键字,但具体虚拟机内部是怎样的一个情况呢?
第一步:虚拟机遇到第一条new指令的时候,首先先在常量池中定位这个指令的参数的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。若没有则执行相应的类加载过程。
第二步:类加载完成后,要进行对象在堆中的内存分配。为对象分配内存是将一块确定大小的内存从堆中分配出来,这需要考虑到堆的内存是否规整,根据是否规整可以考虑两种分配策略。
指针碰撞:在内存绝对规整的情况下,所有用过的内存和空闲的内存全部分开,然后中间用一个指针作为分界指示器,当空闲的内存被分配称为用过的内存,就将该指针所指向的地址转换到新的空闲内存首地址,该过程就称为指针碰撞。
在这里插入图片描述
空闲列表:在内存不规整的情况下,已使用的内存和空闲内存相互交错,虚拟机此时不能单纯用指针的方式了,可以维护一个列表,记录空闲内存块,然后当分配新的内存时,从列表中寻找可用的内存块然后分配,分配成功后再在列表里记录,这个列表就称为“空闲列表”。
至于java堆是否规整,要看垃圾处理器是否拥有整理的能力,具体我们下次在谈gc时再详细说明(笑脸~~)。
除了考虑内存划分外,还需要考虑当修改指针位置的时候,再并发情况下不是线程安全的。可能在给对象A分配内存时,指针还没有修改位置,对象B就开始请求分配内存。针对线程安全我们也是两种处理策略:一是进行线程同步。二把内存划分的动作依据线程在不同的空间进行,即每个线程都在java堆中预先分配一块内存,称为本地线程分配缓冲(TLAB)。哪个线程需要分配内存,就在它的TLAB上进行分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
三、内存分配完成后,虚拟机需要将分配到的内存空间都初始化为0。这一步保证了我们创建新的对象,对象的属性在没有赋初值的情况下可以直接使用。
四、接下来,虚拟机要对对象进行必要的设置,如这个对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的分代年龄等信息。这些信息会存放在对象的对象头中。
2、对象的内存布局
对象在内存中的布局有三部分:对象头、实例数据和对齐填充。
对象头:HotSpot虚拟机的对象头分为两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针知道该对象是哪个类的实例。如果对象是java数组,那对象头中还必须有一块用于记录数组长度的数据。
实例数据:对象存储的有效信息,即代码中所定义的各种类型的字段内容。
对齐填充:该部分并不是必然存在。这部分是考虑到对象的大小必须是8字节的整数倍,因对象头部分确定必须是32位或者64位,即对齐填充主要用于填充不是8字节整数倍的实例数据。
3、对象的定向访问
对象实例是存储在堆中的,而程序需要通过操作栈上的引用数据来操作具体对象。

Object o = new Object();

我们定义了一个Object实例o,其中o会反映为栈的局部变量表中的引用类型,而new Object()会反映到java堆中,形成一块存储了对象实例的内存数据区。其中java栈中的引用实例是如何定位到堆中的具体数据中的呢?目前主流的访问方式有两种:
1、句柄访问。java堆中将会专门划分出一块内存作为句柄池,栈的引用类型存储的就是对象的句柄地址。而句柄中包含了对象实例数据和类型数据各自的地址信息。
在这里插入图片描述
2、直接指针访问。即栈中的引用类型存储的是对象实例的首地址。
在这里插入图片描述句柄访问的最大优势在于当我们需要改变实例数据位置的时候,只需要改变句柄中的实例数据指针,而栈中reference本身不需要修改。
而指针访问的最大优势是它的速度快,因为它就剩了一次指针定位的时间开销。

三、内存溢出(OOM)异常

1、堆溢出
java用于存储对象实例,在不断创建对象的同时,这些对象因到GC Roots之间有可达路径(关于可达性算法在下一篇介绍gc时会讲到)造成对象没有被垃圾回收机制清除,当对象所栈内存达到堆的最大容量的时候就会产生OOM异常。
要解决此问题首先是要判断到底是内存溢出还是内存泄露。首先要分清什么是内存溢出什么是内存泄露。
内存泄露:当我们new了一个对象实例,很长一段时间没有使用它,但它却因为被某个或某些到GC Roots之间有可达性路径的实例所持有而没有被垃圾处理器清理掉,这种情况造成的溢出就被称为内存泄露,即该被释放的对象没有被释放掉。
内存溢出:就是使用的内存超出了其申请内存的大小。
如果是内存泄露,可通过工具查看泄露对象到GC Roots的引用链,就能掌控泄露对象是通过怎样的路径和GC Roots相连,可以比较准确的定位出泄露代码的位置。
如果不是内存泄露,就应该检查堆参数,与机器物理内存对比看是否可以调大,从代码上检查是否存在某些对象使用生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
2、虚拟机栈和本地方法栈溢出
关于这种栈溢出存在两种情况。
1、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
2、如果虚拟机在扩展时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。
其中在单线程的情况下只有可能出现StackOverflowError异常,只有在多线程的情况下才会出现OutOfMemoryError异常。其原因也很好理解,操作系统分配给各个进程的内存是有限的,而各个进程又由线程构成,线程所占用的内存会被堆和方法区、程序计数器(占用内存很小)所瓜分,除开进程本身耗费的内存,剩下的就是栈所占用的内存,所以栈占用的内存是有限的。如果每个线程分配给栈的内存越大,就越容易发生OutOfMemoryError异常。
3、方法区和运行时常量池溢出
方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,其判断条件非常苛刻。在产生大量未被回收的类的情况下,可能发生方法区溢出。所以在动态生成大量Class的应用中,需要特别注意类的回收情况。至于运行时常量池溢出就是加入的常量过多的情况下会发生。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值