一、JAVA运行时的数据区
- 程序计数器:可以看做是当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的执行位置 线程私有 除了此区域其他区域都会发生OutOfMemoryError
- JAVA虚拟机栈 线程私有 生命周期与线程相同 每个方法执行的时候都会创建一个栈帧用于存储局部变量表(基本数据类型、对象引用;局部变量表所需的内存空间在编译期完成分配,在方法运行期间不会改变局部变量表的大小)、操作栈、动态链接、方法出口。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。在这个区域存在两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常
- 本地方法栈 与虚拟机栈类似 只不过是为native方法服务
- Java堆 被所有线程共享的一块内存区域 可以处于物理上不连续而逻辑上连续的内存空间
- 方法区(Permanent Generation) 各个线程共享的区域 存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码 也是堆的一部分但常被称为非堆用以区分 该区域可以不选择进行垃圾回收 所以不用担心static变量的回收问题 这个区域的内存回收目标主要是针对常量池的回收和类型的卸载(执行的条件相当苛刻)
- 运行时常量池 是方法区的一部分 Class文件中的常量池,用于存放编译期生成的各种字面量和符号引用,在类加载后存放到方法区的运行时常量池 相对于Class文件常量池的一个重要特性是具有动态性可以在运行期间将新的常量放入池中 String 的intern()方法
- 直接内存 并非虚拟机运行时的数据区
方法区解密:
● 该类型的常量池
● 字段信息
● 方法信息
● 除了常量以外的所有类(静态)变量
● 一个到类ClassLoader的引用
● 一个到Class类的引用
常量池
虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量(final)和对其他类型、字段和方法的符号引用(注意是符号引用!)。池中的数据项就像数组一样是通过索引访问的。因为常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在Java程序的动态连接中起着核心的作用。
字段信息
对于类型中声明的每一个字段。方法区中必须保存下面的信息。除此之外,这些字段在类或者接口中的声明顺序也必须保存。
○ 字段名
○ 字段的类型
○ 字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)
方法信息
对于类型中声明的每一个方法,方法区中必须保存下面的信息。和字段一样,这些方法在类或者接口中的声明顺序也必须保存。
○ 方法名
○ 方法的返回类型(或void)
○ 方法参数的数量和类型(按声明顺序)
○ 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)
除了上面清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下列信息:
○ 方法的字节码(bytecodes)
○ 操作数栈和该方法的栈帧中的局部变量区的大小
○ 异常表
类(静态)变量(static变量)
类变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。这些变量只与类有关——而非类的实例,因此它们总是作为类型信息的一部分而存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。
而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为常量池或字节码流的一部分,编译时常量保存在方法区中——就和一般的类变量一样。但是当一般的类变量作为声明它们的类型的一部分数据而保存的时候,编译时常量作为使用它们的类型的一部分而保存。
指向ClassLoader类的引用
每个类型被装载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是作为方法表中的类型数据的一部分保存的。
虚拟机会在动态连接期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的方式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的。
指向Class类的引用
对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。
在Java程序中,你可以得到并使用指向Class对象的引用。Class类中的一个静态方法可以让用户得到任何已装载的类的Class实例的引用。
public static Class<?> forName(String className)
比如,如果调用forName("java.lang.Object"),那么将得到一个代表java.lang.Object的Class对象的引用。可以使用forName()来得到代表任何包中任何类型的Class对象的引用,只要这个类型可以被(或者已经被)装载到当前命名空间中。如果虚拟机无法把请求的类型装载到当前命名空间,那么会抛出ClassNotFoundException异常。
另一个得到Class对象引用的方法是,可以调用任何对象引用的getClass()方法。这个方法被来自Object类本身的所有对象继承:
public final native Class<?> getClass();
比如,如果你有一个到java.lang.Integer类的对象的引用,那么你只需简单地调用Integer对象引用的getClass()方法,就可以得到表示java.lang.Integer类的Class对象。
二、最简单的访问也会涉及到JAVA堆 JAVA栈 JAVA方法区
Object obj=new Object();
obj存放在栈中,对象的实例数据(new Object)存放在堆中,而对象类型数据(接口 父类)的地址信息 存放在方法区中(堆的一部分)
引用访问对象有两种方式:通过句柄访问对象(reference中存储的是稳定的句柄地址,对象被移动时无需修改)和通过直接指针访问对象(操作速度快,节省了一次指针定位的时间开销) Sun HotSpot 使用的就是第二种方式进行的对象访问。
三、JVM参数配置
-Xms:堆的最小值初始值
-Xmx:堆的最大值 Ps:最大值和最小值设置为相同时,可以避免堆自动扩展
-Xss:栈的容量
-XX:PermSize:方法区的大小
四、不同运行区域的OutOfMemoryError
1.java堆溢出 -Xms -Xmx
有两种情况:内存泄漏(无用但不可清理导致内存不够)和内存溢出(都有用,但内存不够)
2.虚拟机栈溢出 -Xss
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常 //单个线程下,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常 Ps:一般发生于多线程
1 /** 2 *VM Args: -Xss2M 3 */ 4 public class JavaVMStackOOM { 5 private void dontStop(){ 6 while(true){ 7 } 8 } 9 public void stackLeakByThread(){ 10 while(true){ 11 Thread thread = new Thread(new Runnable(){ 12 @Override 13 public void run(){ 14 dontStop(); 15 } 16 }); 17 thread.start(); 18 } 19 } 20 public static void main(String[] args) throws Throwable{ 21 JavaVMStackOOM oom = new JavaVMStackOOM(); 22 oom.stackLeakByThread(); 23 } 24 }
3.运行时常量池溢出 -XX:PermSize
4.方法区溢出
反射动态代理,这个区域测试的思路是,运行时产生大量的类去填充方法区,直到溢出 Why??
5.本机直接内存溢出