一.基本概念
JDK:Java程序设计语言,Java虚拟机,java API类库;
JRE:Java API 类库中的Java SE API子集,Java虚拟机;
Java技术体系分为四个平台:
- Java Card:运行在小设备(如智能卡)上的平台;
- Java ME(Micro Edition):支持Java程序运行在移动终端(手机,PDA)上的平台,对Java API有所精简,并加入针对移动终端的支持,以前称为J2ME;
- Java SE(Standard Edition):支持面向桌面级应用(如Windows下的应用程序)的Java平台,提供完整的Java核心API,以前称为J2EE;
- Java EE(Enterprise Edition):支持使用多层架构的企业应用(如ERP,CRM应用)的Java平台,除了提供Java SE API外,还对其做了大量的扩充并提供了相关的部署支持,以前称为Java EE。
Java版本的更新:
- V1.1:Jar文件格式,JDBC,JavaBean,RMI,内部类,反射;
- V1.2:技术体系拆分:面向桌面应用开发的J2SE,面向企业级开发的J2EE,面向手机等移动终端开发的J2ME;
- V1.4:正则表达式,异常链,NIO,日志类,XML解释器和XSLT转换器;
- V1.5:语法易用性做出了很大的改进:自动装箱,泛型,动态注解,枚举,可变长参数,遍历循环等语法特性;
- V1.6:提供动态语言支持,提供编译API和微型HTTP服务器API等;Java虚拟机内部做了大量改进,包括锁与同步,垃圾收集,类加载等方面的算法都有相当多的改动;
二.Java内存区域与内存溢出异常
Java虚拟机运行时的内存模型如下:
1.程序计数器
- 当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令;
- 线程私有的内存:每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响;
- 执行Java方法时,记录正在执行的虚拟机字节码指令地址;执行native方法时值为空(Undefine);
- Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域;
2.虚拟机栈
- 线程私有的,生命周期和线程相同;
- 描述了Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息;方法执行完毕后,栈帧出栈;
- 栈通常描述为虚拟机栈,或者说是虚拟机栈中局表变量表部分;
- 局部变量表存放编译器可知的各种基础数据类型,对象引用(reference类型,可能是指向对象起始地址的引用指针,或者指向一个代表对象的句柄,或者其他与对象相关的位置)和returnAddress类型(指向一条字节码指令的地址);
- 64位长度的long和double类型数据占用2个局部变量空间,其余的数据傀类型占用1个。局部变量表所需要的内存控件在编译期间完成分配,所以当进入方法时,需要在帧中分配多大的局部变量空间是完全确定的,在运行期间不会改变局部变量表的大小;
- *Error:线程请求的栈深度大于虚拟机所允许的深度;
- OutOfMemoryError:动态扩展时无法申请到足够的内存;
3.本地方法栈
与虚拟机的工作原理和作用相似,区别是虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。
4.Java堆
- 被所有线程共享的一块内存区域,在虚拟机启动时创建;(随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致不是所有的对象都在堆上分配)
- 现在的收集器基本上都是采用分代收集算法;
- Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以;
- 如果堆中没有内存完成实例分配,并且堆也无法在扩展,抛出OutOfMemoryError异常;
5.方法区
- 线程共享;
- 用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据;
- 虚拟机规范将方法区描述为堆的一个逻辑部分,别名为Non-Heap(非堆),用于区分Java的堆;
6.运行时常量池
- 方法区的一部分(class文件中还包含类版本信息,字段,方法,接口等描述信息);
- 用于存放编译期生成的各种字面量和符号引用,在类加载后进入方法区的运行时常量池;
- 并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,如String类的intern() 方法。
- OutOfMemoryError:无法再申请到内存;
String.intern()是一个Native方法,作用是:如果字符串常量池中包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String的对象引用。
7.直接内存
本机直接内存的分配不会收到Java堆大小的限制,受本机总内存(包含RAM以及SWAP区或者分页文件) 大小和处理器寻址空间的限制。
8.对象的创建
第一步:遇到new指令时,虚拟机先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。Java堆分配内存划分区间分为两种:
- 绝对规整的“指针碰撞”:所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。分配内存只是将那个指针向空闲空间那边挪动一段与对象大小相等的距离。
- 不规整的“空闲列表”:虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
选择哪种方式由Java堆是否规整决定,Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
第二步:并发情况下的分配方案:
- 对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
第三步:分配完内存后,虚拟机需要将分配的内存空间都初始化为零值,如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。
第四步:对象头信息:虚拟机对对象进行必要的设置,如对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄信息。
第五步:执行<init>方法,把对象按照程序员的意愿进行初始化。
9.对象的内存布局
HotSpot虚拟机中,对象在内存中存储的布局分别3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
对象头:运行时数据+类型指针
(1)一部分存储对象自身的运行时数据,如哈希值,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等等,为非固定的数据结构以便在小的内存空间存储尽量多的信息,会根据对象的状态复用自己的存储空间;
(2)另一部分是类型指针,即对象指向它的类元数据的指针。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,就是说查找对象元数据信息不一定要经过对象本身。当对象是一个Java数组,在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
实例数据:对象真正存储的有效信息,存储顺序受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。分配策略体现为相同宽度的字段总是被分配到一起。
对齐填充:占位符作用,自动内存管理系统要求对象的起始地址必须为8的倍数,也就是说对象内存大小也为8的倍数;
10.对象的访问定位
通过栈上的reference数据来操作堆上的具体对象。reference类型只规定了一个指向对象的引用, 并没有定义通过何种方式去定位、访问堆中的对象的具体位置,所以对象的访问方式取决于虚拟机的实现而定。目前主流的访问方式有使用句柄和直接指针两种。
(1)句柄访问
Java堆中划分一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了实例对象数据与对象类型数据各自具体的地址信息。最大的好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
(2)直接指针
Java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,而reference中存储的是对象地址。最大的好处是速度更快,节省了一次指针定位的时间开销。
11.常见的异常和触发方式
(1)Java堆溢出:java.lang.OutOfMemoryError: Java heap space,触发方式为创建list,然后不断加入对象;
(2)虚拟机栈和本地方法栈:java.lang.*Error
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常;
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常;
(3)方法区和运行时常量池溢出:java.lang.OutOfMemoryError: PermGen space,触发方式为使用String.intern()方法,不断添加到list中;
- 常量池的首次出现原则:如果常量池中已经出现,在返回常量池的对象引用;如果常量池中首次出现,返回对象被本身的引用;
- 溢出异常常见于:使用CGLib字节码增强和动态语言,大量JSP或动态产生JSP文件的应用,基于OSGi的应用(被不同的加载器加载属于不同的类)
(4)本机直接内存溢出:发生于申请分配内存的方法unsafe.allocateMemory()
三. 垃圾收集器与内存分配策略
程序计数器,虚拟机栈,本地方法栈三个区域随线程而生,随线程而灭,栈中的栈帧随方法的进入和退出有条不紊地执行出入栈操作,每个栈帧中分配的内存基本上是在类结构确定下来时就已知的。当方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆和方法区的内存,一个接口中的多个实现或者一个方法的多个分支,所需要的内存可能是不一样的,只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分的内容。
1.可达性分析算法
通过一系列的称为“GC Roots”对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象是不可用的。
Java语言中,可作为GC Roots的对象包含以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 本地方法栈中JNI(即一般说的Native方法)引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
2.引用类型
(1)强引用
例如Object object = new Object(),只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
(2)软引用
用于描述一些还有用但并非必需的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。JDK 1.2之后,提供了SoftReference类来实现软引用。
(3)弱引用
被弱引用关联的对象只能生存在下一次垃圾收集器发生之前。JDK 1.2之后,提供了WeakReference类来实现弱引用;
(4)虚引用
一个对象是否存在虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例,为对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知。JDK 1.2之后,提供了PhantomReference类来实现虚引用。
3.回收方法区
Java虚拟机规范不要求虚拟机在方法区实现垃圾收集,而且方法区中进行垃圾回收的性价比很低,常规应用进行一次的垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分:废弃常量和无用的类。废弃常量的回收和回收Java堆的对象非常相似。判定是否为“无用的类”条件相对苛刻,需要同时满足下面3个条件:
- 该类所有的实例都已经被回收,Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法;
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备卸载的功能,以保证永久代不会溢出。