一、 Java数据区

java工程超过xmx就会内存溢出吗 java线程内存溢出_方法区

  以上是jvm运行时的数据区。

     1. 程序计数器

             程序计数器是一块较小的内存,用来指示下一条要执行的字节码指令。每个线程都需要一个程序计数器,因为Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,各个线程执行各自的指令,为了线程切换后能正确执行,所以每个线程都需要一个程序计数器来指示当前线程的执行指令。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

   2. Java虚拟机栈

             与程序计数器一样,Java虚拟机栈也是线程私有的,其生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型: 方法在执行时会创建一个栈帧用来存储局部变量表,操作数栈,动态链接,方法出口等信息。方法的调用执行过程就是栈帧的入栈和出栈的过程。在局部变量表中存放了编译器可知的基本数据类型、对象引用类型和返回类型,之所以存放这些类型是为了确定局部变量表所需的内存空间。

              Java虚拟机栈有两种异常: StackOverflow和OutOfMemmory异常。 如果线程请求的栈深度大于虚拟机所允许的深度,发生StackOverflow异常。如果虚拟机栈的动态扩展在扩展过程中无法申请到足够的内存,就会抛出OutOfMemmory异常。

   3. 本地方法栈

              与Java虚拟机栈显示,不过其为本地方法服务。也有StackOverflow和OutOfMemmory异常。

  4. Java堆

            Java堆是Java虚拟机管理的内存中最大 的一块,Java堆被所有线程共享,在虚拟机启动时创建。 所有的对象实例和数组都在这里分配内存(不考虑JIT)。 Java垃圾回收器管理的主要区域也在堆中。可以通过-Xmx和-Xms来控制Java堆的分配。如果堆中没有内存完成实例的分配,并且堆无法扩展时,将会抛出OutOfMemmoeryError异常。

   5. 方法区

            方法区与Java堆一样,也是各个线程所共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。也有人将方法区成为老年代,可以像Java堆一样堆方法区进行垃圾回收,也可以不实现垃圾收集。此区域的内存回收主要针对常量池的回收和对类型的卸载。当方法区中无法满足内存分配需求时,将抛出OutOfmemoryError异常。

         回收常量: 当没有任何引用指向方法区中常量池中的常量时,必要的话,这个常量会被系统清理。

        回收无用类: 当该类的所有实例都被回收,加载该类的ClassLoader以及被回收,该类对应的Class对象没有被引用,那么改类可能被回收。

    6. 运行时常量区

           运行时常量区是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息还有一项信息是常量池,常量池用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量区中存放。Java虚拟机对Class文件的每一部分的格式都有严格的规定,对于运行时常量池,Java虚拟机规范没有做任何细节的要求。运行时常量区具备Class文件常量池所没有的动态性,程序运行期间也可以将新的常量放入池中。 有OutOfMemmory异常。

    7. 直接内存

            直接内存并不属于虚拟机运行时数据区一部分。Jdk1.4中加入了NIO,引入了一种基于通道和缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存。会产生OutOfMemmory异常。

 

二、 虚拟机对象

          1.  对象的创建

                     当虚拟机遇到一条new指令时,首先会去检查这个指令的参数能否在常量池中定位到这个类的符号引用,并且检查这个类的符号引用代表的类是否被加载、解析和初始化过,没有的话会先执行类加载过程。

                     当类加载完毕后,会进行对象内存的分配(对象需要的内存大小在类加载过程中会确定),分配内存就是从堆中划分一块内存。有两种方式进行内存的分配: 指针碰撞和空闲列表。 指针碰撞对于内存规则的堆来说,将使用和未使用的内存使用指针划分开来,分配内存时使用指针向空闲区域挪动一段和对象内存大小相等的距离即可。对于堆中的内存不规则的情形,虚拟机维护一个空闲列表,分配时从列表中选取一块足够的空间划分给对象。 选择哪种方式有内存堆是否规整决定,而内存堆的规整与否与垃圾收集器相关。 因此使用Serial、parnew等收集器使用的是指针碰撞,使用cms等收集器使用的是空闲列表方式。

                    划分完内存空间后需要进行内存分配动作,此过程要保持原子性。 此时可以采用: CAS+失败重试方式和根据线程在不同的空间之中进行(每个线程在Java堆中预先分配一小块内存【本地线程分配缓存(TLAB)】)。 哪个线程要分配就在哪个线程的TLAB上分配。

                   在内存分配完后,虚拟机将分配到的内存空间都初始化零值,如果在TLAB上进行操作,这一过程可以在TALB分配时进行。这就是为什么Java中实例数据不需要初始化也可以运行的原因。

                  接下来虚拟机需要对这个对象做一些必要的设置,来表示这个对象。如此对象的类的实例、对象的hash码、对象的类的元数据信息定位等信息。 这些信息存放在对象头这个数据结构中。  至此,从虚拟机的角度来看以及生成了一个可用的对象了。 但是用户程序还需要对对象进行初始化。 执行new指令后接着Java虚拟机会执行初始化函数进行初始化。

        2. 对象的内存布局

                   对象在内存中的存储布局可以分为3块: 对象头、实例数据和对齐填充。

                   对象头包括两个部分: 第一部分存储对象自身的运行时数据如上面说的hash码,GC分代年龄、锁状态标志等。对象头的另一部分是类型指针,用来指向他的类元数据的指针。Java虚拟机通过这个来判断此对象属于哪一个类的实例。但并不是所有虚拟机实现都需要在对象上保留类型指针,即查找对象的元数据信息并不一定要经过对象本身。数组对象还需要在对象头张包括数据长度。

                  实例数据部分是对象真正存储的有效信息,即程序中定义的各种类型的数据内容。包括了父类中的数据内容。在虚拟机中相同宽度的字段总是被分配到一起。父类的变量会出现在子类之前。

                对齐填充不一定要存在,也没有特殊的意义,其占位符的作用。

       3. 对象的访问定位

                 通过栈上的引用来操作堆上的具体对象。如何具体的通过引用来访问堆上的对象由两种方式: 句柄访问和直接访问。

                句柄访问: 在Java堆中会开辟出一块内存作为句柄池,引用指向的就是句柄池地址。在句柄中包括了对象的实例数据和类型数据的指针来指向真实的数据。 优点: 引用中存储的是稳定的句柄地址。

                直接访问: Java堆的对象内存中直接存放的是对象的实例数据和到对象类型数据的指针,引用中存储的直接就是对象的地址。 优点: 速度块。