背景:一谈到JVM一直是很多人觉得头疼的知识点,那么针对JVM这个痛点,我总结了一些,网上很多谈到由浅入深JVM,其实丑话说在前,一篇文章或者几篇文章是不够深入JVM的,但至少知其然。
PS:至于知其所以然,依旧还是推荐《深入理解JVM》这本书,虽说它很多还是基于JDK1.7去演示的,但万变不离其宗。且目前已有更新第三版,完全不用担心过时。周老师还是很强滴~~
首先我们来看一张图。由图我们可得知,JVM组成主要包含 堆、栈、元区间(方法区)、本地方法栈、PC寄存器等。
且JVM内存中包含 栈、本地方法栈、PC寄存器。且在1.8之前是包含方法区的。1.8之后揪出来放在了内存。
堆:只要new一个对象,就会存放在堆里。比如定义一个数组,堆数据所有线程都会共享。在Java虚拟机启动就建立堆,最主要的内存工作区域,几乎所有的对象实例都存放到Java堆中。我们可以认为堆中的变量是持久存在的,而栈的变量是临时态的。至于老年代新生代垃圾回收后期记载。
比如创建一个数组array:
public int[] array = new int[] {1, 2, 3};
那么该对象array就会存放在堆内存中,被所有线程共享。由此也会引发一个很头疼的问题,当类A与类B同时操作该数组时,会出现冲突,造成数据错乱,那么就产生线程安全问题。让人头疼。当然解决的方法也有很多种,JDK中syn锁,lock锁等都可以处理,这里跳过。
栈:栈她由一个个栈帧组成,那么栈帧是什么?其实就是一个一个的方法体,比如方法say,就是一个栈帧。
public static void say(String text){
String remark = "Hello world";
System.out.println(remark + text);
}
栈帧(方法体):那么通过方法体来形象的理解栈帧。每个栈帧都包含 局部变量表、操作数栈、动态链接、返回链接。
begin
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0(100)和1(98)的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加(100+98),再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果(198),并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。
看完以上一堆代码我们再来简单复述下,它对应的Java代码实际上就是
public static int add(){
int i0 = 100;
int i1 = 98;
// int i2 = i0 + i1; return i2;
//198
return i0 + i1;
}
是不是很简单?
public static void print(){
System.out.println(add());
}
本地方法栈,最大不同为本地方法栈用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通过使用C语言写)。
上图说到Native修饰的方法就是本地方法,那么有哪些呢?举个最通俗的例子。String类源码中的intern方法。
public native String intern();
那么该方法就是个本地方法,至于使用与作用,在下文方法区详细讲解。
方法区主要存放类的信息、常量信息、常量池信息、包括字符串字面量和数字常量等。
那么举个栗子来了解一下方法区
public static void main(String[] args) {
String a = "abc";
String b = "abc";
String c = new String("abc");
String d = "a";
System.out.println(a == b);//true
System.out.println(a == c);//false
System.out.println(a == c.intern());//true
System.out.println(a == d);//false
}
下面画图演示下ABC三者的关系图
首先根据a、b、c属于方法内局部变量,那么就是存放在栈中。其次是方法区字符串常量池中的"abc",是由方法内部创建而来。
至于堆中也会有个"abc",那是由于 c是通过new String()创建的。
细心的朋友会发现,代码中,a==c为false,a==c.intern()为什么会为true呢?
原因:调用了intern方法的字符串会将堆的值放入到常量池中,原理,被native关键字修饰。其实就是将JVM内存中的数据放入了本地内存中。相当于Hashset赋值,堆里面的数据就不会有了。
通过c.intern后的引用图状态。
千万注意!!! JDK1.6及之前是false,JDK1.7及以后为true。面试如果有指定JDK的坑的intern相关笔试题,就不要弄错了!
PC(Program Couneter)寄存器也是每个线程私有的空间, Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,如果是本地方法,则PC寄存器值为Underfined。
寄存器存放当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。
通俗点说,PC寄存器即为程序执行位置。
在上文中的栈帧中提到了汇编的操作,相信大家应该也是迫不及待的想知道原理,也想自己动手操作呢!那么教程
比如现在要汇编Test类
public class Test {
/*
汇编代码执行
0: bipush 100 将一个8位带符号整数压入栈(这里是压入100)
2: istore_1 将int类型值存入局部变量(其他类型有其他的规则)
3: bipush 99 同100压入
5: istore_2 将int类型值存入局部变量(其他类型有其他的规则)
6: iload_1 从局部变量中装载int类型值 (将100装载到操作数栈)
7: iload_2 从局部变量中装载int类型值 (装99载到操作数栈)
8: iadd 执行int类型的加法 (99+100)
9: istore_3 将int类型值存入局部变量(其他类型有其他的规则) 定义给第三个变量 c,存入局部变量
10: return 方法返回
*/
public void add(){
int a = 100;
int b = 99;
int c = a + b;
}
public static void main(String[] args) {
Test t = new Test();
t.add();
}
}
1.首先我们切到对应目录输入命令,注意是当前Test类在的目录
java -c Test.java
2.我们会发现出现不是处理文件,那么我们打开设置
4是名字,5是备注都可以更改
4:JavapUser
5:执行Javap命令,我要汇编我不管必须要行
6:$JDKPath$\bin\javap.exe
7:-v $FileClass$
8:$OutputPath$
3.编辑完成后,我们运行命令也不会成功的,至少我不会,那么如何做呢?
右键呼出菜单,选择External Tools点击显示的命令即可
JVM内存中主要包含栈(由多个栈帧组成,一个栈帧等于一个方法)、本地方法栈(Native修饰的方法,如String源码中的intern)、PC寄存器(程序执行位置),JDK1.7之前包含方法区(存放class对象、静态属性、常量池等等)。
1.8之后方法区移动到内存中,更名元区间,与堆(存放new对象)共处一室。