深入理解JVM之一:Java内存区域

前言

Java虚拟机运行时数据区分为以下几个部分:
方法区、虚拟机栈、本地方法栈、堆、程序计数器。如下图所示:

程序计数器

程序计数器可以理解为当前线程执行的字节码的行号指示器,字节码解释器就是通哟改变这个值来获取需要执行的下一条需要执行的字节码指令。对于多线程来说,每条线程都有自己的程序计数器,这样各线程之间的计数器互不影响,这类内存区域也叫作“私有内存”(可以看到其实并不是私有的),之所以这么设计,是因为在多线程的情况下,完全可能出现线程中断的情况,那么当被中断的线程需要回复执行的时候,怎么知道上次该线程执行到哪里了呢?这就需要程序计数器发挥作用了,由于每个线程都有自己的程序计数器,这样当CPU重新调度该线程的时候,从其计数器中取出下一条的字节码执行指令,于是就可以继续执行了。

Java虚拟机栈

也是线程私有的,生命周期与线程相同,Java虚拟机栈描述了Java的方法执行模型,每个方法执行时都会创建一个栈帧。会抛出StackOverFlowError和OOM

本地方法栈

与Java虚拟机栈类似,只不过其描述的是本地方法的执行模型,也会抛出StackOverFlowError和OOM

Java堆

与Java虚拟机栈不同,Java堆是所有线程共享的,在虚拟机启动的时候创建,此内粗区域的唯一目的就是存放对象实例。同时,这块内存区域也是垃圾收集器的主要区域,也被成为GC堆

Hotspot虚拟机对象

对象的创建

首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有没有就执行下一步

检查通过后,虚拟机会为新生的对象分配内存,主要由两种内存分配策略,一种是指针碰撞,一种是空闲列表。所谓指针碰撞就是把Java堆中的内存一分为二,一边是所有用过的内存(这部分内存不能被分配了),一边是空闲的内存,是可以被分配的,这样的话,在可用于不可用的内存之间会有一个分割点指示器,那么为对象分配内存实际上就是从这个分界点指示器往空闲内存的一边拨动一段空间就可以了。而空闲列表则没有这个假设,已使用的内存与空闲内存可能是交叉在一起的,那么使用指针碰撞的方式分配内存就会产生问题,但是虚拟机维护着一张列表,这张列表记录了哪些区域的内存是可用的,那么在分配内存的时候就从选择可以容纳对象要求大小的内存区域分配给这个对象。

虚拟机将分配到的内存空间都初始化为零(不包括对象头),这里的初始化不同于我们在Java中利用构造函数进行初始化的过程,这里的初始化时保证Java的一些原生数据类型在不重新赋值的时候就可以直接使用,程序在使用这些对象的时候可以直接使用零值。

接下来,虚拟机要对对象一些必要的设置,进行这些设置的目的是可以知道这些对象是哪个类的实例、对象的哈希码、对象的GC分代年龄等信息,这些信息都存储在对象头中。

上面这些工作完成之后,从虚拟机的角度看,一个对象已经构造完成,但是从开发人员的角度看,还需要进行new对象之后初始化,接着执行init方法,把对象按照程序员的意愿进行初始化。到这里一个对象才算真正创建完毕

对象的内存布局

主要包括三部分的信息:对象头、实例数据和对齐填充

对象头又包括两部分信息,第一部分用于存储对象自身的运行时数据,比如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等;第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。当然,类型指针不是必须的。可能有疑问,如果没有类型指针,这么知道这个对象是哪个类的实例呢?答案是知道对象属于哪个类并不一定需要通过对象本身

实例数据部分是对象真真好存储的有效信息,也是程序代码中所定义的各种类型的字段内容

第三部分不是必然存在的,只是起到占位符的作用,因为HotspotVM规定对象的起始地址必须是8字节的整数倍。所以很有可能以上两部分的大小不够8字节的整数倍,那么这个字段就可以发挥作用了。

对象的访问定位

创建了对象,要使用对象就必须定位这个对象,那么在VM中是如何定位一个对象呢?主要是通过Java栈中的reference数据,通过这个reference数据只是一个指向对象的引用,那么对象的访问方式就可以不同。目前主流的对象访问方式主要由句柄和直接指针两种。通过句柄访问的话,会在Java堆中划分出一块句柄池,句柄池中国句柄存放了对象的实例数据和类型指针,而reference数据则存放了句柄的地址引用。使用直接指针访问对象,那么reference数据存放的就是对象的地址。

使用句柄访问的最大好处是reference中存储的稳定的句柄地址,当对象的地址发生了改变可以不用去关心。而直接指针的最大好处是速度更快,在于节省了一次指针定位的时间。

一个OOM异常的例子

一般而言,发生OOM异常无非就那么几类:

  • Java堆溢出
  • 虚拟机栈和本地方法栈溢出
  • 方法区和运行常量池溢出
  • 本机直接内存溢出

