【学习笔记】深入理解Java虚拟机 第一章~第二章
第一章 走近Java
Java技术体系
JDK(Java Development Kit):Java程序设计语言、Java虚拟机、Java API类库。
JRE(Java Runtime Environment):Java API类库的Java SE API子集和Java虚拟机。
JDK是用于支持Java程序开发的最小环境。
JRE是支持Java程序运行的标准环境。
Java ME(Micro Edition):支持Java程序运行在移动终端(手机、PDA)上的平台,对Java API有所精简,并加入了针对移动终端的支持,这个版本以前称为J2ME。
Java SE(Standard Edition):支持面向桌面级应用(如Windows下的应用程序)的Java平台,提供了完整的Java核心API,这个版本以前称为J2SE。
Java EE(Enterprise Edition):支持使用多层架构的企业应用的Java平台,除了提供Java SE API外,还对其做了大量的扩充并提供了相关的部署支持,这个版本以前称为J2EE。
第二章 Java内存区域与内存溢出异常
运行时数据区域
其中方法区和堆是线程共享区,虚拟机栈、本地方法栈、程序计数器是线程独占区。
程序计数器:
是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
Java虚拟机栈:
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。
当进入一个方法时,这个方法在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈:
和虚拟机栈的作用类似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法则为虚拟机使用到的Native方法服务。
Java堆:
对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。此内存区域的唯一目的就是存放对象实例。
Java堆是垃圾收集器管理的主要区域。从内存回收的角度看,现在的收集器基本都采用分代收集算法。
当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OOM异常。
方法区:
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。有一个别名“非堆”,目的是和Java堆区分开来。
对于HosSpot虚拟机,很多人把方法区成为“永久代”,本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机来说是不存在永久代的概念的。
会抛出OOM异常。
运行时常量池:
是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,运行期间也可以,比如String类的intern()方法。
也会抛出OOM异常。
直接内存:
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
也会导致OOM。
HotSpot虚拟机对象探秘
对象的创建:
1.虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载。
2.在类加载通过后,虚拟机为新生对象分配内存。对象所需大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定的大小的内存从Java堆中划分出来。
假设堆中内存是绝对规整的,所有用过的内存都放在一边、空闲的内存放在另一边,中间放一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配称为“指针碰撞”。如果Java堆中的内存不是规整的,已使用的内存和空闲内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录哪些内存块是可用的,这种分配方式称为“空闲列表”。
Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
并发情况不是线程安全的。两种方案:
1)对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
2)把内存分配的动作划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
3.内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。
4.虚拟机对对象进行必要的配置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。
5.在上面工作都完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的角度来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段还为零。执行new指令之后会接着执行<init>方法。
对象的内存布局:
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它“Mark Word”。
另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息。
对象的访问定位
句柄访问:Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址。
好处是reference存储稳定的句柄地址,在对象移动时只会改变句柄中的实例数据指针,而reference不需要改变。
直接指针访问:reference中存储的直接就是对象地址。
好处是速度更快。
实战OOM异常
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。