深入理解java虚拟机(一):Java 内存区域与内存溢出异常

参考博客
http://blog.csdn.net/dongyuxu342719/article/details/78809049

一、运行时数据区域

1、线程隔离的数据区

  • 程序计数器(Program Counter Register)
    • 当前线程所执行代码的行号指示器
    • 每个线程有一个独立的程序计数器
  • 虚拟机栈(VM Stack)
    • 线程私有,生命周期等同于线程
    • 每个方法在执行的同时会创建一个帧栈
    • 储存局部变量表、操作数栈、动态链接、方法出口等信息。最重要的是局部变量表,也是大家常常讨论的“栈”空间。
  • 本地方法栈(Native Method Stack)
    • 本地方法栈类似于虚拟机栈,只不过虚拟机栈为虚拟机执行的java方法服务,本地方法栈为虚拟机使用到的Native方法服务

Notice:

  1. HotPot 的实现合并了本地方法栈和虚拟机栈

2、由线程共享的数据区

  • 堆(Heap)
    • 线程共享,虚拟机不关闭生命就不会结束。因此需要对此空间进行管理,是垃圾收集机制(GC)的主要区域。
    • 绝大部分对象实例及数组都要在堆上分配内存
  • 方法区(Method Area)
    • 线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 运行时常量池(Runtime Constant Pool)
    • 方法区的一部分,用于存放编译器生成的各种字面量和符号引用
  • 直接内存(Direct Memory)
    • 直接分配堆外内存,不属于虚拟机运行时数据区。
    • 避免在Java堆和Native堆中来回复制数据,在某些场景能显著提高性能。如NIO中的通道(Channel)与缓冲区(Buffer)。

Notice:

  1. 为什么GC的主要区域是Heap:动态创建的对象都在Heap上为其实例分配内存。Stack在方法和局部代码调用完成后释放帧栈空间,但Heap生命周期与虚拟机相同,内存不能自动释放。C/C++中程序员往往在代码中显示调用 free/delete 释放堆中对象的空间,操作繁琐,且容易发生内存泄漏。java 虚拟机的自动垃圾收集机制则能自动释放不需要用到的对象实例,实现堆内存的自动管理。
  1. 已加载类的基本信息和方法储存在方法区
  2. 常量(final)和静态变量(static)储存在方法区。问题:局部变量声明final,储存在哪个区域(堆、方法区、虚拟机栈)。
  3. Object 和 Array 的实例在堆中分配内存,并在相应的位置(如栈)创建引用。没有有效引用将被GC。
  1. HotPot 的实现把方法区合并到了堆的永久代区(Permanent Generation)。
  2. 如何实现方法区是虚拟机的技术实现细节,但是使用永久代实现方法区现在看来并不是一个好主意,因为这样更容易遇到内存泄漏问题。JDK 1.7 的 HotPot 中,已经把原本放在永久代的字符串常量池移出。

二、HotPot 虚拟机对象探秘

1、对象的内存布局

  1. 对象头(Header)
    1. (Mark Word)对象自身的运行时数据:如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
    2. 类型指针:对象指向元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。数组还会记录数组的长度。
  2. 实例数据(Instance Data)
    1.各种成员变量,包括父类继承的和子类定义的。
  3. 对齐填充(Padding)
    1. HotPot VM 要求对象起始地址必须是8字节的整数倍
    2. 对象实例数据没有对齐时,用对齐填充补全。

2、对象的访问定位

  • 句柄
    • 如果使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
    • 句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是很普遍的行为)时,只会修改句柄中的实例数据的指针,而reference本身不需要修改。
通过句柄访问对象
  • 直接指针
    • 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何存放访问类型数据的相关信息,而reference中存储的直接就是对象地址。
    • 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多也是一项非常可观的执行成本。
    • 就HotSpot虚拟机而言,它是使用直接指针进行对象访问的。
通过直接指针访问对象

三、堆栈溢出异常

1、对应参数:

  • -Xms 堆的最小值
  • -Xmx 堆的最大值
  • -Xss 栈容量
  • -Xoss 本地方法栈(HotPot 无效,因为没有设置单独的本地方法栈)
  • -XX:PermSize=10M -XX:MaxPermSize=10M 永久代
  • -XX:MaxDirectMemorySize=10M 直接内存
  • -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。