下面是一个方法区和运行时常量池溢出的例子:

代码清单:

/**
 * 在Eclipse-Run Configuration中设置 VM arguments如下:
 * VM args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i=0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}

出现的异常如下:

注意的是,必须使用jdk1.6及之前的版本才会出现这个异常,否则程序会一直运行下去

时间: 2024-11-03 19:21:05

深入理解JVM之一:Java内存区域的相关文章

学习JVM之java内存区域与异常_java

一.前言 java是一门跨硬件平台的面向对象高级编程语言,java程序运行在java虚拟机上(JVM),由JVM管理内存,这点是和C++最大区别:虽然内存有JVM管理,但是我们也必须要理解JVM是如何管理内存的:JVM不是只有一种,当前存在的虚拟机可能达几十款,但是一个符合规范的虚拟机设计是必须遵循<java 虚拟机规范>的,本文是基于HotSpot虚拟机描述,对于和其它虚拟机有区别会提到:本文主要描述JVM中内存是如何分布.java程序的对象是如何存储访问.各个内存区域可能出现的异常. 二.

[jjzhu学java]深入理解JVM笔记之内存管理机制

深入理解JVM笔记之内存管理机制 运行时数据区域 程序计数器 JVM栈 本地方法栈 Java堆 方法区 运行时常量池 直接内存 对象访问 OutOfMemoryError异常 Java堆溢出示例 JVM栈和本地方法栈溢出 运行时常量池溢出 本机直接内存溢出 深入理解JVM笔记之内存管理机制 运行时数据区域 程序计数器 每个线程都有一个程序计数器(PC),是当前线程所执行的字节码的行号指示器,通过改变程序计数器的值来选取下一条指令.各线程之间的计数器互不影响,是线程私有的内存. 如果线程执行的是一

Java内存区域与内存溢出

内存区域 Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域.Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器.Java虚拟机栈.本地方法栈.Java堆.方法区.下面详细阐述各数据区所存储的数据类型. 程序计数器(Program Counter Register) 一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支.跳转.循环等基础功能都要依赖它来实现

深入探讨Java内存区域_java

一.概述 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间.Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示: 下面就每一个区域进行阐述. 二.运行时数据区域 程序计数器 程序计数器,可以看做是当前线程所执行的字节码的行号指示器.在虚拟机的概念模型里,字节码解释器工作就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支.循环.跳转.异常处理.线程恢复等基础功能都要依赖这个计数器来完成.

深入分析Java内存区域的使用详解_java

Java 内存划分:     在Java内存分配中,java将内存分为:方法区,堆,虚拟机栈,本地方法栈,程序计数器.其中方法区和堆对于所有线程共享,而虚拟机栈和本地方法栈还有程序计数器对于线程隔离的.每个区域都有各自的创建和销毁时间. 程序计数器:     作用是当前线程所执行的字节吗的行号指示器.Java的多线程是通过线程轮流切换并分配处理器执行时间方式来实现的.因此,每个线程为了能在切换后能恢复到正确的位置,每个线程需要独立的程序计数器. Java 虚拟机栈:     每个放在被执行的时候

JVM:查看java内存情况命令

jinfo:可以输出并修改运行时的java 进程的opts. jps:与unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号. jstat:一个极强的监视VM内存工具.可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量. jmap:打印出某个java进程(使用pid)内存内的所有'对象'的情况(如:产生那些对象,及其数量). jconsole:一个java GUI监视工具,可以以图表化的形式显示各种数据.并可通过远程连接监视远程的服务器V

深入理解JVM之内存区域与内存溢出

文章目录 1. Java内存区域与内存溢出异常 1.1. 运行时数据区域 1.1.1. 程序计数器 1.1.2. java虚拟机栈 1.1.3. 本地方法栈 1.1.4. Java堆(Java Heap) 1.1.5. 方法区 1.1.6. 运行时常量池 1.1.7. 直接内存 1.2. HotSpot虚拟机 1.2.1. 对象的创建 1.2.2. 对象的访问定位 1.3. OOM异常的解决思路 1.4. 参考 Java内存区域与内存溢出异常 运行时数据区域 程序计数器 当前线程所执行的字节码的

全面理解Java内存模型

Java内存模型即Java Memory Model,简称JMM.JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式.JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的. 如果我们要想深入了解Java并发编程,就要先理解好Java内存模型.Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步.原始的Java内存模型效率并不是很理想,因此Java1.5版本对其进行了重构,现在的Java8仍沿用了Java1.5的版本. 关于并发编程 在并发

海子-JVM的内存区域划分

学过C语言的朋友都知道C编译器在划分内存区域的时候经常将管理的区域划分为数据段和代码段,数据段包括堆.栈以及静态数据区.那么在Java语言当中,内存又是如何划分的呢? 由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分.在讨论JVM内存区域划分之前,先来看一下Java程序具体执行的过程:                                       如上图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码