关于jvm:彻底吃透-JVM-虚拟机内存各个区域就这一篇

39次阅读

共计 8727 个字符,预计需要花费 22 分钟才能阅读完成。

一、引言

对于从事 C、C++ 程序开发的开发人员来说,在内存治理畛域,他们既是领有最高势力的“皇帝”,又是从事最根底工作的劳动人民——既领有每一个对象的“所有权”,又负担着每一个对象生命从开始到终结的保护责任。

对于 Java 程序员来说,在虚拟机主动内存管理机制的帮忙下,不再须要为每一个 new 操作去写配对的 delete/free 代码,不容易呈现内存透露和内存溢出问题,看起来由虚拟机治理内存所有都很美妙。

不过,也正是因为 Java 程序员把管制内存的势力交给了 Java 虚拟机,一旦呈现内存透露和溢出方面的问题,如果不理解虚拟机是怎么应用内存的,那排查谬误、修改问题将会成为一项异样艰巨的工作。

二、运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它治理的内存划分成若干个不同的数据区域。

运行时数据区域

这些组成部分一些事线程公有的,其余的则是线程共享的。

线程公有的:

  • 程序计数器
  • 虚拟机栈
  • 本地办法栈

线程共享的:

  • 办法区
  • 间接内存

三、程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它能够看作是以后线程所执行的字节码的行号指示器。在 Java 虚拟机的概念模型里,字节码解释器工作时就是通过扭转这个计数器的值来选取下一条须要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异样解决、线程复原等根底性能都须要依赖这个计数器来实现。

因为 Java 虚拟机的多线程是通过线程轮流切换、调配处理器执行工夫的形式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令 因而,为了线程切换后能复原到正确的执行地位,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程公有”的内存

如果线程正在执行的是一个 Java 办法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)办法,这个计数器值则应为空(Undefined)。此内存区域是惟一一个在《Java 虚拟机标准》中没有规定任何 OutOfMemoryError 状况的区域

四、Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈也是线程公有的,它的生命周期和线程雷同,形容的是 Java 办法执行的内存模型。

Java 内存能够毛糙的辨别为堆内存(Heap)和栈内存(Stack), 其中栈就是当初说的虚拟机栈,或者说是虚拟机栈中局部变量表局部。(实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都领有:局部变量表、操作数栈、动静链接、办法进口信息。)

局部变量表次要寄存了编译器可知的各种数据类型 (boolean、byte、char、short、int、float、long、double)、 对象援用(reference 类型,它不同于对象自身,可能是一个指向对象起始地址的援用指针,也可能是指向一个代表对象的句柄或其余与此对象相干的地位)。

Java 虚拟机栈会呈现两种异样:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不容许动静扩大,那么当线程申请栈的深度超过以后 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异样。
  • OutOfMemoryError: 若 Java 虚拟机栈的内存大小容许动静扩大,且当线程申请栈时内存用完了,无奈再动静扩大了,此时抛出 OutOfMemoryError 异样。

Java 虚拟机栈也是线程公有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创立而创立,随着线程的死亡而死亡。

五、本地办法栈

本地办法栈(Native Method Stacks)与虚拟机栈所施展的作用是十分类似的,其区别只是虚拟机栈为虚拟机执行 Java 办法(也就是字节码)服务,而本地办法栈则是为虚拟机应用到的本地(Native)办法服务

《Java 虚拟机标准》对本地办法栈中办法应用的语言、应用形式与数据结构并没有任何强制规定,因而具体的虚拟机能够依据须要自在实现它,甚至有的 Java 虚拟机(譬如 Hot-Spot 虚拟机)间接就把本地办法栈和虚拟机栈合二为一。与虚拟机栈一样,本地办法栈也会在栈深度溢出或者栈扩大失败时别离抛出 StackOverflowError 和 OutOfMemoryError 异样。

六、Java 堆

对于 Java 应用程序来说,Java 堆(Java Heap)是虚拟机所治理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创立。此内存区域的惟一目标就是寄存对象实例,Java 世界里“简直”所有的对象实例都在这里分配内存

在《Java 虚拟机标准》中对 Java 堆的形容是:“所有的对象实例以及数组都该当在堆上调配”,而这里笔者写的“简直”是指从实现角度来看,随着 Java 语言的倒退,当初曾经能看到些许迹象表明日后可能呈现值类型的反对,即便只思考当初,因为即时编译技术的提高,尤其是逃逸剖析技术的日渐弱小,栈上调配、标量替换 [插图] 优化伎俩曾经导致一些奥妙的变动悄悄产生,所以说 Java 对象实例都调配在堆上也慢慢变得不是那么相对了。

