1.java运行时数据区如图所示:
2.每个区域的功能
(1)程序计数器(寄存器)
当前线程所执行的字节码的行号指示器
为了线程切换后能够恢复到正确的执行位置,因此每个线程拥有自己独立的程序计数器
如果线程正在执行的是一个java方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址
如果正在执行的是Native方法,这个计数器的值为空
这个区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
(2)java虚拟机栈
生命周期与线程相同
每个方法在执行的同时都会创建一个栈帧用来存储局部变量表、操作数表、方法出口等等。
(3)本地方法栈
源码中,很多的算法或者一个功能的实现,都被java封装到了本地方法中,程序直接通过调用本地的方法就行了,
本地方法就是用来存放这种方法的,可以是c或者c++等其它语言实现。
(4)堆
所有的线程共享这块内存区域,是虚拟机中最大的一块,用来存放对象实例
(5)方法区
所有线程共享这块内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。
3、创建对象,在堆中开辟内存时是如何分配内存的?
两种方式:指针碰撞和空闲列表。我们具体使用的哪一种,就要看我们虚拟机中使用的是什么了。
指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存度放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,所分配内存就仅仅是把哪个指针向空闲空间那边挪动一段与对象大小相等的举例,这种分配方案就叫指针碰撞
空闲列表:有一个列表,其中记录中哪些内存块有用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,然后更新列表中的记录。这就叫做空闲列表
4、对象引用是如何找到我们在堆中的对象实例的?
这个问题也可以称为对象的访问定位问题,也有两种方式。句柄访问和直接指针访问。 画两张图就明白了。
句柄访问:Java堆中会划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息
解释图:在栈中有一个引用变量指向句柄池中一个句柄的地址,这个句柄又包含了两个地址,一个对象实例数据,一个是对象类型数据(这个在方法区中,因为类字节码文件就放在方法区中),
直接指针访问:引用变量中存储的就直接是对象地址了,如图所示
解释:在堆中就不会分句柄池了,直接指向了对象的地址,对象中包含了对象类型数据的地址。
区别:这两种各有各的优势,
使用句柄来访问的最大好处就是引用变量中存储的是稳定的句柄地址,对象被移动(在垃圾收集时移动对象是很普通的行为)时就会改变句柄中实力数据指针,但是引用变量所指向的地址不用改变。
而使用直接指针访问方式最大的好处就是速度更快,节省了一次指针定位的时间开销,但是在对象被移动时,又需要改变引用变量的地址。在我们上面分析的例子中,就是使用的直接指针访问的方式。
5.OutOfMemoryError?
除了程序计数器区不会出现这个错误,其它的几个区都有可能出现这个错误。
(1)java堆溢出:只要不断的创建对象,并且保证GC Roots到对象之间有可达路径,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。
当出现java堆内存溢出时怎么查修?
第一步:通过参数-xx:+HeapDumpOnOutOfMemoeyError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆存储
快照以便事后进行分析。
第二步:通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,确认是不是内存泄漏导致的
第三步:如果是内存泄漏就需要通过与GC Roots的引用链定位到出现内存泄漏的地方,然后解决
第四步:如果不是内存泄漏,就看一下堆内存设置的大小,如果可以调大的话,就继续调大,然后就是从代码层面分析,看是否存在可以缩短对象生命
周期的地方,进行优化
(2)java虚拟机栈和本地方法栈溢出
可能是OutOfMemoryError,也可能是*Error
(3)运行时常量池溢出
可以通过配置修改相应的大小
(4)方法区溢出