1、程序计数器为很小的内存空间,为当前线程执行的字节码的行号指示器,通过改变计数器的值来选取下一条需要执行的字节码指令,循环、分支等基础功能都是需要计数器来完成的
2、Java虚拟机栈为Java方法执行的内存模型,每个方法被执行时都会同时创建栈帧用于存储局部变量表,操作栈、动态链接、方法出口等信息,方法被执行到结束对应一个栈帧从虚拟机栈入栈出栈
两种异常情况:如果线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,只是无法申请到足够的内存时抛出OutOfMemoryError异常
3、本地方法栈:上面是为Java方法服务,而这个是为虚拟机使用的Native方法服务
4、Java堆:被所有线程共享的内存区域,Java虚拟机管理的最大内存。几乎所有对象实例存放区域,垃圾收集器管理的主要区域,根据分代收集算法,Java堆还可以细分:新生代和老年代;Eden空间、From Survivor空间、To Survivor空间;细分主要为了更好地回收内存和更快分配内存
5、方法区,也是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。不需要连续的内存和可以选择固定大小或者扩展,还可以选择不实现垃圾收集,内存回收目标主要针对常量池的回收和对类型的卸载
6、运行时常量池:方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这些内容将在类加载后方法存放到方法区的运行时常量池,具有动态性,运行期间也可以将新的常量放入池中,当常量池无法申请内存会抛出OutOfMemoryError异常
7、对象访问:这涉及Java栈、Java堆、方法区的关联,比如
Object obj=new Object();
Object obj反映Java栈的本地变量表,作为一个reference类型数据出现。而new Object()反映到Java堆,形成一个存储Object类型所有实例数据值(对象中各个实例字段的数据)的结构化内存,Java堆中还包含此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中
主要的访问方式有两种:使用句柄和直接指针
注意Java堆内部有句柄池(优势:reference中存储的是稳定的句柄地址,对象被移动,reference毫无影响)
指针访问(优势:速度快,节省一次指针定位的时间开销)
实战:OutOfMemoryError异常
1、Java堆溢出
不断创建对象,当对象数量达到最大堆的容量限制就产生内存溢出异常
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list=new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at 深入Java虚拟机.HeapOOM.main(HeapOOM.java:13)
通过内存映像分析根据Memory Analyzar对dump出来的堆转储快照进行分析
2、虚拟机栈和本地方法栈溢出
栈容量由-Xss参数设定,在Java虚拟机描述了两种异常
1、如果线程请求的栈深度大于虚拟机所允许的最大深度,就抛出StackOverflowError异常
2、如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
下面的两种方法的结果都是StackOverflowError异常
(1)使用-Xss参数减少栈内存容量,结果:抛出StackOverflowError异常,异常出现时输出的栈深度相应缩小.
(2)定义大量的本地变量,增加此方法帧中本地变量表的长度,结果:抛出StackOverflowError异常时输出的栈深度相应缩小
public class JavaVMStackSOF {
private int stackLength=1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom=new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Exception e) {
System.out.println("stack length"+oom.stackLength);
throw e;
}
}
}
Exception in thread "main" java.lang.StackOverflowError
表明,在单个线程下,无论是栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机都会抛出StackOverflowError异常
操作系统32位的windows限制为2GB减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),剩下的内存由虚拟机栈和本地方法栈“瓜分”每个线程分配到的栈容量越大,建立的线程数量就越小,建立线程时越容易把剩下的内存耗尽
如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程
3、运行时常量池溢出
使用String.intern()这个native给运行时常量池添加内容,该方法作用:如果已经存在在池中就返回String对象的字符串,没有就将其添加到常量池并且返回该String对象的引用,由于常量池在方法区,可以限制方法区的大小从而间接限制其中常量池的容量
4、方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段、描述、方法描述等,测试思路是运行时产生大量的类去填满方法区
在经常动态生成大量Class的应用中,需要特别注意类的回收状况,常见:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译成Java类)、基于OSGi的应用(同一类文件,被不同的加载器加载也会视为不同的类)