本文为本人的JVM的学习记录,适合有一定的java编程基础( J2SE)并希望进一步理解java的程序员,虚拟机爱好者,jvm实践者。
大多数java开发工程师,都是处于使用框架阶段,使用java api开发系统,很少有关注java底层核心技术jvm,对其了解的很少。如果我们把java核心类库的API比做数学公式的话,那么java虚拟机jvm的知识就好比公式的推导过程。
关于学习资料推荐:
关于官方文档:
https://docs.oracle.com/javase/specs/index.html
所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机
无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。
java技术的核心就是java虚拟机**(JVM,Java Virtual Machine)**,因为所有的java程序都运行在java虚拟机内部。
优势
跨平台性、优秀的垃圾回收器,以及可靠的即时编译器。
作用
java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条java指令,java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数, 处理结果存储在哪里。
特点
随着java7的正式发布,java虚拟机的设计者们通过JSR-292规范基本实现:在java虚拟机平台上运行非java语言编写的程序。
java虚拟机根本不关心运行在其内部的程序是使用何种编程语言编写的,它只关心“字节码”文件。也就是java虚拟机拥有语言无关性,并不会单纯地与java语言“终身绑定”,只要其他编程语言的编译结果满足并包含java虚拟机的内部指令集、符号表以及其他的辅助信息,它就是一个有效的字节码文件,就能被虚拟机所识别并装载运行。
多语言混合编程
java平台上的多语言混合编程逐渐称为主流,通过特定领域的语言去解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向。
试想一下,在一个项目之中,并行处理用Clojure语言编写,展示层使用JRuby/Rails,中间层是java,每个应用层都将使用不同的编程语言来完成,而且,接口对每一层的对于开发者都是透明的,各种语言之间的交互不存在任何困难,就使用自己语言的原生API一样方便,因为他们最终都运行在一个虚拟机上。
对这些运行与java虚拟机之上、java之外的语言,来自系统级的、底层的支持正在迅速增强,以 JSR-292为核心的一系列项目和功能改进(如Davinci Machine项目、Nashorn引擎),推动java虚拟机 从**"java语言的虚拟机"向"多语言虚拟机’’**的方向发展。
javac Demo.java
//javac:命令将其编程成字节码文件
//java:命令来执行class字节码文件
javap -v Demo
//javap:是将字节码进行反编译(与javac对应),可以查看java编译器为我们生成的字节码。
package com.nyf;
public class Demo {
private int a = 1;
public void testMethod(){
System.out.println("testMethod");
}
}
运行出来大概是这样:
~/IdeaProjects/day30 ⍉
▶ cd src/com/nyf/
src/com/nyf
▶ javac Demo.java
src/com/nyf
▶ javap -v Demo
警告: 文件 ./Demo.class 不包含类 Demo
Classfile /Users/monologuist/IdeaProjects/day30/src/com/nyf/Demo.class
Last modified 2020年6月16日; size 423 bytes
SHA-256 checksum fd90b5ec57cf96746d17b0ca80967ccb1b9db4fae909b8cf9a76cbfed216744c
Compiled from "Demo.java"
public class com.nyf.Demo
minor version: 0
major version: 57
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // com/nyf/Demo
super_class: #2 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "":()V
#4 = Utf8 java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // com/nyf/Demo.a:I
#8 = Class #10 // com/nyf/Demo
#9 = NameAndType #11:#12 // a:I
#10 = Utf8 com/nyf/Demo
#11 = Utf8 a
#12 = Utf8 I
#13 = Fieldref #14.#15 // java/lang/System.out:Ljava/io/PrintStream;
#14 = Class #16 // java/lang/System
#15 = NameAndType #17:#18 // out:Ljava/io/PrintStream;
#16 = Utf8 java/lang/System
#17 = Utf8 out
#18 = Utf8 Ljava/io/PrintStream;
#19 = String #20 // testMethod
#20 = Utf8 testMethod
#21 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
#22 = Class #24 // java/io/PrintStream
#23 = NameAndType #25:#26 // println:(Ljava/lang/String;)V
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (Ljava/lang/String;)V
#27 = Utf8 Code
#28 = Utf8 LineNumberTable
#29 = Utf8 SourceFile
#30 = Utf8 Demo.java
{
public com.nyf.Demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iconst_1
6: putfield #7 // Field a:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
public void testMethod();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #19 // String testMethod
5: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
}
SourceFile: "Demo.java"
我们平时说的java字节码指的是用java语言编译成的字节码。准确的说任何能在jvm平台上执行的字节码格式都是一样的。所以应该统称为: jvm字节码。
不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的jvm上运行。
java虚拟机与java语言并没有必然的联系,它只与特定的二进制文件格式-class文件格式所关联, class文件中包含了java虚拟机指令集(或者称为字节码,Bytecodes)和符号表,还有一些其他辅助信息。
字节码和机器码的区别
高级语言:最接近人类语言,但机器是无法执行的,需要最终编译度连接成二进制的机器代码才可被计 算机执行
汇编语言:是将二进制的机器码通过抄助记符的方式让人可以更方便的编写并检查的低级语言,汇编语言接近机器语言,可以看做是机袭器语言的另一种形式,计算机在运行时也需要将其变为机器语言的二进制才可运行
字节码:是一种中间状态(中间码)的二进制代码(文件),需要直译器转译后才能成为机器码。 机器语言:是计算机可以识别并运行的二进制代码,机器码是电脑CPU直接读取运行的机器指令,运行速度最快,但是非常晦涩难懂,也比较难编写,一般从业人员接触不到。
Oracle/Sun JDK 中使用的 JVM 是 HotSpot VM.
提起HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。
➜ ~ java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
Oracle JDK
Oracle JDK由Oracle公司开发,该公司是Sun许可证,基于Java标准版规范实现。它以二进制产品 的形式发布。它支持多种操作系统,如Windows,Linux,Solaris,MacOS等。它支持不同的平台,如 Intel 32位和64位架构,ARM架构和SPARC。它完全基于Java编程语言。之后,该许可证宣布将根据 GPL(通用公共许可证)许可证发布。Oracle JDK包含许多组件作为库形式的编程工具集合。
OpenJDK
历史上的原因是,OpenJDK是JDK的开放源码版本,以GPL协议的形式发布。(General Public License)
在JDK7的时候,OpenJDK已经成为JDK7的主干开发版,SUN JDK7是在OpenJDK7的基础上发布的,其大部分源码都相同,只有少部分源码被替换掉。
使用JRL(Java Research License,Java研究授权协议)发布。
OpenJDK是Java SE平台版的开源和免费实现,它是Sun Corporation(现在的Oracle Corporation)于2006年开始的开发结果。它是根据GNU GPL许可证授权的。它最初于2007年发布。 它由Oracle Corporation,Red Hat,IBM,Apple Inc.,OpenJDK和Java Community等开发。它是使 用C ++和Java编程语言编写的。它支持不同的操作系统,如FreeBSD,Linux,Microsoft Windows, Mac OS X。OpenJDK是Java SE Platform Edition的官方参考实现。
对比
Oracle JDK可用于开发Java Web应用程序,独立应用程序以及许多其他图形用户界面以及其他开发 工具。Oracle JDK执行的所有操作或任务也可以由OpenJDK执行,但只有Oracle与OpenJDK之间的区别 在于Open JDK在现有Oracle JDK之上的许可和其他工具集成和实现。使用OpenJDK的优点是可以根据 应用程序的要求修改性能,可伸缩性和实现,以根据需要调整Java虚拟机。
OpenJDK的优势更多,Oracle JDK的使用在Oracle JDK实现中使用的标准方面也有一些好处,这将确保应用程序稳定和良好维护。
注意: 对于Java 11之前,两者有少部分的区别,Oracle JDK有一些自己独有的东⻄。但是Java 11 之后这两者几乎没有区别,图中提示了两者共同代码的占比要远高于图形上看到的比例, 所以我们编译 的OpenJDK基本上可以认为性能、功能和执行逻辑上都和官方的Oracle JDK是一致的.
说明
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载这操作系统和应用程序的实时 运行。JVM内存布局规定了java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运 行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经 典的JVM布局。
HotSpot VM是目前市面上高性能虚拟机的代表之一。它采用解释器和即时编译器并存的架构。在 今天,java程序的运行性能早已脱胎换⻣,已经到了可以和C/C++程序一较高下的地步。
进程与线程
进程(process)是具有一定独立功能的程序,操作系统利用进程把工作划分为一些功能单元。 进 程是进行资源分配和调度的一个独立单位。它还拥有一个私有的虚拟地址空间,该空间仅能被它所包含 的线程访问。 一个应用程序(application)是由一个或多个相互协作的进程组成的。
线程(thread)是进程中所包含的一个或多个执行单元。它只能归属于一个进程并且只能访问该进 程所拥有的资源。 它进程中执行运算的最小单位,是进程中的一个实体,是被进程独立调度和分派的基 本单位。 线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源(计数器、寄存器和栈),但 它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同 一进程中的多个线程之间可以并发执行。 当操作系统创建一个进程后,该进程会自动申请一个名为主线 程(首要线程)的线程。
首先,进程和线程如同列⻋和⻋厢,没有可比性,但是他们有一定的相关性:
方法区是Java虚拟机规范中的定义,是一种规范,而永久代和元空间是 HotSpot VM 不同版本的两种实现。
jdk1.7及之前,HotSpot虚拟机对于方法区的实现称之为“永久代”, Permanent Generation 。 jdk1.8之后,HotSpot虚拟机对于方法区的实现称之为“元空间”, Meta Space 。
java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,这些是线程共享的;另外一些则是与线程一一对应的,这些与线程对应 的数据区域会随着线程开始和结束而创建和销毁,这些是属于线程独享的。
代码缓存:JVM在运行时会将频繁调用方法的字节码编译为本地机器码。这部分代码所占用的内存 空间成为CodeCache区域。Java进行JIT的时候,会将编译的本地代码放在codecache中。
每个JVM只有一个Runtime实例,即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。
这里源码也可以看到,Runtime采用的是单例模式,也就是说只会new一个Runtime对象,且用private修饰,不允许开发者自己去new,你只能去调用它的静态方法。
官方地址: https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
JVM中的程序计数寄存器(Program Counter Register),Register的命名源于CPU寄存器,寄存器存储指令地址(或者称偏移地址),CPU只有把数据装载到寄存器才能运行,
在这里,并非是广义上指的物理寄存器,或许翻译成PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
PC寄存器用来存储指向下一条指令的地址,也就是即将要执行的指令代码。由执行引擎读取下一条指令。
public class PCRegisterTest {
public static void main(String[] args){
int i = 100;
int j = 200;
int m = i + j;
String str = "a";
System.out.println(m);
System.out.println(str);
}
}
1 为什么使用PC寄存器记录当前线程的执行地址?
因为CPU需要不停地切换线程,这时候切换回来以后,线程就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
2 PC寄存器为什么被设定为线程私有?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
由于CPU时间片轮换限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫不差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
由于跨平台性的设计,java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
栈是运行时的单位,而堆是存储的单位;
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
java虚拟机栈是什么?
java虚拟机栈(java virtual Machine Stack),早期也叫java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的java方法调用。它是线程私有的。
生命周期
生命周期和线程一致。
作用
主管java程序的运行,它保存方法的局部变量**(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的返回和调用**
栈
栈是一种动态集合,它是一种LIFO(last in first out后进先出)结构
队列
与栈不同,它是一种FIFO(first in first out先进先出)结构
public class StackTest {
public static void main(String[] args) {
StackTest stackTest = new StackTest();
stackTest.methodA();
System.out.println("main方法结束");
}
public void methodA(){
int i = 10;
int j = 20;
methodB(); System.out.println("i:"+i+",j:"+j);
}
public void methodB(){
int k = 30;
int m = 40;
System.out.println("k:"+k+",m:"+m);
}
}
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM直接对java栈的操作只有两个:
Java虚拟机规范允许虚拟机栈的大小是动态的或者是固定不变的。
我们可以使用参数**-Xss**选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 JDK5.0以后每个线程栈大小为1M,以前每个线程栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一 个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
/**
* 演示栈中的异常
*
* 默认情况下:count 10823
* 设置栈的大小: -Xss256k count 1874 */
public class StackErrorTest { private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
在IDEA中设置一个类的VM参数:
JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出/后进先出的原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧**(Current Frame)**,与当前栈帧对应的方法就是当前方法 (Current Frame)
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
public class StackFrameTest {
public static void main(String[] args) {
StackFrameTest test = new StackFrameTest();
test.method1();
//输出 method1()和method2()都作为当前栈帧出现了两次,method3()一次
// method1()开始执行。。。
// method2()开始执行。。。
// method3()开始执行。。。
// method3()执行结束。。。
// method2()执行结束。。。
// method1()执行结束。。。
}
public void method1(){
System.out.println("method1()开始执行。。。");
method2();
System.out.println("method1()执行结束。。。");
//return 可以省略
}
public int method2(){
System.out.println("method2()开始执行。。。");
int i = 10;
int m = (int) method3();
System.out.println("method2()执行结束。。。");
return i+m;
}
public double method3(){
System.out.println("method3()开始执行。。。");
double j = 20.0;
System.out.println("method3()执行结束。。。");
return j;
}
}
每个栈帧中存储着
1.举例栈溢出的情况?(StackOverflowError)
2.调整栈的大小,就能保证不出现溢出么?
3.分配的栈内存越大越好么?
4.垃圾回收是否会涉及到虚拟机栈?
5.方法中定义的局部变量是否线程安全?
具体情况具体分析
/** * * * * * *
题
*
面试题:
方法中定义的局部变量是否线程安全?具体情况具体分析
何为线程安全?
如果只有一个线程可以操作此数据,则必定是线程安全的。 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问
我们知道StringBuffer是线程安全的源码中实现synchronized,StringBuilder源码未实现synchronized,在多线程情况下是不安全的
* 二者均继承自AbstractStringBuilder *
*/
public class StringBuilderTest {
//s1的声明方式是线程安全的,s1在方法method1内部消亡了
public static void method1(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
//stringBuilder的操作过程:是不安全的,因为method2可以被多个线程调用
public static void method2(StringBuilder stringBuilder){
stringBuilder.append("a");
stringBuilder.append("b");
}
//s1的操作:是线程不安全的 有返回值,可能被其他线程共享
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s1的操作:是线程安全的 ,StringBuilder的toString方法是创建了一个新的String,s1在内部消亡了
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
new Thread(()->{
s.append("a");
s.append("b");
}).start();
method2(s);
}
}
后续内容请看JVM概述&内存结构2
关注作者不迷路,持续更新高质量Java内容~
原创不易,您的支持/转发/点赞/评论是我更新的最大动力!