您的位置:首页 > 编程语言 > Java开发

深入理解java虚拟机读书笔记(二)-内存管理机制

2017-01-06 15:37 232 查看
一、前沿

二、第一部分-走进java

三、第二部分-走进内存管理机制

1、java内存区域与内存溢出的异常

2、垃圾收集器与内存分配策略 

3、虚拟机性能监控与内存分配策略       

四、第三部分-虚拟机执行子系统

五、程序编译与代码优化

六、高效并发

                                                     二、第二部分-走进内存管理机制

2.1Java内存区域与内存溢出异常

java有了虚拟机就不会那么容易出现内存泄漏和内存溢出,但不可能杜绝这种情况,所以一旦出现这种情况了解怎样处理非常有必要了解java虚拟机。如图java运行时内存区域划分。



内存划分不同的区域有着不同的用途以及创建和销毁时间下面具体举例说一下不同区域的作用:

1):程序计数器:(Program Counter Register)是一块较小的内存空间,它可以看作是当前线

程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、 循环、 跳转、 异常处理、 线程恢复等基础功能都需要依赖这个计数器来完成。java的多线程是快速切换线程任务依此获得cpu执行,对于单核的处理器一个时刻只能执行一条指令,所以执行结束要切换回来为了保证之前的指令正确继续执行,每条线程都有一个独立的程序计数器,称之为线程私有内存区。如果正在执行java方法,程序计数器执行的是虚拟机字节码指令的地址。如果是本地native方法,则计数器值为空,此区域是唯一一个java虚拟机中没有规定OutOfMemoryError情况的区域。
2):Java虚拟机栈:(Java Virtual Machine Stacks)也是线程私有,生命周期与线程相同,他描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、 操作数栈、 动态链接、 方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。一般java内存划分为堆(Heap)和栈(Stack),粗糙的划分涉及最密切的两个区域,栈就是虚拟机栈或者说是虚拟机栈中局部变量的表示部分。局部变量表存放的是各种基本类型、引用类型(不等同于本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。long和double数据占用2个局部变量空间,其余只占一个,他们所需的内存空间在编译时期完成分配。在方法运行期间不会改变局部变量表的大小。关于本区域的两种异常:1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;2:、如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3)本地方法区:(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 在虚拟机规范中对本地方法栈中方法使用的语言、 使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
甚至有的虚拟机直接就把本地方法栈和虚拟机栈合二为一。 与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

4)java堆:java虚拟机管理的最大的一块内存,被所有线程共享的一块内存,虚拟机启动时创建。它就是用来存放对象实例(规范写道:所有的对象实例以及数组都分配在堆上)不过随着JIT编译器的发展与逃逸技术的成熟,不是那么绝对的区域了。现在得垃圾收集器都是基于分代收集算法,堆是垃圾回收的主要区域,java堆又分为新生代和老年代;再细致一点的有Eden空间、 From Survivor空间、 To Survivor空间等。进一步的划分是为了更好地回收或者是更快的分配与存放内容无关。



5)方法区(Non-Heap):(Method Area)和堆一样是线程共享的区域,用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译后的代码数据。这部分区区域在HotSpot虚拟上会和永久代联系在一起。但并不等同,垃圾回收一般不会触及到这部分区域但是这部分的回收还是很有必要一般针对常量池和对类型的卸载。关于内存溢出,当方法区无法满足内存分配的需求时将抛出OutOfMemoryError异常。
6)运行池常量:(Runtime Constant Pool)是方法区的一部分Class文件中除了有类的本、字段、 方法、 接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行池常量池的一个特征是动态性,并非预置的才能进入,运行期间也有可能进入新的常量(String类的intern()方法)也会出现无法满足申请时的OutOfMemoryError异常。
7)直接内存:(Direct
Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域但是这部分内存也被频繁地使用而且也可能导OutOfMemoryError异常出现。

对象的创建:(HotSpot虚拟机上探讨)
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。 如果没有,那必须先执行相应的类加载过程。类加载完后就是内存划分,内存的划分又涉及到内存是否规整,内存是否规整又和虚拟机采用的垃圾收集器是否带有压缩整理功能有关:所以对象的分配两种方式1:、指针碰撞(内存规整,即内存中用过的放一边没用的放另一边中间放一些指针作为作为分界点指示器),分配就是指针移动画出与对象大小内存一样的空间。2、空闲列表(内存不规整)这种方式虚拟机要建立一个维护列表记录哪些可用哪些不可用。在使用Serial、
ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
划分时又涉及到安全问题,即同步时一个对象划分指正没有完成,另一个对象也来用该指针划分。解决有1、对内存分配进行同步处理保证原子性操作。2、把分配动作按照线程划分到不同的空间中进行。即每个线程在java堆中预先分配一小块的内存区域成为线程分配缓冲(TLAB)。
内存分配完成虚拟机进行内存空间的初始化零值,保证对象的实例在java代码中不赋值就能直接使用。
接下来是对象的一些设置如这个对象是哪个类的实例、 如何才能找到类的元数据信息、 对象的哈希码、 对象的GC分代年龄等信息。 这些信息存放在对象的对象头(Object Header)之中。 根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。从虚拟机层面讲对象的创建已经完成但是还没用执行<init>方法,此方法的执行才是按照程序员的意愿进行对象的初始化。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: