Java技术体系包括以下几个组成部分
- Java程序设计语言
- 各种硬件平台上的Java虚拟机
- Class文件格式
- JavaAPI类库
- 来自商业机构和开源社区的第三方Java类库
Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK(Java Development Kit),JDK是用于支持Java程序开发的最小环境。
Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE(Java Runtime Environment),JRE是支持Java程序运行的标准环境。
图1-2展示了Java技术体系所包含的内容,以及JDK和JRE所涵盖的范围。
根据《Java虚拟机规范(Java SE7版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示:
1.程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅时概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
2.Java虚拟机栈
与程序计数器一样,Java虚拟机(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。
3.本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用时非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(特就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用的方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
4.Java堆
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化法生,所有的对象都分配在对上也渐渐变得不是那么“绝对”了。
5.方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是他却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
5.1运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息时常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
5.2直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓存区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
二. Java虚拟机
2.1对象创建
虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,虚拟机将为新生代分配内存。对象所需大小在类加载完成后便可完全确定。分配空间分为两种方式:
1.Java堆中内存是绝对规整的,所有用过的内存都放在一遍,空闲的放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞(Bump the Pointer)”。
2.如果Java堆中的内存并不是规整的,已使用的你没存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新类表上的记录,这种分配方式称为“空闲列表(Free List)”。
选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而是用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
分配内存时候可能会出现分配冲突,解决冲突的两种方案:
1.对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
2.把内存分配的动作按照线程划分在不用的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
2.2对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头包括两部分:
1.第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和62位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。
2.对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
- 实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
- 对齐填充
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
2.3对象的访问定位
访问对象的两种方式:
1.使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如下图所示
2.如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如下图所示:
使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。Sun HotSpot使用的就是指针访问方式。
package com.life.circle.base;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
public class OutOfMemoryTest {
public static void main(String[] args) throws Exception {
// heapOutMemory();
OutOfMemoryTest outOfMemoryTest = new OutOfMemoryTest();
// try {
// outOfMemoryTest.stackLeak();
// }catch(Exception e) {
// System.out.println("stackLength=" + outOfMemoryTest.stackLength);
// throw e;
// }
// outOfMemoryTest.stackLeakByThread();
// outOfMemoryTest.functionOutMemory();
// System.out.println(System.getProperty("java.version"));
// outOfMemoryTest.functionMemory();
// outOfMemoryTest.funtionOutMemoryByClass();
System.out.println(outOfMemoryTest.getClass().getClassLoader().toString());
}
static class OOMObject{
}
/**
* -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:/Development/outMemoryLog/dump2.hprof
* 堆内存溢出
*/
public static void heapOutMemory() {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true) {
list.add(new OOMObject());
}
}
/**
* -Xss128k
*
* 栈的溢出
*/
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
private void dontStop() {
while(true) {}
}
/**
* -Xss2M
*
* 通过创建线程时内存溢出
* */
public void stackLeakByThread() {
while(true) {
Thread th = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
th.start();
}
}
/**
* JDK1.6之前的常量池在永久代中,因此将永久代空间设置小就可以达到内存溢出
* JDK1.7以后常量池从永久代中拿出来,作为方法区的一部分
*
*/
public void functionOutMemory() {
List<String> list = new ArrayList<String>();
int i = 0;
while(true) {
list.add(String.valueOf(i++).intern());
}
}
/**
* 测试JDK1.6 常量池处于永久带中
*/
public void functionMemory() {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
String str3 = new StringBuilder("py").append("thon").toString();
System.out.println(str3.intern() == str3);
}
/**
* 通过动态的创建类使方法区内存溢出
* -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public void funtionOutMemoryByClass() {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2);
}
});
enhancer.create();
}
}
/**
* 本地内存溢出
* openjdk会有Unsafe类
*/
public void nativeOutMemory() {
//Field field = Unsafe.class.getDeclaredField("theUnsafe");
}
}