为什么要了解JVM?
在开发环境或者生产环境,你是否遇到过OutOfMemoryError(OOM)内存溢出问题?你是否遇到过tomcat容器中加载项目过多引起的OOM,导致web服务无法正常启动。这就是JVM引发的问题。
运行时数据区
java虚拟机在执行java程序的过程中会把它管理的内存划分为若干个不同的数据区域。分析JVM内存结构,主要就是分析JVM运行时数据存储区域。JVM运行时数据区域主要包括:堆、栈、方法区、程序计数器等。JVM优化问题主要集中在线程共享的数据区中:堆、方法区
程序计数器
程序计数器是一块较小的内存空间,可以看做是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来服务下一条指令。确切地说:一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令, 从而确保线程的正确执行。
为了确保线程切换后(上下文切换)能够恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器独立存储,也就是说程序计数器是线程私有的内存。
如果线程执行java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是native方法,计数器值为Undifined。
程序计数器不会发生内存溢出(oom)问题
栈
JVM中的栈包括Java虚拟机栈和本地方法栈,两者的区别是,java虚拟机栈为JVM执行Java方法服务,本地方法栈为JVM使用到的Native方法服务。
Native方法是什么?
JDK中有很多方法是使用Native修饰的,Native方法不是以java语言实现的,而是以本地语言实现的(例如C或者C++)。Native 方法是与操作系统直接交互的。比如通知垃圾收集器进行垃圾回收的代码 System.gc(),就是使用 native 修饰的。
public final class System {
public static void gc() {
Runtime.getRuntime().gc();
}
}
public class Runtime {
//使用native修饰
public native void gc();
什么是栈?
栈是线程私有的,生命周期和线程相同。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间。
栈帧是栈的元素。每个方法在执行的时候都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。
局部变量表
栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。
JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。
为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用。
操作数据栈
操作数据栈是一个后进先出栈。操作数栈的元素可以是任意的java数据类型。方法刚开始执行的时候,操作数据栈是空的,在方法执行的过程中,通过字节码指令对操作数据栈进行压栈和出栈的操作。通常进行算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候通过操作数据栈进行参数传递。操作数据栈可以理解为栈帧中用于计算的临时数据存储区。
栈中可能出现哪些异常?
StackOverflowError:栈溢出错误
如果一个线程在计算时所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出 StackOverflowError
OutOfMemoryError:内存不足
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
如何设置栈参数?
使用**-Xss设置栈大小,通常几百k就够了。由于栈是线程私有**的,线程数越多,占用栈空间越大。
**栈决定了函数调用的深度。**这也是慎用递归调用的原因。递归调用时,每次调用方法都会创建栈帧并压栈。当调用一定次数之后,所需栈的大小已经超过了虚拟机运行配置的最大栈参数,就会抛出 StackOverflowError 异常。
Java堆栈
堆栈是java虚拟机所管理的内存中的最大一块存储区域。堆内存被所有线程共享,主要存放使用new关键字创建的对象,所有对象实例以及数组都要在堆上分配。垃圾回收就是根据GC算法,收集堆上对象所占用的内存空间。
java堆分为年轻代和老年代,年轻代又分为伊甸园和幸存区,幸存区又纷纷为From Survivor和To Survivor空间。
年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。
老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。
注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。
方法区
java方法区与java堆一样是被所有线程共享的而区域,用于加载已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码。静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。
注:JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。元空间两个参数:
MetaSpaceSize:初始化元空间大小,控制发生GC阈值
MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串、final修饰的常量等。方法引用则包括接口类和接口的全限定名,方法名和描述符,字段名和描述符等。
常量池有什么用?
优点:常量池避免了频繁的创建和销毁对象而影响系统性能,实现了对象的共享。
举个栗子哦:Integer常量池(缓存池)和字符串常量池。
Integer常量池:
==基本数据类型比较的是数值,而引用数据类型比较的是内存地址。
public void TestIntegerCache()
{
public static void main(String[] args)
{
Integer i1 = new Integer(66);
Integer i2 = new integer(66);
Integer i3 = 66;
Integer i4 = 66;
Integer i5 = 150;
Integer i6 = 150;
System.out.println(i1 == i2);//false
System.out.println(i3 == i4);//true
System.out.println(i5 == i6);//false
}
}
i1 和 i2 使用 new 关键字,每 new 一次都会在堆上创建一个对象,所以 i1 == i2 为 false。i3 == i4 为什么是 true 呢?Integer i3 = 66 实际上有一步装箱的操作,即将 int 型的 66 装箱成 Integer,通过 Integer 的 valueOf 方法。
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 对象。
而 IntegerCache 是 Integer的静态内部类,作用就是将 [-128,127] 之间的数“缓存”在 IntegerCache 类的 cache 数组中,valueOf 方法就是调用常量池的 cache 数组,不过是将 i3、i4 变量引用指向常量池中,没有真正的创建对象。而new Integer(i)则是直接在堆中创建对象。
IntegerCache 类中,包含一个构造方法,三个静态变量:low最小值、high最大值、和Integer数组,还有一个静态代码块。静态代码块的作用就是在 IntegerCache 类加载的时候,对high最大值以及 Integer 数组初始化。也就是说当 IntegerCache 类加载的时候,最大最小值,和 Integer 数组就已经初始化好了。这个 Integer 数组其实就是包含了 -128到127之间的所有值。
private static class IntegerCache {
static final int low = -128;//最小值
static final int high;//最大值
static final Integer cache[];//缓存数组
//私有化构造方法,不让别人创建它。单例模式的思想
private IntegerCache() {}
//类加载的时候,执行静态代码块。作用是将-128到127之间的数缓冲在cache[]数组中
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];//初始化cache数组,根据最大最小值确定
int j = low;
for(int k = 0; k < cache.length; k++)//遍历将数据放入cache数组中
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
}
而 i5 == i6 为 false,就是因为 150 不在 Integer 常量池的最大最小值之间【-128,127】,从而 new 了一个对象,所以为 false。
public static void main(String[] args){
Integer i1 = new Integer(4);
Integer i2 = new Integer(6);
Integer i3 = new Integer(10);
System.out.print(i3 == i1+i2);//true
}
由于 i1 和 i2 是 Integer 对象,是不能使用+运算符的。首先 i1 和 i2 进行自动拆箱操作,拆箱成int后再进行数值加法运算。i3 也是拆箱后再与之比较数值是否相等的。所以 i3 == i1+i2 其实是比较的 int 型数值是否相等,所以为true。
string常量池
string是final修饰的类,是不可以被继承对的。通常有两种方式来创建对象:
//1、
String str = new String("abcd");
//2、
String str = "abcd";
第一种使用new创建的对象,存放在堆中。每次调用都会创建一个新的对象。
第二种先在栈上创建一个String类的对象引用变量str,然后通过富豪区引用字符串常量池中找有没有“abcd”,如果没有则将“abcd”存放到字符串变量中,并将栈上的str变量引用指向常量池中的“abcd”。如果常量池中已经有“abcd”了,则不会再创建,而是直接将str的引用指向常量池中的“abcd”。
对于string类,equals方法用于比较字符串内容是否相同。==用于比较内存地址是否相同,即是否指向同一个对象。
public static void main(String[] args){
String str1 = "abcd";
String str2 = "abcd";
System.out.print(str1 == str2);//true
}
首先在栈上存放变量引用 str1,然后通过符号引用去常量池中找是否有 abcd,没有,则将 abcd 存储在常量池中,然后将 str1 指向常量池的 abcd。当创建 str2 对象,去常量池中发现已经有 abcd 了,就将 str2 引用直接指向 abcd 。所以str1 == str2,指向同一个内存地址。
public static void main(String[] args){
String str1 = new String("abcd");
String str2 = new String("abcd");
System.out.print(str1 == str2);//false
}
str1和str2使用new创建对象,分别在堆上创建了不同的对象。俩昂个饮用指向堆中两个不同的对象,所以为false。
关于字符串+号连接的问题:
对于字符串常量的+号连接,在程序编译期JVM就会将其优化为+号连接后的值。所以在编译器其字符串常量的值就确定了。
String a = "a1";
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + "true";
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true
关于字符串引用+号连接的问题:
由于字符串引用在编译器是无法确定下来的,在程序的运行期动态分配并创建新的地址存储对象。
public static void main(String[] args){
String str1 = "a";
String str2 = "ab";
String str3 = str1 + "b";
System.out.print(str2 == str3);//false
}
对于以上代码,str3等于str1引用+字符串常量“b”,在编译期无法确定,在运行期动态地分配并将连接后新的地址复制给str3,所以2和3的引用内存地址不同。
过程:
new 一个 StringBuilder 对象,然后使用 append 方法优化了 + 操作符。new 在堆上创建对象,而 String s1=“ab”则是在常量池中创建对象,两个应用所指向的内存地址是不同的,所以 s1 == s2 结果为 false。
在 for 循环中使用 + 连接字符串,每循环一次,就会新建 StringBuilder 对象,append 后就“抛弃”了它。如果我们在循环外创建StringBuilder 对象,然后在循环中使用 append 方法追加字符串,就可以节省 n-1 次创建和销毁对象的时间。所以在循环中连接字符串,一般使用 StringBuilder 或者 StringBuffer,而不是使用 + 号操作
public static void main(String[] args){
StringBuilder s = new StringBuilder();
for(int i = 0; i < 100; i++){
s.append("a");
}
}
使用final修饰的字符串
public static void main(String[] args){
final String str1 = "a";
String str2 = "ab";
String str3 = str1 + "b";
System.out.print(str2 == str3);//true
}
final修饰的变量是一个常量,编译期就能确定其值。所以str1+"b"就等同于“a”+“b”,所以结果是true。
String对象的intern方法
public static void main(String[] args){
String s = "ab";
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2;
System.out.println(s3 == s);//false
System.out.println(s3.intern() == s);//true
}
通过前面学习我们知道,s1+s2 实际上在堆上 new 了一个 StringBuilder 对象,而 s 在常量池中创建对象 “ab”,所以 s3 == s 为 false。但是 s3 调用 intern 方法,返回的是s3的内容(ab)在常量池中的地址值。所以 s3.intern() == s 结果为 true。