JVM 基础知识(一)
JVM 从编译到执行
Java 程序的执行过程
一个java程序,首先经过javac编译成.class文件,然后JVM 将其加载到方法区,执行引擎将会执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的翻译存在,输入字节码,调用操作系统函数。
过程如下:Java 文件->编译器>字节码->JVM->机器码
JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class 后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。
JVM、JRE、JDK 的关系
jdk是最小的开发环境,由jre++java工具组成。
jre是java运行的最小环境,由jvm+核心类库组成。
jvm是虚拟机,是java字节码运行的容器,如果只有jvm是无法运行java的,因为缺少了核心类库。
JDK还提供了一些非常好用的小工具,比如 javac(编译代码)、java、jar (打包代码)、javap(反编译<反汇编>)等。这个就是 JDK。具体可以文档可以通过官网去下载:https://www.oracle.com/java/technologies/javase-jdk8-doc-downloads.html
JVM 的作用是:从软件层面屏蔽不同操作系统在底层硬件和指令的不同。这个就是我们在宏观方面对 JVM 的一个认识。
同时 JVM 是一个虚拟化的操作系统,类似于 Linux 或者 Windows 的操作系统,只是它架在操作系统上,接收字节码也就是class,把字节码翻译成操作系统上的机器码且进行执行。
**
从跨平台到跨语言
**
跨平台:我们写的一个类,在不同的操作系统上(Linux、Windows、MacOS 等平台)执行,效果是一样,这个就是 JVM 的跨平台性。为了实现跨平台型,不同操作系统有对应的 JDK 的版本。
https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
Hotspot:目前使用的最多的 Java 虚拟机。在命令行 java –version。它会输出你现在使用的虚拟机的名字、版本等信息、执行模式。
JVM 的内存区域
运行时数据区域
在 JVM 中,JVM 内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈等。同时按照与线程的关系也可以这么划分区域:
按照与线程的关系也可以这么划分区域:
线程私有区域:一个线程拥有单独的一份内存区域。
线程共享区域:被所有线程共享,且只有一份。
这里还有一个直接内存,这个虽然不是运行时数据区的一部分,但是会被频繁使用。你可以理解成没有被虚拟机化的操作系统上的其他内存(比如操作系统上有 8G 内存,被 JVM 虚拟化了 3G,那么还剩余 5G, JVM 是借助一些工具使用这 5G 内存的,这个内存部分称之为直接内存)
 {
A();
}
public static void A(){
B();
}
public static void B(){
C();
}
public static void C(){
System.out.println("123");
}
}
这段代码很简单,就是起一个 main 方法,在 main 方法运行中调用 A 方法,A 方法中调用 B 方法,B 方法中运行 C 方法。
我们把代码跑起来,线程 1 来运行这段代码, 线程 1 跑起来,就会有一个对应 的虚拟机栈,同时在执行每个方法的时候都会打包成一个栈帧。比如 main 开始运行,打包一个栈帧送入到虚拟机栈。
C方法运行完了,C 方法出栈,接着 B 方法运行完了,B 方法出栈、接着 A 方法运行完了,A 方法出栈,最后 main 方法运行完了,main 方法这个栈帧就出栈了。
这个就是 Java 方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。而每一个方法对应一个栈帧。
可以使用jdk编译成.class文件后用javap -c 文件名.class 查看相关详细运行信息。
https://cloud.tencent.com/developer/article/1333540
以上链接可以帮助查看java虚拟机 JVM字节码 指令集
虚拟机栈
栈的数据结构:先进后出(FILO)的数据结构,
虚拟机栈的作用:在 JVM 运行过程中存储当前线程运行方法所需的数据,指令、返回地址。
虚拟机栈是基于线程的:哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
虚拟机栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如-Xss256k。
参数官方文档(JDK1.8):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
栈帧 在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。
栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)
1.局部变量表
顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个 32 位的长度,主要存放我们的 Java 的八大基础数据类型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的 Object 对象,我们只需要存放它的一个引用地址即可
2.操作数栈
存放 java 方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。
操作数栈本质上是 JVM 执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。
3、 动态连接:
Java 语言特性多态(后续章节细讲,需要结合 class 与执行引擎一起来讲)。
4、 返回地址:
正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定) 同时,虚拟机栈这个内存也不是无限大,它有大小限制,默认情况下是 1M。
如果我们不断的往虚拟机栈中入栈帧,但是就是不出栈的话,那么这个虚拟机栈就会爆掉。
Exception in thread “main” java.lang.StackOverflowError
程序计数器
较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。
本地方法栈
本地方法栈跟java虚拟机栈的功能类似,java虚拟机栈用于管理java函数的调用,而本地方法栈用于管理本地方法的调用,本地方法使用C语言实现。
**
方法区
**
方法区是可提供各线程共享的运行时内存区域。它存储每一个类的信息结构,例如,运行时常量池、方法名、类名、字面量、字节码等。
jdk1.7称为永久代,jdk1.8使用元空间实现方法区
元空间
方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。
java7版本已经将永久代的静态变量和运行时常量池移到了堆中,其余部分分别存储到JVM的非堆内存中。而java8版本已经将方法区中实现的永久代去掉了,并用元空间代替永久代,并且元空间的存储位置是本地内存。
元空间大小参数:
jdk1.7及以前(初始和最大值):-XX:PermSize; -XX:MaxPermSize;
jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -xx:MaxMetaspaceSize
jdk1.8以后大小受本机总内存的限制
JDK每次升级都会做很多优化,我们使用最多的String常量类也在不断被优化。这次和大家分享的是JDK1.8中对String的优化之一,intern()方法的使用。就是通过常量池复用来提升性能
我们的常量池就在方法区中,在实际应用中会跟堆区配合使用。平时大家都统称它为常量池,严格划分的话常量池又分为:静态常量池、运行时常量池和字符串常量池。
堆
堆是JVM上最大的内存区域,几乎所有的对象,都是在这里存储。垃圾回收操作对象就是堆。
堆大小参数:
-Xms:堆得最小值
-Xmx:堆得最大值
-Xmn:新生代大小
-XX:NewSize:新生代最小值
-XX:MaxNewSize:新生代最大值
例如: -Xmx256m
直接内存(堆外内存)
JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也就是堆外内存。
它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;如果使用了 **NIO**,这块区域会被频繁使用,在 java 堆内可以用**directByteBuffer** 对象直接引用并操作;
这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异
常。其他对外内存,主要是使用了Unsafe或者其他JNI手段直接申请的内存。Unsafe类不安全,里面可以手动释放内存,不是jdk自动gc那么方便。
链接: link.