JVM之--Java内存结构(第一篇)

最近在和同事朋友聊天的时候,发现一个很让人思考的问题,很多人总觉得JVM将java和操作系统隔离开来,导致很多人不用熟悉操作系统,甚至不用了解JVM本身即可完全掌握Java这一门技术,其实个人的观点是,Java由于有了JVM才使这门语言简单上手,同时也正是因为Java有了JVM才使的Java这门技术很难深入了解。

在C/C++中我们可以很方便的new内存,delete内存,在内存的使用中我们拥有至高的权利,而Java则不行,JVM这一扇大门死死的堵住了内存的操作细节,你无法直接操作内存,所以你能做的就是百分之百的信任JVM给你带来的各种便利都是非常科学和合理的,但是有时候事实并非如此,JVM也不能百分之百的根据你的程序去猜想你所需要的内存资源更谈不上分布情况了,那么JVM能做的就是以他自认为比较合理的方式去为申请,划分内存。

举个最简单的例子,你了解OutOfMemoryError么?笼统来说它就是内存不足引起的,可是他到底是那一块内存溢出所导致的呢?我们只有掌握和了解了JVM的内存划分,才能真的掌握关于内存出现问题的诊断,甚至可以很方便的调优,起到事半功倍的效果,当然也就会让你不再抱怨JVM。

在本文中,我们将重点介绍如下内容:

  • Java的内存划分
  • 各个内存的详解
  • 创建一个对象后的内存分布情况。

第一、Java内存划分

JVM在运行java程序的时候会把内存划分为如下的几部分,如下图所示:


1.1 程序计数器:

首先来说说程序计数器,程序计数器是一个比较小的内存空间,他的作用是什么呢?回想一下CPU的总线结构吧,CPU有三个总线,数据总线,控制总线,地址总线,RAM和CPU交互的时候其实就是逐条的通过一些命令字透过控制总线发送命令,并且将数据通过数据总线进行来回交互,Java在运行时期,其实也是内存在和cpu来回往返的发送各种命令字,并且交换数据,我们的java代码会通过java编译器最终转换成一些底层的命令字(class文件->本地方法将class文件解析转换成标准的命令字)既然是一堆命令字相关的东西,也就存在先运行什么?调用哪个方法,获取那个数据,进行如何的操作等等,在程序计数器中存放的就是这些东西。

我们知道cpu执行的执行时间和分配是由cpu随机或者根据某种cpu的算法规则轮流切换执行某个命令字的,在某一个时刻,他始终只能执行一条命令字,在执行完毕某个命令字之后也需要能够确保回到下一个执行命令字位置的正确性,因此java将这一块内存设计成了私有的,也就是说一个执行的线程都会有一个自己私有的/独享的程序计数器内存空间,该内存空间非常小,另外如果调用的是一个native的方法,则内存计数器不会分配内存空间,并且此内存空间不会出现OutofMemoryError的情况,也是唯一一个。

1.2 虚拟机栈:

虚拟机栈也是线程私有的,每一个方法被执行的时候都会创建一个栈帧,存放在虚拟机栈中,虚拟机栈的结构大致如下所示

每一个方法被调用直到完成的过程,就对应着一个栈帧在虚拟机栈中从如栈到出栈的过程,其中局部变量表就是很多人所说的栈(堆栈地址),他所存放的是基本类型数据和对象的引用类型(reference),其局部变量表中的数据在编译时期就基本上已经确认了,操作栈主要就是压栈或者弹栈,其中动态链接这一部分我个人的理解是动态寻找获取下一个方法的入口地址信息等(个人理解的,有可能不准确)
大多数JVM的虚拟机栈都可以动态扩展的,当无法申请到足够的内存时候会抛出OutOfMemoryError。

1.3 本地方法栈

本地方法栈和虚拟机栈基本上类似,只不过区别是这样的,虚拟机栈是虚拟机本身为java程序开辟的一段内存单元,而本地方法栈是虚拟机调用本地方法时所需要的内存空间。本地方法栈也存在着内存溢出的风险,在SUN提供的JDK中本地方法栈和虚拟机栈合二为一!

1.4 堆

堆在java内存单元中占据着比较大的比重,也是最大的一部分内存单元,在虚拟机启动的时候,该部分的内存就会被创建,所有的对象创建,以及数组内存的申请分配都是在该内存单元上发生的。

