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

一、引言

对于从事 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个网站助你成为程序员大牛【倡议珍藏】

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

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

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据