2、Java 堆溢出

  • java.lang.OutOfMemoryError:Javaheap space

  • 要解决这个区域的异常,一般的手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)堆Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要分清楚到底是内存泄露还是内存溢出。

  • 如果是内存泄露,可进一步通过工具查看泄露对象到GCRoots的引用链。于是就能找到泄露对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法对他们进行回收的。掌握了泄露对象的类型信息及GC Roots引用链信息,就可以比较准确的定位到泄露代码的位置。

  • 如果不存在泄露,换句话说,就是内存中的对象确实都是必须要存活的,那就应当检查虚拟机的堆参数(-Xms和-Xmx),与机器的物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态过长的情况,尝试减少程序运行期的内存消耗。

3、虚拟机栈和本地方法栈溢出

  • java.lang.StackOverflowError :某线程中线程请求的栈深度大于虚拟机所允许的最大深度

  • java.lang.OutOfMemoryError:unable to create new native thread:申请多线程时栈容量不够

  • 因为操作系统分配给每个进程的内存是有限制的,比如32位Windows系统限制为2GB。虚拟机提供了参数来限制Java堆和方法区这两部分的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机本身消耗的内存不计算在内,剩余的内存就是由虚拟机栈和本地方法栈瓜分掉了,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把内存耗尽。

  • 所以,如果是建立过多线程导致的内存溢出,在不能减少线程数量或更换64位操作系统的情况下,可以通过减小最大堆和减小栈容量来换取更多的线程。如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会很难想到。

4、方法区和运行时常量池溢出

  • java.lang.OutOfMemoryError:PermGen space

  • HotPot中方法区和常量池都存放在永久代。

  • 运行时常量池:字符串和整型等常量池数据的存放方式在 JDK 1.7 中有一定的调整,因此表现与 JDK 1.6 会有所不同。如 intern()方法。

  • 方法区溢出:方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾回收器回收的条件是非常苛刻的。在经常生成大量Class的应用中,需要特别主要类的回收情况。比如:程序中使用了CGLib字节码增强和动态语言(Spring、Hibernate等主流框架)、大量JSP或动态生成JSP文件的应用(JSP第一次运行时需要编译为Java类),基于OSGi的应用(即使是同一个类文件被不同的类加载器加载也会被视为不同的类)等。

5、本机直接内存溢出

  • java.lang.OutOfMemoryError

  • DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,可以直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsa()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。

  • 由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接的使用了NIO,那就可以考虑检查一下是不是这方面的原因。

四、Intern() 方法

不必太纠结于 intern() 方法,这个例子只是告诉我们,虚拟机底层的不同实现,会影响某些代码的结果。高版本的虚拟机对低版本做了一些优化,效率更高。

public class RunTimeConstantPool {
    public static void main(String [] args){
        String str1=new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern()==str1);
        String str2=new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern()==str2);
    }
}

这段代码在JDK1.6中运行,会得到两个false,而在JDK1.7及以后的版本运行,会得到一个true和一个false。 产生差异的原因是:在JDK1.6中intern()会把首次出现的字符串复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在堆中,所以必然不是同一个引用,将返回false。而在JDK1.7中的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和StringBuilder创建的那个字符串是同一个。对str2比较返回false是因为”java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中指向的是第一次出现的引用,所以和str2不相同。

JDK 1.6:intern()

image
  • new str1:对应步骤1,在堆中为字符串分配内存,并在栈中创建引用
  • str1.intern():对应步骤2,在JDK1.6中intern()会把首次出现的字符串复制到永久代中(从实例2 copy 实例3),返回的也是永久代中这个字符串(str.intern()返回实例3)。
  • new str2:对应步骤3,在堆中为字符串分配内存,并在栈中创建引用
  • str2.intern():对应步骤4,由于实例4不是首次出现的”java”字符串对象,因此永久代的实例5是从首次出现的实例1复制而来,str2.intern() 返回实例5。

JDK 1.7:intern()

image
  • new str1:对应步骤1,在堆中为字符串分配内存,并在栈中创建引用
  • str1.intern():对应步骤2,JDK1.7中的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,如图在永久代记录实例2的引用,str1.intern() 返回引用3
  • new str2:对应步骤3,在堆中为字符串分配内存,并在栈中创建引用
  • str2.intern():对应步骤4,由于实例4不是首次出现的”java”字符串对象,因此永久代记录实例1的引用,str2.intern() 返回引用4。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容