由于堆内存所占的比重比较大,因此他也就是java垃圾回收器最关注的一块内存,因此该内存单元也被称为GC堆。如果以后您了解了GC机制,您会知道,Java允许内存单元不连续,只要逻辑上是连续的即可,这部分的内存也是可以进行扩展的,在启动虚拟机时我们可以通过-Xmx,-Xms进行控制,当堆中的内存再也申请不到的时候就会抛出内存溢出的异常,另外该内存空间是线程共享的,我们经常使用到的锁其实就是在这部分内存中活动。

1.5 方法区

方法区也是各个内存的共享区域,用于存放虚拟机的类加载信息,常量,变量,静态变量等数据,每一个方法的执行其实就是在这部分内存中运行,要操作的数据是在虚拟机栈中获取,也可以理解为方法的活动区域。

1.6 运行时常量池

java在编译的时候会将我们定义的final类型做自动的优化存放在常量池中,这样可以提高访问和寻址的速度,因为常量不会再运行期间变化,也就是说他的数据单元地址不会发生改变,一次寻址即可,他其实是方法区的一部分,在android中执行完编译之后除了有class文件还会有一个idx文件,该文件其中的一些数据就是将java文件中常量字面量,这也是为什么一些有经验的人在编写代码的时候非常喜欢用final进行类型的修饰,在方法的参数中,方法体中,类变量中,只要是认为不可变的都进行final声明,试图告诉java虚拟机,这样的变量存放在常量池中,提高寻址速度。

1.7 直接内存

还记得《java NIO》一书中,作者在描述NIO为什么比传统的IO快得原因么?传统的IO进行数据读写操作,首先是Java程序操作java堆中的内存,java堆又拷贝本地方法内存中的数据,java本地方法通过操作系统进行文件的操作,而直接内存就可以不通过java堆和本地方法堆的来回拷贝,而直接操作堆内存以外的内存,这样会节省很多来回数据复制的时间。

2、对象访问

其实上文中的很多内容,也是通过参阅jvm规范以及别人写的jvm得来的,因为它并不像我们进行一个加法运算或者方法调用那样理所当然的去分析,如果碰到很底层的问题或者JVM内部的问题,尽可能的了解到,并且记住他的大概原理,如果要我自己去论证JVM的内存分布是否真的是如此,确实是一个很艰巨的任务,当然借助一些工具也可以看到他们的大概分布情况,在以后的文章中介绍JVM相关工具的时候我们在一起研究探索吧。
了解了上面的知识,为了将概念性的东西,转换为比较直观的东西,我们来看看对象访问的一个过程,并且用图解的方式来说明。
 
一个很简单的Object obj = new Object()其实涉及的内存单元有虚拟机栈内存,堆内存,方法区,程序计数器等。当执行了new方法之后,jvm会在堆内存中开辟一块内存单元存放obj信息,与此同时,obj的类型信息,父类,接口,方法等信息会被存放在方法区中,堆内存为了能够访问到这些信息,除了存放obj的信息之外,还会存放访问这些信息的地址信息。与此同时产生的引用,基本数据类型也会存放到栈内存之中,编译时期生成的命令字也理所当然的存放到了程序计数器之中,如下图所示。

上图所描述的是通过句柄的方式访问方法区,并且给栈内存提供访问方式,下图中将是通过指针的方式访问方法区,提供栈内存访问基本上没有太多的变化,如下图所示。
可以看到在对象实例中就包含了方法区的地址指针,而不用再存在一个句柄空间专门存放,这样当栈访问堆中的引用时,堆就可以直接获取方法区的数据。

好了,本文就大概描述到这里,JVM是一个非常神秘的东西,很多东西我们只有记住的份,因为内存你说了不算,只有他才是操作内存的入口。但是了解他的内存结构,我们就能有效的合理的分配堆栈内存,提高程序运行效率,在下一篇文章中,我们一起设计一些能直接影响到java不同内存的程序,一起分析他们的原因和如何规避,如何调试等。本文中肯定存在很多偏颇之处,希望各位能够直言不讳,写作博客的目的就是为了进步,如果写出来仅此而已,那么意义不大,另为,如果本文中有些欠妥的东西,千万不要以讹传讹。


原文地址:https://www.cnblogs.com/riskyer/p/3237025.html