Java 堆是垃圾收集器治理的内存区域,因而一些材料中它也被称作“GC 堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。从回收内存的角度看,因为古代垃圾收集器大部分都是基于分代收集实践设计的,所以 Java 堆中常常会呈现“新生代”“老年代”“永恒代”“Eden 空间”“From Survivor 空间”“To Survivor 空间”等名词,在这里笔者想先阐明的是这些区域划分仅仅是一部分垃圾收集器的独特个性或者说设计格调而已,而非某个 Java 虚拟机具体实现的固有内存布局,更不是《Java 虚拟机标准》里对 Java 堆的进一步粗疏划分。不少材料上常常写着相似于“Java 虚拟机的堆内存分为新生代、老年代、永恒代、Eden、Survivor……”这样的内容。在十年之前(以 G1 收集器的呈现为分界),作为业界相对支流的 HotSpot 虚拟机,它外部的垃圾收集器全副都基于“经典分代”来设计,须要新生代、老年代收集器搭配能力工作,在这种背景下,上述说法还算是不会产生太大歧义。然而到了明天,垃圾收集器技术与十年前已不可同日而语,HotSpot 外面也呈现了不采纳分代设计的新垃圾收集器,再依照下面的提法就有很多须要商讨的中央了。

如果从分配内存的角度看,所有线程共享的 Java 堆中能够划分出多个线程公有的调配缓冲区(Thread Local Allocation Buffer,TLAB),以晋升对象调配时的效率。不过无论从什么角度,无论如何划分,都不会扭转 Java 堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将 Java 堆细分的目标只是为了更好地回收内存,或者更快地分配内存。在本章中,咱们仅仅针对内存区域的作用进行探讨,Java 堆中的上述各个区域的调配、回收等细节将会是下一章的主题。

依据《Java 虚拟机标准》的规定,Java 堆能够处于物理上不间断的内存空间中,但在逻辑上它应该被视为间断的,这点就像咱们用磁盘空间去存储文件一样,并不要求每个文件都间断寄存。但对于大对象(典型的如数组对象),少数虚拟机实现出于实现简略、存储高效的思考,很可能会要求间断的内存空间。

Java 堆既能够被实现成固定大小的,也能够是可扩大的,不过以后支流的 Java 虚拟机都是依照可扩大来实现的(通过参数 -Xmx 和 -Xms 设定)。如果在 Java 堆中没有内存实现实例调配,并且堆也无奈再扩大时,Java 虚拟机将会抛出 OutOfMemoryError 异样。

七、办法区

办法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、动态变量、即时编译器编译后的代码缓存等数据。尽管《Java 虚拟机标准》中把办法区形容为堆的一个逻辑局部,然而它却有一个别名叫作“非堆”(Non-Heap),目标是与 Java 堆辨别开来。

说到办法区,不得不提一下永恒代 这个概念,尤其是在 JDK 8 以前,许多 Java 程序员都习惯在 HotSpot 虚拟机上开发、部署程序,很多人都更违心把办法区称说为“永恒代”(Permanent Generation),或将两者一概而论

实质上这两者并不是等价的,因为仅仅是过后的 HotSpot 虚拟机设计团队抉择把收集器的分代设计扩大至办法区,或者说应用永恒代来实现办法区而已,这样使得 HotSpot 的垃圾收集器可能像治理 Java 堆一样治理这部分内存,省去专门为办法区编写内存治理代码的工作。

