本文已经收录自笔者开源的 JavaGuide: github.com/Snailclimb (【Java学习+面试指南】 一份涵盖大部分Java程序员所须要掌握的核心知识)若是以为不错的还,不妨去点个Star,鼓励一下!html
若是没有特殊说明,都是针对的是 HotSpot 虚拟机。java
对于 Java 程序员来讲,在虚拟机自动内存管理机制下,再也不须要像 C/C++程序开发程序员这样为每个 new 操做去写对应的 delete/free 操做,不容易出现内存泄漏和内存溢出问题。正是由于 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,若是不了解虚拟机是怎样使用内存的,那么排查错误将会是一个很是艰巨的任务。c++
Java 虚拟机在执行 Java 程序的过程当中会把它管理的内存划分红若干个不一样的数据区域。JDK. 1.8 和以前的版本略有不一样,下面会介绍到。git
JDK 1.8 以前:程序员
JDK 1.8 :github
线程私有的:面试
线程共享的:算法
程序计数器是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都须要依赖这个计数器来完成。spring
另外,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。后端
从上面的介绍中咱们知道程序计数器主要有两个做用:
注意:程序计数器是惟一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的建立而建立,随着线程的结束而死亡。
与程序计数器同样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是经过栈传递的。
Java 内存能够粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是如今说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java 虚拟机栈是由一个个栈帧组成,而每一个栈帧中都拥有:局部变量表、操做数栈、动态连接、方法出口信息。)
局部变量表主要存放了编译器可知的各类数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不一样于对象自己,多是一个指向对象起始地址的引用指针,也多是指向一个表明对象的句柄或其余与此对象相关的位置)。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
Java 虚拟机栈也是线程私有的,每一个线程都有各自的 Java 虚拟机栈,并且随着线程的建立而建立,随着线程的死亡而死亡。
扩展:那么方法/函数如何调用?
Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:
无论哪一种返回方式都会致使栈帧被弹出。
和虚拟机栈所发挥的做用很是类似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会建立一个栈帧,用于存放该本地方法的局部变量表、操做数栈、动态连接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
Java 虚拟机所管理的内存中最大的一块,Java 堆是全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,所以也被称做GC 堆(Garbage Collected Heap).从垃圾回收的角度,因为如今收集器基本都采用分代垃圾收集算法,因此 Java 堆还能够细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在 JDK 7 版本及JDK 7 版本以前,堆内存被一般被分为下面三部分:
JDK 8 版本以后方法区(HotSpot 的永久代)被完全移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),中间一层属于老年代。
大部分状况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,若是对象还存活,则会进入 s0 或者 s1,而且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增长到必定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,能够经过参数 -XX:MaxTenuringThreshold
来设置。
修正(issue552):“Hotspot遍历全部对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,做为新的晋升年龄阈值”。
动态年龄计算的代码以下
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { //survivor_capacity是survivor空间的大小 size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); size_t total = 0; uint age = 1; while (age < table_size) { total += sizes[age];//sizes数组是每一个年龄段对象大小 if (total > desired_survivor_size) break; age++; } uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; ... } 复制代码
堆这里最容易出现的就是 OutOfMemoryError 错误,而且出现这种错误以后的表现形式还会有几种,好比:
OutOfMemoryError: GC Overhead Limit Exceeded
: 当JVM花太多时间执行垃圾回收而且只能回收不多的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space
:假如在建立新的对象时, 堆内存中的空间不足以存放新建立的对象, 就会引起java.lang.OutOfMemoryError: Java heap space
错误。(和本机物理内存无关,和你配置的对内存大小有关!)方法区与 Java 堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区也被称为永久代。不少人都会分不清方法区和永久代的关系,为此我也查阅了文献。
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的做用,并无规定如何去实现它。那么,在不一样的 JVM 上方法区的实现确定是不一样的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其余的虚拟机实现并无永久代这一说法。
JDK 1.8 以前永久代还没被完全移除的时候一般经过下面这些参数来调节方法区大小
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
复制代码
相对而言,垃圾收集行为在这个区域是比较少出现的,但并不是数据进入方法区后就“永久存在”了。
JDK 1.8 的时候,方法区(HotSpot 的永久代)被完全移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
下面是一些经常使用参数:
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
复制代码
与永久代很大的不一样就是,若是不指定大小的话,随着更多类的建立,虚拟机会耗尽全部可用的系统内存。
当你元空间溢出时会获得以下错误:
java.lang.OutOfMemoryError: MetaSpace
你可使用 -XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize
调整标志定义元空间的初始大小若是未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地从新调整大小。
元空间里面存放的是类的元数据,这样加载多少类的元数据就不禁 MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 历来没有一个叫永久代的东西, 合并以后就没有必要额外的设置这么一个永久代的地方了。
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各类字面量和符号引用)
既然运行时常量池是方法区的一部分,天然受到方法区内存的限制,当常量池没法再申请到内存时会抛出 OutOfMemoryError 错误。
JDK1.7 及以后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
直接内存并非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用。并且也可能致使 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它能够直接使用 Native 函数库直接分配堆外内存,而后经过一个存储在 Java 堆中的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样就能在一些场景中显著提升性能,由于避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,可是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
经过上面的介绍咱们大概知道了虚拟机的内存状况,下面咱们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。
下图即是 Java 对象的建立过程,我建议最好是能默写出来,而且要掌握每一步在作什么。
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,而且检查这个符号引用表明的类是否已被加载过、解析和初始化过。若是没有,那必须先执行相应的类加载过程。
在类加载检查经过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后即可肯定,为对象分配空间的任务等同于把一块肯定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:(补充内容,须要掌握)
选择以上两种方式中的哪种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",仍是"标记-整理"(也称做"标记-压缩"),值得注意的是,复制算法内存也是规整的
内存分配并发问题(补充内容,须要掌握)
在建立对象的时候有一个很重要的问题,就是线程安全,由于在实际开发过程当中,建立对象是很频繁的事情,做为虚拟机来讲,必需要保证线程是安全的,一般来说,虚拟机采用两种方式来保证线程安全:
内存分配完成后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头),这一步操做保证了对象的实例字段在 Java 代码中能够不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
初始化零值完成以后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不一样,如是否启用偏向锁等,对象头会有不一样的设置方式。
在上面工做都完成以后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象建立才刚开始,<init>
方法尚未执行,全部的字段都还为零。因此通常来讲,执行 new 指令以后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算彻底产生出来。
在 Hotspot 虚拟机中,对象在内存中的布局能够分为 3 块区域:对象头、实例数据和对齐填充。
Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是那个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各类类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位做用。 由于 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),所以,当对象实例数据部分没有对齐时,就须要经过对齐填充来补全。
创建对象就是为了使用对象,咱们的 Java 程序经过栈上的 reference 数据来操做堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:
这两种对象访问方式各有优点。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 自己不须要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
String 对象的两种建立方式:
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",若是字符串常量池中没有,则建立一个,而后 str1 指向字符串常量池中的对象,若是有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中建立一个新的对象
String str3 = new String("abcd");//堆中建立一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false
复制代码
这两种不一样的建立方法是有差异的。
记住一点:只要使用 new 方法,便须要建立新的对象。
再给你们一个图应该更容易理解,图片来源:www.journaldev.com/797/what-is…:
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 对象
复制代码
字符串拼接:
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。
将建立 1 或 2 个字符串。若是池中已存在字符串常量“abc”,则只会在堆空间建立一个字符串常量“abc”。若是池中没有字符串常量“abc”,那么它将首先在池中建立,而后在堆空间中建立,所以将建立总共 2 个字符串对象。
验证:
String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出 false,由于一个是堆内存,一个是常量池的内存,故二者是不一样的。
System.out.println(s1.equals(s2));// 输出 true
复制代码
结果:
false
true
复制代码
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);
}
复制代码
应用场景:
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面试突击》: 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本公众号后台回复 "Java面试突击" 便可免费领取!
Java工程师必备学习资源: 一些Java工程师经常使用学习资源公众号后台回复关键字 “1” 便可免费无套路获取。
做者的其余开源项目推荐:
安利一下阿里云双 12 的活动,1 核 2g 只要 89 一年,薅波羊毛,感受甚爽,不过最低的优惠都是新人才能享有的,我是用我女友的帐号买的,没有女友的,emm.....,能够考虑一下亲人的。