1.Java技术体系结构

Java技术体系:广义上,Clojure、JRuby、Groovy等运行于java虚拟机上的语言以及相关的程序都属于java技术体系的一员。sun官方所定义的java技术体系包括以下几个组成部分:

  java程序设计语言、各种平台上的java虚拟机、class文件格式、java api 类库、来自商业机构和开源机构的第三方类库

2. java运行时数据区

2.1 程序计数器(Program Counter Register)

   程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。

    每条线程都有一个独立的程序计数器。

    如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是native方法,计数器为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2.2 java虚拟机栈

     同样是线程私有,描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法对应一个栈帧。

   常有人把java内存分为堆内存(heap)和栈内存(stack)其实是粗糙的,所指的栈就是虚拟机栈,或者是说虚拟机中的局部变量部分

    局部变量表存放了各种基本类型、对象引用和returnAddress类型(指向了一条字节码指令地址)。其中64位长度long 和 double占两个局部变量空间,其他只占一个。

   java虚拟机规范中,规定这个区域的异常情况有两种:1.线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;2.如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常。

   关于OutOfMemoryError参考博客:https://blog.csdn.net/hzy38324/article/details/76719105?utm_source=gold_browser_extension

 2.3 本地方法栈

  本地方法栈(native method stack)与虚拟街栈发挥的作用相似,不同的是本地方法栈为Native方法服务。

2.4 java堆

      是Java虚拟机所管理的内存中最大的一块。由所有线程共享,在虚拟机启动时创建。堆区唯一目的就是存放对象实例。

      堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。在实现中可以扩展大小,(通过-Xmx和Xms控制)

      如果在堆中没有内存完成实例并且堆无法扩展时,抛出OutOfMemoryError异常

2.5 方法区

  所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

      当方法区无法满足内存分配需求时,抛出OutOfMemoryError

2.6 运行时常亮池

  时方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述,还有一项就是常量池(constant pool table),用于存放编译期生成的各种字面量和符号引用,着不放会在类加载后放在方法区的运行时常量池中。  

  并非预置入Class文件中常量池的内容才进入方法运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

 

    当方法区无法满足内存分配需求时,抛出OutOfMemoryError

2.7直接内存

  并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

   JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。因为避免了在Java堆和Native堆中来回复制数据,提高了性能。

    当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。

2.3  对象访问

对象访问在Java语言中无处不在,即使最简单的访问也涉及Java栈、Java堆、方法区这三个重要的内存区域中。

例:Object obj = new Object();

  Object obj     反映到Java栈(Java VM Stack)的本地变量表,作为一个reference类型数据出现。

  New Object() 反映到Java堆中,形成了一块存储了Object类型的所有实例数据值的结构化内存。根据具体对象类型以及虚拟机实现对象内存局部表的不同,这块内存的长度是不固定的。同时Java堆中还包含查找此对象信息的地址信息。

  通过reference类型如何访问Java堆中的对象?主流的访问方式有两种:

使用句柄访问方式

  Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄地址中包含了对象实例数据和类型数据各自的具体地址。

使用直接指针访问方式:

  Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象堆地址。

优势对比:

    句柄访问方式:最大的好处就是:reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时移动对象是非常普遍的行为)时只要修改句柄中的实例数据指针,而reference本身不需要被修改。

    直接指针访问方式:最大好处就是速度快,它节省了一次指针定位的时间开销。

 2.4 实战:内存溢出(OutOfMemoryError)异常

 2.4.1 java堆溢出

  Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

        下面代码限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。

  eclipse dug启动参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M

 2.4.2 虚拟机栈和本地方法栈溢出

  由于HotSpot虚拟机并不区分虚拟机栈和本地方法栈,因此-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在java虚拟机规范中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError
  • 如果虚拟机在扩展是无法申请到足够的内存空间,则抛出OutOfMemoryError
/**
 * 设置  VM Args: -Xss128k
 */
public class JavaVMStackSOF {
    private int stackLength = 1;

    //反复调用
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

结果可以说明:在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配时,在虚拟机抛出的都是StackOverflowError异常,栈内存溢出异常与栈空间是否够大并不存在任何联系,这种情况,每个线程的栈分配的内存越大,反而容易产生内存溢出异常。

2.4.3 运行时常量池溢出

  如果要向运行时常量池中添加内容,最简单的做法就是使用 String.intern()这个 Native 方法。该方法的作用是:如果池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize 和-XX:MaxPermSize 限制方法区的大小,从而间接限制其中常量池的容量代码运行时常量池导致的内存溢出异常

/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用 List 保持着常量池引用,避免 Full GC 回收常量池行为
        List<String> list = new ArrayList<String>();
        // 10MB 的 PermSize 在 integer 范围内足够产生 OOM 了
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

从运行结果中可以看到,运行时常量池溢出,在 OutOfMemoryError 后面跟随的提示信息是“PermGenspace”,说明运行时常量池属于方法区( HotSpot 虚拟机中的永久代)的一部分。

2.4.4 方法区溢出

  方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。 
对于这个区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用 JavaSE API 也可以动态产生类(如反射时的GeneratedConstructorAccessor 和动态代理等),但在本次实验中操作起来比较麻烦。在代码清单 2-5 中,笔者借助 CGLib①直接操作字节码运行时,生成了大量的动态类。

  值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经常会出现在实际应用中:当前的很多主流框架,如 Spring 和 Hibernate 对类进行增强时,都会使用到 CGLib 这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的 Class 可以加载入内存。

/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class JavaMethodAreaOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method,
                        Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
    static class OOMObject {
    }
}

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收掉,判定条件是非常苛刻的。在经常动态生成大量 Class 的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了 GCLib 字节码增强外,常见的还有:大量 JSP 或动态产生 JSP 文件的应用( JSP 第一次运行时需要编译为Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

2.4.5 本机直接内存溢出

  DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xxm指定)一样。下面的例子直接通过反射获取Unsafe实例并进行内存分配。 

import java.lang.reflect.Field;

/**
 * -XX:MaxDirectMemorySize=10M
 */
class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalArgumentException,
            IllegalAccessException {
        Field unsafeField = sun.misc.Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

 

posted on 2018-09-07 12:00  清风徐来随心  阅读(187)  评论(0编辑  收藏  举报