jvm内存模型教程

一、java各个版本jvm内存模型区别

在这里插入图片描述

1.7及之前版本,如上图其中,虚拟机栈,本地方法栈以及程序计数器为线程隔离。方法区和堆是所有线程共享的数据区域。

1.8版本主要的区别是移除了方法区,方法区用于存储已被虚拟机加载的类信息、常量、静态变量、动态生成的类等数据。实际上在Java虚拟机的规范中方法区是堆中的一个逻辑部分,但是它却拥有一个叫做非堆(Non-Heap)的别名。

对于方法区的实现,不同虚拟机中策略也不同。以我们常用的HotSpot虚拟机为例,其设计团队使用永久带来实现方法区,并把GC的分代收集扩展至永久带。这样设计的好处就是能够省去专门为方法区编写内存管理的代码。但是在实际的场景中,这样的实现并不是一个好的方式,因为永久带有MAX上限,所以这样做会更容易遇到内存溢出问题。

关于方法区的GC回收,Java虚拟机规范并没有严格的限制。虚拟机在实现中可以自由选择是否实现垃圾回收。主要原因还是方法区内存回收的效果比较难以令人满意,方法区的内存回收主要是针对常量池(1.7已经将常量池逐步移除方法区)以及类型的卸载,但是类型卸载在实际场景中的条件相当苛刻。

另外还需要注意的是在HotSpot虚拟机中永久带和堆虽然相互隔离,但是他们的物理内存是连续的。而且老年代和永久带的垃圾收集器进行了捆绑,因此无论谁满了都会触发永久带和老年的GC。

因此在Java1.8中,HotSpot虚拟机已经将方法区(永久带)移除,取而代之的就是元空间。

1、程序计数器(Program Counter Register)
它是一块较小的内存空间,可以看做是指向当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖计数器来完成()。

为什么程序计数器为线程私有呢?

由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,因此,为了线程切换后能恢复到正常的执行位置,每条线程都需要一个独立的程序计数器,各线程之间计数器互不影响,独立存储,为线程私有的内存。
此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

2、Java虚拟机栈(Java Virtual Machine Stacks)

java虚拟机栈与程序计数器一样,也是线程私有的,他的生命周期和线程保持一致。他是存储当前线程运行方法时所需要的数据、指令、返回地址。在每个方法执行时,虚拟机栈都会创建一个栈帧(Stack Frame),用于存储:局部变量表、操作数栈、动态链接、方法出口等信息。

在这里插入图片描述
其中局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同与对象本身,可能是一个指向对象其实地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) 和returnAddress类型(指向了一条字节码指令的地址)

局部变量表的存储空间是32位,刚好可以放一个int类型,所以长度为64为的long和double类型的数据会占用2个局部变量空间(Slot),局部变量表的大小在编译器就已经确定了

在java虚拟机规范中,对java虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分虚拟机都可以动态扩展,只不过ava虚拟机规范中也允许固定长度的虚拟机栈),扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

3、本地方法栈(Native Method Stack)

与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。

4、堆(Heap)
在这里插入图片描述

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor,这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。java堆是java虚拟机管理的内存中最大的一块,java堆是被所有线程共享的一块内存区域,堆的唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配内存。
 java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们磁盘空间一样。(不过在实现中既可以大小固定,也可以是可扩展,通过-Xmx 和-Xms控制),如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

5、方法区(Method Area)
  方法区和堆一样,是各个线程共享的区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据等
ps:方法区中还包括运行时常量池(Runtime Constant Pool),Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Tabel),用于存放编译期生成的各种字面量常量和符号引用,这部分内容将在类加载后进入方法区的运行时常量中存放,当常量池无法再申请到内存时也会抛出OutOfMemoryError异常

6、元空间
使用本地内存,取代方法区(永久代),其中静态变量、字符串常量池移到堆中。

默认情况下元空间大小是无限的,但是JVM同样提供了参数来控制它的使用:

-XX:MetaspaceSize
class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,
同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,
那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
-XX:MaxMetaspaceSize
可以为class metadata分配的最大空间。默认是没有限制的。
-XX:MinMetaspaceFreeRatio
在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
-XX:MaxMetaspaceFreeRatio
在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。

二、生成对象时的内存情况

字符串常量池的设计思想

a、字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。

b、JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。

为字符串开辟一个字符串常量池,类似于缓存区。

创建字符串常量时,首先坚持字符串常量池是否存在该字符串。

存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。

c、实现的基础

实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享。

运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。

先来看使用引号""创建字符串的方式

单独(注意是单独)使用引号来创建字符串的方式,字符串都是常量,在编译期已经确定存储在常量池中了。
用引号创建一个字符串的时候,首先会去常量池中寻找有没有相等的这个常量对象,没有的话就在常量池中创建这个常量对象;有的话就直接返回这个常量对象的引用。

String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);//true

这个例子的结果是true,首先 String str1 = “hello”,会先到常量池中检查是否有“hello”的存在,发现是没有的,于是在常量池中创建“hello”对象,并将常量池中的引用赋值给str1;第二个字面量 String str2 = “hello”,在常量池中检测到该对象了,直接将引用赋值给str2。

然后是new的方式创建字符串

String a = new String("abc")

new这个关键字,毫无疑问会在堆中分配内存,创建一个String类的对象。因此,a这个在栈中的引用指向的是堆中的这个String对象的。

然后,因为"abc"是个常量,所以会去常量池中找,有没有这个常量存在,没的话分配一个空间,放这个"abc"常量,并将这个常量对象的空间地址给到堆中String对象里面;如果常量池中已经有了这个常量,就直接用那个常量池中的常量对象的引用呗,就只需要创建一个堆中的String对象。

在这里插入图片描述
关于“+”运算符

String s1 = "he" +"llo";
String s2 = "hello";
System.out.println(s2==s1); //true

两个或者两个以上的字符串常量相加,在预编译的时候“+”会被优化,相当于把两个或者两个以上字符串常量自动合成一个字符串常量.

String的intern()方法
不仅如此,在intern方法返回的引用上,JDK1.6和JDK1.7也有个地方不一样,来看看书本上给的例子:

	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创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。

而JDK1.7的intern()不会再复制实例,只是在常量池中记录首次出现的实例的引用,因此intern()返回的引用和StringBuilder创建的那个字符串的实例是同一个。对str2比较返回false是因为"java"这个字符串在执行StringBuilder.toString()之前就已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值