Java虚拟机运行时数据区域

前言

最近刚换了新工作,忙于理解新的业务,就没有什么时间写博客,今天抽空写一篇关于Java虚拟机运行时内存分配的文章,闲言少续,开整。

根据Java虚拟机规范第7版,规定了Java虚拟机所管理的内存会包含以下几个数据区域,如下图所示。
Java虚拟机运行时数据区域
接下来对每一个区域的特点进行简单的概述。

程序计数器

程序计数器占用的内存比较小,主要作用是告诉线程下一步该执行哪一个字节码指令。因此,为了保证线程切换后能恢复到正确的执行位置,每个线程就需要一个独立的程序计数器,各个线程之间的互不影响,也就是说程序计数器是线程私有的。

如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,但是当他执行的是Native方法的时候,这个计数器为空。

程序计数器也是唯一一个没有规定OutOfMemoryError情况的区域。

虚拟机栈

Java方法执行的内存模型:每个方法执行时会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等信息,其实每个方法从开始到执行完成,就是一个栈帧从虚拟机栈中入栈到出栈的过程。每个线程可能在执行不同的方法,由此可见,虚拟机栈也是线程私有的。

从上面的描述得知,栈存放了局部变量表,那局部变量表都包括什么呢?局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。局部变量表的内存空间是在编译期间完成分配的,在方法运行期间不会改变局部变量表的大小。

虚拟机栈的异常分为两种:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  2. 如果虚拟机可以动态扩展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈和虚拟机栈的作用比较相似,虚拟机栈是为Java方法服务,而本地方法栈是为虚拟机使用到的Native方法服务。同样,也是线程私有的。

既然作用一样,异常也和虚拟机栈一样,会抛出StackOverflowError异常和OutOfMemoryError异常。

Java堆是Java虚拟机所管理的内存中最大的一块。Java堆存放对象实例,几乎所有的对象实例及数组都要在堆上分配内存,由此可知,堆是被所有线程共享的内存区域。

Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆。从垃圾回收的角度分析,Java堆可分为新生代和老年代,不同的区域采用不同的垃圾收集算法,如下图所示。
在这里插入图片描述
新生代:主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。 新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。

  • Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
  • ServivorTo:保留了一次MinorGC过程中的幸存者。
  • ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。

MinorGC的过程:MinorGC采用复制算法。首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

老年代:主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

MajorGC采用标记—清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。

当老年代也满了装不下的时候,就会抛出OutOfMemoryError异常。

方法区

Java虚拟机虽然把方法区描述为堆的一个逻辑部分,但是为了把它和堆区分开,有个别名叫Non-Heap(非堆),方法区主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,也会进行垃圾回收。

方法区在Java 8前后有不同的实现,之前一般称为 “永久代”,Java 8换成了 “元空间”。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中。 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

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

运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。

运行时常量池相对于Class文件常量池的另外一个重要特性是具备动态性,Java语言并不要求常量一定只有编译期才能产生,运行期间也可以将新的常量放入池中,比如String类的intern()方法。

既然是方法区的一部分,自然也会在申请不到内存时抛出OutOfMemoryError异常。

由于我也没用过intern()方法,所以就研究了一下:

intern()是把堆中的引用丢入常量池中,然后返回这个引用。当常量池中已经存在这个引用,就直接返回这个引用。举几个例子。

String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1.intern()==s2);//1.true
System.out.println(s1==s2);//2.false

s1是String对象,s2是常量池中“abc”的引用,所以s1!=s2;
s1.intern()是把“abc”丢入常量池中,因为常量池中已经存在“abc”的引用,所以直接返回"abc"的引用,所以s1.intern()==s2。

String s1 = new String("abc") + new String("01");
s1.intern();
String s2 = "abc01";
System.out.println(s1==s2);//true

s1是两个字符串的拼接,在堆里面生成了新的字符串对象“abc01”,s1.intern()之后会把堆中的“abc01”引用丢入常量池中,s1本身就是这个引用,s2去获取“abc01”也是去常量池中获取,得到了常量池中的“abc01”引用,所以s1 == s2。

String s3 = new String("abc") + new String("02");
String s4 = "abc02";
s3.intern();
System.out.println(s3==s4);//false

这里可以看到s3.intern()执行是在s4 = "abc02"之后,在s4 = "abc02"的时候,常量池中并没有“abc02”,所以创建了“abc02”并把该引用丢入了常量池中,然而s3任然是堆中“abc02”的引用,所以s3 != s4。

总结

根据上面的介绍,可以简单概括出:

  • 除了程序计数器,其他区域都可抛出OutOfMemoryError异常;
  • 本地方法栈和虚拟机栈都可抛出StackOverflowError异常;
  • 堆和方法区是垃圾收集的重点区域;
  • 堆和方法区是被所有线程共享的内存区域,而程序计数器、本地方法区、虚拟机栈是线程私有的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值