一. java结构体系
Description of Java Conceptual Diagram(java结构)
我们经常说到JVM调优,JVM和JDK到底什么关系,大家知道么?这是java基础。
这幅图很重要,一定要了解其结构。这是jdk的结构图。从结构上可以看出java结构体系, JDK主要包含两部分:
第一部分:是java 工具(Tools&Tool APIs)
比如java, javac, javap等命令. 我们常用的命令都在这里
第二部分: JRE(全称:Java Runtime Enveriment), jre是Java的核心,。
jre里面定义了java运行时需要的核心类库, 比如:我们常用的lang包, util包, Math包, Collection包等等.这里还有一个很重要的部分JVM(最后一部分青色的) java 虚拟机, 这部分也是属于jre, 是java运行时环境的一部分. 下面来详细看看:
- 最底层的是Java Virtual Machine: java虚拟机
- 常用的基础类库:lang and util。在这里定义了我们常用的Math、Collections、Regular Expressions(正则表达式),Logging日志,Reflection反射等等。
- 其他的扩展类库:Beans,Security,Serialization序列化,Networking网络,JNI,Date and Time,Input/Output等。
- 集成一体化类库:JDBC数据库连接,jndi,scripting等。
- 用户接口工具:User Interface Toolkits。
- 部署工具:Deployment等。
从上面就可看出,jvm是整个jdk的最底层。jvm是jdk的一部分。
二. java语言的跨平台特性
1. java语言是如何实现跨平台的?
跨平台指的是, 程序员开发出的一套代码, 在windows平台上能运行, 在linux上也能运行, 在mac上也能运行. 我们知道, 机器最终运行的指令都是二进制指令. 同样的代码, 在windows上生成的二进制指令可能是0101, 但是在linux上是1001, 而在mac上是1011。这样同样的代码, 如果要想在不同的平台运行, 放到相应的平台, 就要修改代码, 而java却不用, 那么java这种跨平台特性是怎么做到的呢?
原因在于jdk, 我们最终是将程序编译成二进制码,把他丢在jvm上运行的, 而jvm是jre的一部分. 我们在不同的平台下载的jdk是不同的. windows平台要选择下载适用于windows的jdk, linux要选择适用于linux的jdk, mac要选择适用于mac的jdk. 不同平台的jvm针对该平台有一个特定的实现, 正是这种特点的实现, 让java实现了跨平台。
2. 延伸思考
通过上面的分析,我们知道能够实现跨平台是因为jvm封装了变化。我们经常说进行jvm调优,那么在不同平台的调优参数可以通用么?显然是不可以的。不同平台的jvm尤其个性化差异。
封装变化的部分是JDK中的jvm,JVM的整体结构是怎样的呢?来看下面一个部分。
三. JVM整体结构和内存模型
1.JVM由三部分组成:
- 类装载子系统
- 运行时数据区(内存模型)
- 字节码执行引擎
其中类装载子系统是C++实现的, 他把类加载进来, 放入到虚拟机中. 这一块就是之前分析过的类加载器加载类,采用双亲委派机制,把类加载进来放入到jvm虚拟机中。
然后, 字节码执行引擎去虚拟机中读取数据. 字节码执行引擎也是c++实现的. 我们重点研究运行时数据区。
2.运行时数据区的构成
运行时数据区主要由5个部分构成: 堆,栈,本地方法栈,方法区,程序计数器
3.JVM三部分密切配合工作
下面我们来看看一个程序运行的时候, 类装载子系统, 运行时数据区, 字节码执行引擎是如何密切配合工作的?
我们举个例子来说一下:
package com.lxl.jvm; public class Math { public static int initData = 666; public static User user = new User(); public int compute() { int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Math math = new Math(); math.compute(); } }
当我们在执行main方法的时候, 都做了什么事情呢?
第一步: 类加载子系统加载Math.class类, 然后将其丢到内存区域, 这个就是前面博客研究的部分,类加载的过程, 我们看源码也发现,里面好多代码都是native本地的, 是c++实现的
第二步: 在内存中处理字节码文件, 这一部分内容较多, 也是我们研究的重点, 后面会对每一个部分详细说
第三步: 由字节码执行引擎执行java虚拟机中的内存代码, 而字节码执行引擎也是由c++实现的
这里最核心的部分是第二部分运行时数据区(内存模型), 我们后面的调优, 都是针对这个区域来进行的.
下面详细来说内存区域
这是java的内存区域, 内存区域干什么呢?内存区域其实就是放数据的,各种各样的数据j放在不同的内存区域
四. 栈
栈是用来存放变量的
4.1. 栈空间
还是用Math的例子来说,当程序运行的时候, 会创建一个线程, 创建线程的时候, 就会在大块的栈空间中分配一块小空间, 用来存放当前要运行的线程的变量
public static void main(String[] args) { Math math = new Math(); math.compute(); }
比如,这段代码要运行,首先会在大块的栈空间中给他分配一块小空间. 这里的math这个局部变量就会被保存在分配的小空间里面.
在这里面我们运行了math.compute()方法, 我们看看compute方法内部实现
public int compute() { int a = 1; int b = 2; int c = (a + b) * 10; return c; }
这里面有a, b, c这样的局部变量, 这些局部变量放在那里呢? 也放在上面分配的栈小空间里面.
效果如上图, 在栈空间中, 分配一块小的区域, 用来存放Math类中的局部变量
如果再有一个线程呢? 我们就会再次在栈空间中分配一块小的空间, 用来存放新的线程内部的变量
同样是变量, main方法中的变量和compute()方法中的变量放在一起么?他们是怎么放得呢?这就涉及到栈帧的概念。
4.2. 栈帧
1.什么是栈帧呢?
package com.lxl.jvm; public class Math { public static int initData = 666; public static User user = new User(); public int compute() { int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Math math = new Math(); math.compute(); } }
还是这段代码, 我们来看一下, 当我们启动一个线程运行main方法的时候, 一个新的线程启动,会现在栈空间中分配一块小的栈空间。然后在栈空间中分配一块区域给main方法,这块区域就叫做栈帧空间.
当程序运行到compute()计算方法的时候, 会要去调用compute()方法, 这时候会再分配一个栈帧空间, 给compute()方法使用.
2.为什么要将一个线程中的不同方法放在不同的栈帧空间里面呢?
一方面: 我们不同方法里的局部变量是不能相互访问的. 比如compute的a,b,c在main里不能被访问到。使用栈帧做了很好的隔离作用。
另一方面: 方便垃圾回收, 一个方法用完了, 值也返回了, 那他里面的变量就是垃圾了, 后面直接回收这个栈帧就好了.
如下图, 在Math中两个方法, 当运行到main方法的时候, 会将main方法放到一块栈帧空间, 这里面仅仅是保存main方法中的局部变量, 当执行到compute方法的时候, 这时会开辟一块compute栈帧空间, 这部分空间仅存放compute()方法的局部变量.
不同的方法开辟出不同的内存空间, 这样方便我们各个方法的局部变量进行管理, 同时也方便垃圾回收.
3.java内存模型中的栈算法
我们学过栈算法, 栈算法是先进后出的. 那么我们的内存模型里的栈和算法里的栈一样么?有关联么?
我们java内存模型中的栈使用的就是栈算法, 先进后出.举个例子, 还是这段代码
package com.lxl.jvm; public class Math { public static int initData = 666; public static User user = new User(); public int compute() { int a = 1; int b = 2; int c = (a + b) * 10; return c; } public int add() { int a = 1; int b = 2; int c = a + b; return c; } public static void main(String[] args) { Math math = new Math(); math.compute(); math.add(); // 注意这里调用了两次compute()方法 } }
这时候加载的内存模型是什么样呢?
- 最先进入栈的是main方法, 会首先在线程栈中分配一块栈帧空间给main方法。
- main方法里面调用了compute方法, 然后会在创建一个compute方法的栈帧空间, 我们知道compute方法后加载,但是他却会先执行, 执行完以后, compute中的局部变量就会被回收, 那么也就是出栈.
- 然后在执行add方法,给add方法分配一块栈帧空间。add执行完以后出栈。
- 最后执行完main方法, main方法最后出栈. 这个算法刚好验证了先进后出. 后加载的方法会被先执行. 也符合程序执行的逻辑。
4.3 栈帧的内部构成
我们上面说了, 每个方法在运行的时候都会有一块对应的栈帧空间, 那么栈帧空间内部的结构是怎样的呢?
栈帧内部有很多部分, 我们主要关注下面这四个部分:
1. 局部变量表 2. 操作数栈 3. 动态链接 4. 方法出口
4.2.1 局部变量表: 存放局部变量
局部变量表,顾名思义,用来存放局部变量的。
4.2.2 操作数栈
那么操作数栈,动态链接, 方法出口他们是干什么的呢? 我们用例子来说明操作数栈
那么这四个部分是如何工作的呢?
我们用代码的执行过程来对照分析.
我们要看的是jvm反编译后的字节码文件, 使用javap命令生成反编译字节码文件.
javap命令是干什么用的呢? 我们可以查看javap的帮助文档
主要使用javap -c和javap -v
javap -c: 对代码进行反编译 javap -v: 输出附加信息, 他比javap -c会输出更多的内容
下面使用命令生成一个Math.class的字节码文件. 我们将其生成到文件
javap -c Math.class > Math.txt
打开Math.txt文件, 如下. 这就是对java字节码反编译成jvm汇编语言.
Compiled from "Math.java" public class com.lxl.jvm.Math { public static int initData; public static com.lxl.jvm.User user; public com.lxl.jvm.Math(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int compute(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn public static void main(java.lang.String[]); Code: 0: new #2 // class com/lxl/jvm/Math 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method compute:()I 12: pop 13: return static {}; Code: 0: sipush 666 3: putstatic #5 // Field initData:I 6: new #6 // class com/lxl/jvm/User 9: dup 10: invokespecial #7 // Method com/lxl/jvm/User."<init>":()V 13: putstatic #8 // Field user:Lcom/lxl/jvm/User; 16: return }
这就是jvm生成的反编译字节码文件.
要想看懂这里面的内容, 我们需要知道jvm文档手册. 现在我们不会没关系, 参考文章(https://www.cnblogs.com/ITPower/p/13228166.html)最后面的内容, 遇到了就去后面查就行了
我们以compute()方法为例来说说这个方法是如何在在栈中处理的
源代码 public int compute() { int a = 1; int b = 2; int c = (a + b) * 10; return c; } 反编译后的jvm指令 public int compute(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn
jvm的反编译代码是什么意思呢? 我们对照着查询手册
0: iconst_1 将int类型常量1压入操作数栈, 这句话的意思就是先把int a=1;中的1先压入操作数栈
1: istore_1 将int类型值存入局部变量1-->意思是将int a=1; 中的a变量存入局部变量表
注意: 这里的1不是变量的值, 他指的是局部变量的一个下标. 我们看手册上有局部变量0,1,2,3
0表示的是this, 1表示将变量放入局部变量的第二个位置, 2表示放入第三个位置.
对应到compute()方法,0表示的是this, 1表示的局部变量a, 2表示局部变量b,3表示局部变量c
1: istore_1 将int类型值存入局部变量1-->意思是将int a=1; 中的a放入局部变量表的第二个位置, 然后让操作数栈中的1出栈, 赋值给a
2: iconst_2 将int类型常量2压入栈-->意思是将int b=2;中的常量2 压入操作数栈