然而对于其余虚拟机实现,譬如 BEA JRockit、IBM J9 等来说,是不存在永恒代的概念的。原则上如何实现办法区属于虚拟机实现细节,不受《Java 虚拟机标准》管教,并不要求对立。但当初回头来看,当年应用永恒代来实现办法区的决定并不是一个好主见,这种设计导致了 Java 利用更容易遇到内存溢出的问题(永恒代有 -XX:MaxPermSize 的下限,即便不设置也有默认大小,而 J9 和 JRockit 只有没有触碰到过程可用内存的下限,例如 32 位零碎中的 4GB 限度,就不会出问题),而且有极少数办法(例如String::intern())会因永恒代的起因而导致不同虚拟机下有不同的体现。当 Oracle 收买 BEA 取得了 JRockit 的所有权后,筹备把 JRockit 中的优良性能,譬如 Java Mission Control 管理工具,移植到 HotSpot 虚拟机时,但因为两者对办法区实现的差别而面临诸多困难。思考到 HotSpot 将来的倒退,在 JDK6 的时候 HotSpot 开发团队就有放弃永恒代,逐渐改为采纳本地内存(NativeMemory)来实现办法区的打算了,到了 JDK 7 的 HotSpot,曾经把本来放在永恒代的字符串常量池、动态变量等移出,而到了 JDK 8,终于齐全废除了永恒代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间(Meta-space)来代替,把 JDK 7 中永恒代还残余的内容(次要是类型信息)全副移到元空间中。

《Java 虚拟机标准》对办法区的束缚是十分宽松的,除了和 Java 堆一样不须要间断的内存和能够抉择固定大小或者可扩大外,甚至还能够抉择不实现垃圾收集。相对而言,垃圾收集行为在这个区域确实是比拟少呈现的,但并非数据进入了办法区就如永恒代的名字一样“永恒”存在了。这区域的内存回收指标次要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收成果比拟难令人满意,尤其是类型的卸载,条件相当刻薄,然而这部分区域的回收有时又的确是必要的。以前 Sun 公司的 Bug 列表中,曾呈现过的若干个重大的 Bug 就是因为低版本的 HotSpot 虚拟机对此区域未齐全回收而导致内存透露。依据《Java 虚拟机标准》的规定,如果办法区无奈满足新的内存调配需要时,将抛出 OutOfMemoryError 异样。

八、运行时常量池

运行时常量池(Runtime Constant Pool)是办法区的一部分。Class 文件中除了有类的版本、字段、办法、接口等形容信息外,还有一项信息是常量池表(Constant Pool Table),用于寄存编译期生成的各种字面量与符号援用,这部分内容将在类加载后寄存到办法区的运行时常量池中。

Java 虚拟机对于 Class 文件每一部分(天然也包含常量池)的格局都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池,《Java 虚拟机标准》并没有做任何细节的要求,不同提供商实现的虚拟机能够依照本人的须要来实现这个内存区域,不过一般来说,除了保留 Class 文件中形容的符号援用外,还会把由符号援用翻译进去的间接援用也存储在运行时常量池中

运行时常量池绝对于 Class 文件常量池的另外一个重要特色是具备动态性,Java 语言并不要求常量肯定只有编译期能力产生,也就是说,并非预置入 Class 文件中常量池的内容能力进入办法区运行时常量池,运行期间也能够将新的常量放入池中,这种个性被开发人员利用得比拟多的便是 String 类的 intern() 办法。既然运行时常量池是办法区的一部分,天然受到办法区内存的限度,当常量池无奈再申请到内存时会抛出 OutOfMemoryError 异样。

String 类和常量池

1 String 对象的两种创立形式:

 String str1 = "abcd";
     String str2 = new String("abcd");
     System.out.println(str1==str2);//false 

这两种不同的创立办法是有差异的,第一种形式是在常量池中拿对象,第二种形式是间接在堆内存空间创立一个新的对象。

记住:只有应用 new 办法,便须要创立新的对象。

2 String 类型的常量池比拟非凡。它的次要应用办法有两种:

  • 间接应用双引号申明进去的 String 对象会间接存储在常量池中。
  • 如果不是用双引号申明的 String 对象,能够应用 String 提供的 intern 方 String.intern() 是一个 Native 办法,它的作用是:如果运行时常量池中曾经蕴含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的援用;如果没有,则在常量池中创立与此 String 内容雷同的字符串,并返回常量池中创立的字符串的援用。
 String s1 = new String("计算机");
       String s2 = s1.intern();
       String s3 = "计算机";
       System.out.println(s2);// 计算机
       System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对 

3 String 字符串拼接

 String str1 = "str";
    String str2 = "ing";
    
    String str3 = "str" + "ing";// 常量池中的对象
    String str4 = str1 + str2; // 在堆上创立的新的对象 
    String str5 = "string";// 常量池中的对象
    System.out.println(str3 == str4);//false
    System.out.println(str3 == str5);//true
    System.out.println(str4 == str5);//false 

