三、自动内存管理机制---Java内存区域

本文参考资料:
深入理解JAVA虚拟机:JVM高级特性与最佳实践/周志明著
JAVA虚拟机精讲/高翔龙编著
 
如有描述不正确之处,欢迎大家指出
共勉

或许我们曾经听过“内存泄漏”这个名词,我当时了解到所谓内存泄漏就是因为开发人员大量使用new、malloc来申请空间但是却没有为申请到手的空间进行delete、free对应的释放空间操作,从而导致大量内存空间被使用完之后还处于一种占用的状态,而无法再次被使用的情况。

这样的内存泄露当然要尽量避免,我想。

然而其实对于Java程序开发者来说,我们并不需要去手动释放内存空间,因为我们有JVM来进行内存管理。

我们在接下来的几篇博客中会了解一下Java虚拟机是如何进行内存管理。

本篇主要是对运行时数据区各部分的学习。


首先,我们需要有的基础是Java虚拟机运行时的数据区(图来自博客园用户逝与、):


  • 程序计数器

       程序计数器是一块较小的内存空间,其可以看作是当前线程所执行的字节码的行号指示器。

       在虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成。

       在多线程情况下,由于虚拟机是通过线程轮流切换并分配处理器执行时间的方式,所以任何一个时刻,一个CPU只能执行一条线程中的指令。为了线程切换之后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,各条线程的计数器互不影响。我们称这类内存区域为“线程私有”的内存。

        在上述各个线程私有的程序计数器的内存区域,是不会有OutOfMemoryError的情况的。


  • Java虚拟机栈

      虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

       每一个方法从被调用到执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

       特别提一下栈帧中的局部变量表部分。局部变量表存放了编译期可知的各种基本数据类型、对象引用、和returnAddress类型。另外,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,且在运行期间不会改变局部变量表的大小。

      Java虚拟机栈的异常可能有:StackOverFlowError 和  OutOfMemoryError。

     其中StackOverFlowError是因为线程请求的栈深度大于虚拟机所允许的深度。

     OutOfMemoryError是因为虚拟机栈动态扩展时无法申请到足够的内存。

 

    下面是对每个线程在Java虚拟机栈中的结构表现(图源自优快云用户薛定谔的鸡):


  • 本地方法栈

      本地方法栈与Java虚拟机栈的功能十分相似。区别在于Java虚拟机栈执行的方法是Java方法,而本地方法栈则执行的是Native方法。

      本地方法栈同样也会有StackOverFlowError 和 OutOfMemoryError错误。


  • Java堆

       Java堆的作用是存放对象实例。

      它是被所有线程共享的一块内存区域,也是Java虚拟机所管理的内存中最大的一条。

      它在虚拟机启动时被创建。

      Java堆也是垃圾收集器管理的主要区域,因为几乎所有的对象实例都存放在此处,自然回收也是从这里回收。因此Java堆也被称为GC堆。从内存回收的角度看,收集器基本都采用分代收集算法,所以可以分为新生代和老年代。从内存分配的角度看,可分为线程私有和分配缓冲区。但是无论怎样,堆中存放的内容都是对象实例。

     此处会有的异常有:OutOfMemoryError 原因是堆中没有内存完成实例分配且堆也无法再扩展。


  • 方法区

     方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

     是线程共享的内存区域。

     方法区和堆一样,内存可以不连续、可以选择固定大小或者可扩展,除此之外,还可以选择不实现垃圾收集。但也不是说进入这个区域的数据就不会被回收而永久存在了,只是方法区的收集行为出现的概率很小。但是也有一些回收是很必要的,比如针对常量池的回收和对类型的卸载。

     在方法区这部分还包括运行时常量池。用于存放编译期间生成的字面量和符号引用,它们将在类加载后存放进常量池。

     方法区的异常有:OutOfMemoryError。因为方法区(包好运行时常量池)都会受到内存的限制,当无法再申请到空间时,就会出现上述错误。


了解完虚拟机运行时数据区的各部分之后,我们再来了解一下直接内存。

注意直接内存并不是运行时数据区的哪个部分,也不属于内存区域。但这我们对直接内存的使用也很频繁,也可能会导致OutOfMemoryError错误出现。所以我们也来了解一下。

直接内存

在JDK1.4中加入了NIO类,引入了一种基于通道于缓冲区的I/O方式。这种方式可以使用Native函数直接分配堆外内存,然后通过一个堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样在一些场景能显著提高性能,因为避免了Java堆和Native堆中来回复制数据。


最后,我们来思考一个问题:在Java语言中,对象的访问是如何进行的

例如:Object obj = new Object();

左边声明一个变量的Object obj 将会反映到Java的本地变量表中,作为一个reference类型的数据出现。

右边创建一个对象的new Object 将会反映到堆中,形成一块存储了Object所有的实例数据值的结构化内存。注意根据具体类型和虚拟机实现的对象内存布局的不同,这块内存的大小是不固定的。

另外,在Java堆中要有此对象类型数据的地址信息,这些类型数据存储在方法区中。

 对象的访问就是拿着栈中的reference类型,去堆中访问到对象实例的具体位置。

不同的虚拟机对对象访问方式的实现不同,主流的访问方式有两种:

使用句柄访问方式 和 使用直接指针访问方式。


  • 使用句柄访问

    若使用此方式来访问对象,那么Java堆中将会划分一块内存来作为句柄池。

    reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

   

  • 使用直接指针访问

       若用直接指针访问,那么reference中直接存储对象地址。

      

以上两种对象访问方式各有优缺点。

 reference中的值优点缺点
句柄访问对象的句柄地址reference中存的值稳定,对象被移动时,指挥改变句柄中的实例数据指针,而不改变reference的值。【修改成本低】查找速度较慢,因为要定位两次指针。
直接指针访问对象地址速度较快,因为只需要一次指针定位。对象一旦移动,对象地址就发生了变化,同时就需要修改reference中的值。

 


最后总结整篇博客提到的知识点:

  • Java运行时数据区结构(堆(分为虚拟机栈和本地方法栈)、栈、方法区、程序计数器)
  • 直接内存
  • 对象访问(句柄访问方式 和 直接指针访问)

最后祝大家学习愉快!特别是还没有就业的在校学生,我们一起加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值