《深刻理解Java虚拟机:JVM高级特性与最佳实践(第二版》读书笔记与常见面试题总结java
本节常见面试题:程序员
介绍下Java内存区域(运行时数据区)。面试
对象的访问定位的两种方式。算法
对于Java程序员来讲,在虚拟机自动内存管理机制下,再也不须要像C/C++程序开发程序员这样为内一个new 操做去写对应的delete/free操做,不容易出现内存泄漏和内存溢出问题。正是由于Java程序员把内存控制权利交给Java虚拟机,一旦出现内存泄漏和溢出方面的问题,若是不了解虚拟机是怎样使用内存的,那么排查错误将会是一个很是艰巨的任务。数组
Java虚拟机在执行Java程序的过程当中会把它管理的内存划分红若干个不一样的数据区域。缓存
程序计数器是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都须要依赖这个计数器来完。微信
另外,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。函数
与程序计数器同样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型。布局
Java内存能够粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是如今说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。性能
局部变量表主要存放了编译器可知的各类数据类型、对象引用。
和虚拟机栈所发挥的做用很是类似,区别是: 虚拟机栈为虚拟机执行Java方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
Java虚拟机所管理的内存中最大的一块,Java堆是全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例以及数组都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,所以也被称做GC堆(Garbage Collected Heap).从垃圾回收的角度,因为如今收集器基本都采用分代垃圾收集算法,因此Java堆还能够细分为:新生代和老年代:在细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
方法区与Java堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即便编译器编译后的代码等数据。
HotSpot虚拟机中方法区也常被称为 “永久代”,本质上二者并不等价。仅仅是由于HotSpot虚拟机设计团队用永久代来实现方法区而已,这样HotSpot虚拟机的垃圾收集器就能够像管理Java堆同样管理这部份内存了。可是这并非一个好主意,由于这样更容易遇到内存溢出问题。
相对而言,垃圾收集行为在这个区域是比较出现的,但并不是数据进入方法区后就“永久存在”了。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各类字面量和符号引用)
直接内存并非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用。并且也可能致使OutOfMemoryError异常出现。
JDK1.4中新加入的NIO(New Input/Output)类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的I/O方式,它能够直接使用Native函数库直接分配堆外内存,而后经过一个存储在java堆中的DirectByteBuffer对象做为这块内存的引用进行操做。这样就能在一些场景中显著提升性能,由于避免了在Java堆和Native堆之间来回复制数据。
本机直接内存的分配不会收到Java堆的限制,可是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
经过上面的介绍咱们大概知道了虚拟机的内存状况,下面咱们来详细的了解一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,而且检查这个符号引用表明的类是否已被加载过、解析和初始化过。若是没有,那必须先执行相应的类加载过程。
在类加载检查经过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后即可肯定,为对象分配空间的任务等同于把一块肯定大小的内存从Java堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
虚拟机采用CAS配上失败重试的方式保证更新操做的原子性。
接下来,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。这些信息存放在对象头中,根据虚拟机当前运行状态的不一样,如是否启用偏向锁等,对象头会与不一样的设置方式。
new指令执行完后,再按照程序员的意愿执行init方法后一个真正可用的对象才诞生。
在Hotspot虚拟机中,对象在内存中的布局能够分为3快区域:对象头、实例数据和对齐填充。
Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是那个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各类类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位做用。 由于Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),所以,当对象实例数据部分没有对齐时,就须要经过对齐填充来补全。
创建对象就是为了使用对象,咱们的Java程序经过栈上的reference数据来操做堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:
这两种对象访问方式各有优点。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference自己不须要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
欢迎关注个人微信公众号:"Java面试通关手册"(一个有温度的微信公众号,期待与你共同进步~~~坚持原创,分享美文,分享各类Java学习资源):