哈哈 皮一下!我本身开源的一个Java学习指南文档。一份涵盖大部分Java程序员所须要掌握的核心知识,正在一步一步慢慢完善,期待您的参与。Github地址:github.com/Snailclimb/… 。看一眼,我以为你必定不会后悔,若是能够的话,能够给个Star鼓励一下!html
对于 Java 程序员来讲,在虚拟机自动内存管理机制下,再也不须要像C/C++程序开发程序员这样为内一个 new 操做去写对应的 delete/free 操做,不容易出现内存泄漏和内存溢出问题。正是由于 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,若是不了解虚拟机是怎样使用内存的,那么排查错误将会是一个很是艰巨的任务。java
Java 虚拟机在执行 Java 程序的过程当中会把它管理的内存划分红若干个不一样的数据区域。 git
线程私有的:程序员
线程共享的:github
程序计数器是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都须要依赖这个计数器来完。面试
另外,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。算法
从上面的介绍中咱们知道程序计数器主要有两个做用:数组
注意:程序计数器是惟一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的建立而建立,随着线程的结束而死亡。缓存
与程序计数器同样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。安全
Java 内存能够粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是如今说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java虚拟机栈是由一个个栈帧组成,而每一个栈帧中都拥有:局部变量表、操做数栈、动态连接、方法出口信息。)
局部变量表主要存放了编译器可知的各类数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不一样于对象自己,多是一个指向对象起始地址的引用指针,也多是指向一个表明对象的句柄或其余与此对象相关的位置)。
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
Java 虚拟机栈也是线程私有的,每一个线程都有各自的Java虚拟机栈,并且随着线程的建立而建立,随着线程的死亡而死亡。
和虚拟机栈所发挥的做用很是类似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会建立一个栈帧,用于存放该本地方法的局部变量表、操做数栈、动态连接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
Java 虚拟机所管理的内存中最大的一块,Java 堆是全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,所以也被称做GC堆(Garbage Collected Heap).从垃圾回收的角度,因为如今收集器基本都采用分代垃圾收集算法,因此Java堆还能够细分为:新生代和老年代:在细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
推荐阅读:
方法区与 Java 堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上二者并不等价。仅仅是由于 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就能够像管理 Java 堆同样管理这部份内存了。可是这并非一个好主意,由于这样更容易遇到内存溢出问题。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并不是数据进入方法区后就“永久存在”了。
运行时常量池是方法区的一部分。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 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不一样,如是否启用偏向锁等,对象头会有不一样的设置方式。
⑤执行 init 方法: 在上面工做都完成以后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象建立才刚开始,<init>
方法尚未执行,全部的字段都还为零。因此通常来讲,执行 new 指令以后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算彻底产生出来。
在 Hotspot 虚拟机中,对象在内存中的布局能够分为3快区域:对象头、实例数据和对齐填充。
Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是那个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各类类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位做用。 由于Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),所以,当对象实例数据部分没有对齐时,就须要经过对齐填充来补全。
创建对象就是为了使用对象,咱们的Java程序经过栈上的 reference 数据来操做堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:
句柄: 若是使用句柄的话,那么Java堆中将会划分出一块内存来做为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
直接指针: 若是使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优点。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 自己不须要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
1 String 对象的两种建立方式:
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false
复制代码
这两种不一样的建立方法是有差异的,第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间建立一个新的对象。
2 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 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"。
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-Guide:一份涵盖大部分Java程序员所须要掌握的核心知识,正在一步一步慢慢完善,期待您的参与。
Github地址:github.com/Snailclimb/…
参考:
你若怒放,清风自来。 欢迎关注个人微信公众号:“Java面试通关手册”,一个有温度的微信公众号。公众号有大量资料,回复关键字“1”你可能看到想要的东西哦!