1. 什么是JVM
我们都知道在 Windows 系统上一个软件包装包是 exe 后缀的,而这个软件包在苹果的 Mac OSX 系统上是无法安装的。类似地,Mac OSX 系统上软件安装包则是 dmg 后缀,同样无法在 Windows 系统上安装。
Java 代码为什么可以在 Windows 系统运行,也可以在 Linux 系统运行?这就是 jvm 的功劳, Java 虚拟机可以理解为一个翻译官,在 Linux 系统上将 Java 代码翻译成 Linux 机器码,在 Windows 系统上将 Java 代码翻译成 Windows 机器码,所以 Java 有了虚拟机之后,可以让 Java 代码运行在不同的系统上。
2. JVM内存结构(运行时数据区)
运行时数据区是官方说法,但很多时候这个名词并不是很形象,再加上日积月累的习惯,很多人都习惯用 JVM 内存结构这个说法
JVM 内存结构(运行时数据区)主要包括:堆、栈(虚拟机栈)、本地方法栈、方法区、程序计数器等。
线程公有:堆、方法区
线程私有:栈、本地方法栈、程序计数器
2.1 堆(公有)
堆内存被所有线程共享。堆内存用于存放由 new 创建的对象和数组。
Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为乐园(Eden)和幸存区(Survivor区);幸存区又分为 From 区(From Survivor 区)和 To 区(To Survivor 区)。
而 JVM 垃圾回收机制主要收集堆中年轻代和老年代对象所占用的内存空间。
为什么默认的虚拟机配置,Eden:from :to = 8:1:1 ?
这是经过大量统计得出的结果发现 80% 的对象存活时间都很短,于是将 Eden 区设置为年轻代的 80%,这样可以减少内存空间的浪费,提高内存空间利用率。
年轻代中的 Minor GC
1、绝大多数刚刚被创建的对象会存放在乐园(Eden)。
2、在乐园内存满时,执行第一次GC(Minor GC)之后,存活的对象被移动到其中一个幸存区(Survivor)。
3、此后,每次乐园执行GC后,存活的对象会被堆积在同一个幸存区。
4、当一个幸存区饱和,还在存活的对象会被移动到另一个幸存区。然后会清空已经饱和的那个幸存区。
5、在以上步骤中重复N次(N = MaxTenuringThreshold(年龄阀值设定,默认15))依然存活的对象,就会被移动到老年代。
从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。
需要重点记住的是,对象在刚刚被创建之后,是保存在乐园的(Eden)。那些长期存活的对象会经由幸存区(Survivor)转存到老年代(Old generation)。
也有例外出现,对于一些比较大的对象(需要分配一块比较大的连续内存空间)则直接进入到老年代(内存分配担保机制)。
注意:当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden满,Survivor满不会引发Minor GC。由于年轻代中的对象存活时间比较短,Minor GC比较频繁,GC 速度也很快。
老年代中的 Full GC
老年代空间的构成很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代一样存活时间很短。这里的对象几乎都是从Survivor 区中熬过来的,它们绝不会轻易的被回收掉。
注意:Full GC 是清理整个堆空间,包括年轻代和老年代,如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。由于老年代内存不会轻易的被回收掉,因此 Full GC 发生的次数不会有 Minor GC 那么频繁。但是老年代内存大,做一次 Full GC 的时间比 Minor GC 要更长(约10倍)。
2.2 方法区(公有)
方法区被所有线程共享。方法区用于存放静态变量、常量、类信息(版本、方法、字段等)、常量池。可以看做是将类(Class)的元数据,保存在方法区里。
Integer常量池
都知道数据类型 == 比较的是内存地址,先看个下边的例子。
public static void main(String[] args)
{
Integer i1 = 66;
Integer i2 = 66;
Integer i3 = 150;
Integer i4 = 150;
System.out.println(i1 == i2);//true
System.out.println(i3 == i4);//false
}
i1 == i2 结果为 true,i3 == i4 结果为 false。由结果得知 i1 和 i2 的内存地址是相同的,而 i3 和 i4 内存地址是不同的。
产生这样结果的原因是 Integer i1 = 66 实际上有一步装箱的操作,通过 Integer 的 valueOf 方法将 int 型的 66 装箱成 Integer。下边是 Integer 中的 valudOf 方法。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer 的 valueOf 方法很简单,它判断变量是否在 IntegerCache 的最小值(-128)和最大值(127)之间,如果在,则返回常量池中的内容,否则 new 一个 Integer 对象。
由于 66 在 -128 ~ 127 之间,所以 66 装箱时,使用的是常量池中的 66,所以 == 结果为 true。
而 150 不在范围内,在装箱时执行了 new Integer(150),所以返回的是新创建的对象,所以 == 结果为 false。
String常量池
String 是由 final 修饰的类,是不可以被继承的。通常有两种方式来创建对象。
// 1
String str = new String("abc");
// 2
String str = abc;
第一种:使用 new 创建的对象,存放在堆中,每次 new 出来的内存地址都不同。
第二种:先在常量池中找有没有 “abc”。有,则直接取常量池内存地址赋值给 str。没有,先在常量池创建“abc”,再取内存地址赋值给 str。
通过代码验证上面理论。
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("abc");
String s3 = "abc";
String s4 = "abc";
System.out.println(s1 == s2);// false
System.out.println(s3 == s4);// true
}
s1 == s2 为 false 原因:str1 和 str2 使用 new 创建对象,分别在堆上创建了不同的对象。两个引用指向堆中两个不同的对象,所以为 false。
s3 == s4 为 true 原因:首先在栈上存放变量引用 s3,然后去常量池中找是否有 abc,没有,则将 abc 存储在常量池中,然后将 s3 指向常量池的 abc。当 s4 = "abc" 时,去常量池中发现已经有 abc 了,就将 s4 引用指向常量池已有的 abc 。所以s3 == s4,指向同一个内存地址。
String 类中有一个方法 intern,可以返回池中的字符串,如下代码
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1 == s2);// false
System.out.println(s1.intern() == s2);// true
}
上边的结果可以看下 intern() 方法注释就知道结果。当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。
2.3 栈(私有)
栈是后进先出的。栈是线程私有的,他的生命周期与线程相同。每个线程都会分配一个栈的空间,一个线程会对应一个栈。
栈存储什么
栈中存储的是栈帧。每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。可以理解为栈帧就是线程所执行的方法。
使用递归时,会导致 StackOverflowError 错误,就是因为不断的在栈中创建栈帧,当栈帧的数量超过了栈的大小时,就会导致报错。
2.4 本地方法栈(私有)
本地方法栈是线程私有的,主要为 JVM 使用到的 Native 方法服务。Native 方法不是以 Java 语言实现的,而是以本地语言实现的(比如 C 或 C++)。
可以理解为 Native 方法是与操作系统直接交互的,比如通知垃圾收集器进行垃圾回收的代码 System.gc(),获取常量池中的字符串引用 String.intern(),都是使用 native 修饰的。
2.5 程序计数器(私有)
程序计数器是一个比较小的内存区域,可能是CPU寄存器或者操作系统内存,其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。
字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。