尽量避免多个字符串拼接,因为这样会从新创建对象。如果须要扭转字符串的花,能够应用 StringBuilder 或者 StringBuffer。

String s1 = new String(“abc”); 这句话创立了几个对象?

创立了两个对象。

验证:

 String s1 = new String("abc");// 堆内存的地值值
  String s2 = "abc";
  System.out.println(s1 == s2);// 输入 false, 因为一个是堆内存,一个是常量池的内存,故两者是不同的。System.out.println(s1.equals(s2));// 输入 true 

后果:

false
true 

解释:

先有字符串 ”abc” 放入常量池,而后 new 了一份字符串 ”abc” 放入 Java 堆(字符串常量 ”abc” 在编译期就曾经确定放入常量池,而 Java 堆上的 ”abc” 是在运行期初始化阶段才确定),而后 Java 栈的 str1 指向 Java 堆上的 ”abc”。

九、间接内存

间接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机标准》中定义的内存区域。然而这部分内存也被频繁地应用,而且也可能导致 OutOfMemoryError 异样呈现,所以咱们放到这里一起解说。

在 JDK 1.4 中新退出了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I / O 形式,它能够应用 Native 函数库间接调配堆外内存,而后通过一个存储在 Java 堆外面的 DirectByteBuffer 对象作为这块内存的援用进行操作。这样能在一些场景中显著进步性能,因为防止了在 Java 堆和 Native 堆中来回复制数据。

显然,本机间接内存的调配不会受到 Java 堆大小的限度,然而,既然是内存,则必定还是会受到本机总内存(包含物理内存、SWAP 分区或者分页文件)大小以及处理器寻址空间的限度,个别服务器管理员配置虚拟机参数时,会依据理论内存去设置 -Xmx 等参数信息,但常常疏忽掉间接内存,使得各个内存区域总和大于物理内存限度(包含物理的和操作系统级的限度),从而导致动静扩大时呈现 OutOfMemoryError 异样。

最初一说,加深印象

8 种根本类型的包装类和常量池

  • Java 根本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创立了数值 [-128,127] 的相应类型的缓存数据,然而超出此范畴依然会去创立新的对象。
  • 两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。
 Integer i1 = 33;
  Integer i2 = 33;
  System.out.println(i1 == i2);// 输入 true
  Integer i11 = 333;
  Integer i22 = 333;
  System.out.println(i11 == i22);// 输入 false
  Double i3 = 1.2;
  Double i4 = 1.2;
  System.out.println(i3 == i4);// 输入 false 

Integer 缓存源代码:

/**
* 此办法将始终缓存 -128 到 127(包含端点)范畴内的值,并能够缓存此范畴之外的其余值。*/
    public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    } 

利用场景:

  1. Integer i1=40;Java 在编译的时候会间接将代码封装成 Integer i1=Integer.valueOf(40);,从而应用常量池中的对象。
  2. Integer i1 = new Integer(40); 这种状况下会创立新的对象。
 Integer i1 = 40;
  Integer i2 = new Integer(40);
  System.out.println(i1==i2);// 输入 false 

Integer 比拟更丰盛的一个例子:

 Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);
  
  System.out.println("i1=i2" + (i1 == i2));
  System.out.println("i1=i2+i3" + (i1 == i2 + i3));
  System.out.println("i1=i4" + (i1 == i4));
  System.out.println("i4=i5" + (i4 == i5));
  System.out.println("i4=i5+i6" + (i4 == i5 + i6));   
  System.out.println("40=i5+i6" + (40 == i5 + i6)); 

后果:

i1=i2   true
i1=i2+i3   true
i1=i4   false
i4=i5   false
i4=i5+i6   true
40=i5+i6   true 

解释:

语句 i4 == i5 + i6,因为 + 这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行主动拆箱操作,进行数值相加,即 i4 == 40。而后 Integer 对象无奈与数值进行间接比拟,所以 i4 主动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比拟。

精彩举荐

Java 开发微服务畅购商城实战 - 全 357 集【附代码课件】

程序员摸鱼神器来了!下班也能够在 VSCode 看股票 & 基金数据啦!!!

扎心了!又被均匀了,盘点支出最高的 24 个开发人员职位

编程学习没方向?这 58 个网站助你成为程序员大牛【倡议珍藏】

互联网公司忽悠员工的黑话,网友自叹套路太深了

疑难?为什么有些大公司技术弱爆了?

正文完
 0