首先分析一下JDK
JVM :英文名称(Java Virtual Machine)运行 class 文件
JRE :英文名称(Java Runtime Environment)包括:jvm 的标准实现和 Java 的一些基本类库。
JDK :英文名称(Java Development Kit)它集成了 jre 和一些好用的小工具。例如:javac,javap,jar , jsonconsle 等。
谈一下高级语言的发展过程及JAVA到处运行的核心。
演化过程:机器语言 (0和1 二进制)-> 汇编语言 (用一些特定符号表示机器语言的码表)-> 高级语言 (java)
计算机只认识 0和1,每个系统对机器语言的解释是不同的。(例如:window :010101(随便写的) -> 可能解释成 1+1 ,Linux : 010101(随便写的)-> 可能解释成 1-1)
解释一下机器语言为什么不用我们更好理解的10进制来表示。
因为使用电子管来表示十种状态过于复杂,所以所有的电子计算机中只有两种基本的状态,开和关。(与或非门)也就是说,电子管的两种状态决定了以电子管为基础的电子计算机采用二进制来表示数字和数据。
刚开始,人们用 0 和 1也可以编写代码,一个简单的计算,这成本很大啊,要写多少个二进制,脑补一下。
我门穿越回去就面对这样的情况也会想办法改进的,看一下前人是怎么想的。他们想把 + - * / 等符号 编写一套码表出来。只需要编写固定码表就能表示机器码了。
比如”地球”,中文,英文,法文都有不同的单词去解释。这个编码也一样。(美国ASCII码, 中国GB2312码)
我们看一下java 中的汇编语言 class 文件
汇编器(理解:汇编语言和机器码的映射关系表) 能识别汇编语言 ->机器语言指令 。
后来的出现的高级语言也是因为逻辑太复杂,汇编语言也受不了,又偷懒,搞出一套高级语言。例如 : java c 等。
java到处运行的核心正是 有不同系统的jvm 去将Java 字节码换成系统识别的机器码而已。
我们学习了解JVM对我们有什么用那?
内存溢出:OutOfMemoryError StackOverflowError
什么情况会出现以上异常。我们就需要知道代码中的变量 对象 常量 类信息 等保存的位置,我们才能定位问题。
线程间变量的共享问题。什么情况会出现线程安全问题。我们了解一下,jvm中哪些是线程共享的,哪些是线程独享的就清楚了。
jvm 内存分布
jvm运行时数据区
1.程序计数器(线程独享)
程序计数器,它保存的是程序当前执行的指令的地址,用来指示 执行哪条指令的。
由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,
在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;
如果线程执行的是native方法,则程序计数器中的值是undefined。
2.虚拟机栈(线程独享)
每个方法在执行时会在虚拟机栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法返回地址、附加信息等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈(压栈)到出栈(弹栈)的过程。
虚拟机栈使用的内存不需要保证是连续的
虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候就已经确定。
如果线程请求分配的栈容量超过了 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异常
Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常
2.1栈帧(Stack Frame)
2.1.1:局部变量:用于存放方法参数和方法内部定义的局部变量。虚拟机通过索引定位的方式使用局部变量表。Class 文件格式属性表中 Code 属性的 max_locals 已经确定了栈的最大深度,最小存储单元 变量槽(Variable Slot)
2.1.2:操作数栈:方法的执行操作在操作数栈中完成。举个列子说明,
int i = 1;
int j = 2;
k = i + j;
上面 几行说明一下,首先 将i 压入栈中,再将j压入栈中。将i,j 出栈 运算后 如果是局部变量就设置到对应局部变量的对应索引的Slot中。
2.1.3动态连接
在 Class 文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用(1.类的全限定名,2.字段名和属性,3.方法名和属性),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。
另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。就是说,JVM不认识当前栈帧要执行那个方法。需要一个路径指定
2.1.4返回方法地址:虚拟机会使用针对每种返回类型的操作来返回,返回值将从操作数栈出栈并且入栈到调用方法的方法栈帧中,当前栈帧出栈,被调用方法的栈帧变成当前栈帧,程序计数器将重置为调用这个方法的指令的下一条指令。
3.本地方法栈(线程独享)
本地方法栈则是为虚拟机使用到的 Native 方法服务。(C C++ 代码)
当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
4.方法区(线程共享)
方法区在JVM中也是一个非常重要的区域,在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。在Class文件中除了类的字段、方法、接口等描述信息外,在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。
字符串常量池在JDK6的时候还是存放在方法区
JDK7后则将字符串常量池移到了Java堆中,
JDK8则是纯粹取消了方法区这个概念,取而代之的是”元空间(Metaspace)
5.堆(线程共享)
所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。
虚拟机规范Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。
堆也是GC垃圾回收的主要位置。堆内存分为新生代 (Young) 和老年代 (Old) ,新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor。
逃逸分析、栈上分配、标量替换等优化技术导致并不是所有对象都会在堆上分配。这里不再扩展
从内存分配的角度看,线程共享的 Java 堆中可能划分出多个线程私有的线程本地分配缓存区(Thread Local Allocation Buffer,TLAB)。
TLAB 在堆上开辟一小块
什么是栈上分配
因此,JVM提供了一种叫做栈上分配的概念,针对那些作用域不会逃逸出方法的对象,在分配内存时不在将对象分配在堆内存中,而是将对象属性打散后分配在栈(线程私有的,属于栈内存)上,这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给gc增加额外的无用负担,从而提升应用程序整体的性能
栈上分配如何开启
栈上分配需要有一定的前提
开启逃逸分析 (-XX:+DoEscapeAnalysis)
逃逸分析的作用就是分析对象的作用域是否会逃逸出方法之外,再server虚拟机模式下才可以开启(jdk1.6默认开启)
开启标量替换 (-XX:+EliminateAllocations)
标量替换的作用是允许将对象根据属性打散后分配再栈上,默认该配置为开启
如何查看逃逸分析的筛选结果
可以通过配置 -XX:+PrintEscapeAnalysis 开启打印逃逸分析筛选结果
网友评论