jvm数据区域主要要5块:方法区(永久区),虚拟机栈(平时说的栈),堆,程序计数器,本地方法栈。
程序计数器:当前线程所执行的字节码的行号指示器,字节码解释器改变计数器来拿取下一条字节码指令,分支,循环等都需要依赖这个来完成,多线程的线程切换都是使用每个线程独立的程序计数器来保证程序正常运行(Native方法运行,程序计数器为空)。
本地方法区:为java字节码方法服务,也就是native方法。
虚拟机栈:线程私有,描述java方法执行的内存模型:每个方法执行都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等,方法执行完成对应一个栈帧在虚拟机栈中入栈到出栈过程。平时说的栈也就是虚拟机栈的变量表部分(局部变量表所需的内存空间在编译期完成分配,在运行期不会改变变量表大小),里面可以存有各种数据类型,引用可以是指向对象的引用,或是指向堆中句柄池,还可以存有returnaddress(指向字节码指令的地址)。
在运行过程:如果线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverFlowError,虚拟机可以动态扩展,若扩展无法申请到足够的空间,将会抛出OutOfMemoryError。
public static class SleepThread implements Runnable{
public void run(){
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]){
for(int i=0;i<1000;i++){
new Thread(new SleepThread(),"Thread"+i).start();
System.out.println("Thread"+i+" created");
}
}
然后设置堆和栈大小 -Xmx1g -Xss1m,就会抛出OutOfMemoryError异常,解决方法:减少堆内存,减少线程栈大小(这样小内存去扩容就不会使jvm内存无法接受)。
堆:分有深堆浅堆(浅堆是当前对像对应变量的大小,深堆对应实际对象回收的大小),存放对象实例(在JIT编译器发展和逃逸分析技术成熟,栈上也是可以分配对象)。这里面分有新生代,老年代:再细致还有Eden区,From Survivor区,To Survivor区。线程共享的堆可划分多个线程私有分配缓存区(TLAB),是为了更好的分配内存。堆可以存在不连续的内存空间,但逻辑上连续,使用-Xmx和-Xms控制,在堆中没能实例分配,且堆也无法扩展时,将会抛出OutOfMemoryError。
public static void main(String args[]){
ArrayList<byte[]> list=new ArrayList<byte[]>();
for(int i=0;i<1024;i++){
list.add(new byte[1024*1024]);
}
}
上面这段代码会抛出OutOfMemoryError异常,解决方法:增大堆空间,释放jvm内存。
永久区(方法区):存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,使用-XX:MaxPermSize控制大小,在jdk1.7hotspot把本来永久代的字符串常量池移出,这个区域内存回收目标是针对常量池的回收和对类型的卸载。当永久区无法满足内存分配需求时,将抛出OutOfMemoryError。
public static void main(String[] args) {
for(int i=0;i<100000;i++){
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
}
}
解决方法:增大perm区或允许class回收。
运行常量池,String类的intern()可在运行时操作,这里也会导致OutOfMemoryError。
直接内存:不属于jvm数据区,但也会导致OutOfMemoryError。NIO使用就是这块区域,使用Native函数库直接分配堆外内存,通过存储在java堆中的DirecByteBuffer对象作为对这块内存的引用进行操作,避免不同内存来回复制数据,受到物理内存影响,还是会出现异常。
for(int i=0;i<1024;i++){
ByteBuffer.allocateDirect(1024*1024);
System.out.println(i);
System.gc();
}
解决方法:减少堆大小,触发GC。
对象分配:使用"指针碰撞","空闲列表"分配,主要由回收器算法所决定,为了解决对象分配的线程安全:一种是CAS配上失败重试的方式保证更新操作的原子性。另一种是按照线程划分在不同空间进行(TLAB),哪个线程需要分配内存,就在哪个TLAB上分配,只有TLAB用完并分配新的TLAB时才需同步锁的,使用-XX:+/-UseTLAB参数设置是否使用TLAB。
对象内存分布:对象头(哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等在32或64位会是对应位数的bit数),实例数据(无论继承或子类各种类型的字段内容,相同位宽会分配到一起),对齐填补(保证对象头正好是8的整数倍)。
对象访问定位:对象类型数据的指针都放在方法区,句柄池和直接引用(减少多一次定位开销)。
jvm内存分析工具:MAT,下载地址http://www.eclipse.org/mat/,里面使用到对象引用图和支配树。