Java | JVM Java虚拟机

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

JVM是建立在操作系统(如:Windows、Mac、Linux)之上的,可以理解为JVM一端连接的是统一的Java语言接口,另一端是适配不同的操作系统的接口,程序员只需关注Java的编写,不同的操作系统上只需下载不同的JVM,JVM屏蔽了与具体平台相关的信息,使得Java语言在不同平台上运行时不需要重新编译,实现一次编写到处运行。

目前最常用的JVM是Hotspot,JDK8的版本,本文即使对此的说明。

 

 

 

一、JVM的内存区域

JVM的运行区域主要包括5个大类:元空间、堆、虚拟机栈、本地方法栈。上图JVM示意图中背景色绿色的部分(虚拟机栈、本地方法栈、程序计数器)表示线程私有的,而白色的部分(元空间、堆)是线程共享的。

另外还有三个组件:执行引擎、本地方法接口、本地方法库,虽然图上也比较详细,但大家看看就好,稍微有个了解,最重要的就是上面的5个区域,重中之重是虚拟机栈

执行引擎是作为Java生命周期中的一个执行者,而JVM运行区域更像是一个容器。包括从.java文件编译成.class文件开始,再到加载、连接、初始化、使用和卸载,详细见下图。(这里可以再开一篇类的声明过程)

本地方法接口和本地方法库主要对应的是非Java语言,如:C、C++等,加载进本地方法栈进行调用。

 

 

 

1.1 程序计数器

先从线程私有的区域开始说起,程序计数器记录的是当前字节码指令的地址,如果是Native方法,则计数器为空,占用的空间比较小,每个线程都会配备一个,此内存区域也是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

 

1.2 虚拟机栈(Stack)

虚拟机栈的数据结构是栈(Stack),数据遵守先进后出FILO(First In Last Out)的原则,虚拟机栈中的是以方法为基本单位的,每个栈帧对应的就是一个方法。当栈帧一直压入而不弹出(如一些死循环的递归方法),栈帧大于虚拟机所允许的深度,是会出现栈内存溢出错误的(StackOverflowError)。

需要注意的是:局部变量表中存放的一些数据,八大原始类型,即基本数据类型,存放的是数据的值,而引用数据类型存放的是对象的引用地址,指向堆内存或常量池中。

 

1.3 本地方法栈

结构上与虚拟机栈类似,但执行的是Native方法。

 

接下来说一下线程共享的区域。

1.4 元空间

在JDK1.7以前,元空间都是叫做方法区的,虽然在逻辑上是独立的空间,但实际上就是放在堆中。在JDK1.8之后,取消了方法区,改叫做元空间(Meta Data Space),存储区域其实也不在JVM中了,放到了本地内存中。

主要存放类信息、运行时常量池、永久代和一些即使编译器编译后的代码(即.class文件代码),运行时常量池主要包括常量、静态变量、基本数据类型和字符串字面量(与字符串常量池的引用值保持一致)。JDK1.6及之前运行时常量池在方法区,JDK1.7将其放入堆中单独划分的一个区域,JKD1.8又将其放入元空间,这里需要与堆中的字符串常量池进行区分。

 

1.5 堆(Heap)

堆中的存储区域主要分为两块:新生区老年区,新生区大概占堆内存的1/3,老年区占2/3。

新生区又分为三个区域:伊甸园区(Eden)、幸存0区幸存1区(这结构就跟套娃一样,也有人把直接新生区叫做伊甸园区的,我们这里还是做一下区分),伊甸园区大概占新生区内存的8/10,幸存0区占1/10,幸存1区也占1/10。

为什么堆里面还分这么多区域?这主要涉及到垃圾回收算法GC,本文主要做个大概的介绍,具体内容后续可以新开一篇进行说明。

堆是JVM管理的内存最大的一块,主要存放对象的实例、数组,还有字符串常量池,字符串常量池存放的时字符串的引用而不是实例,实例对象会在堆中开辟一块空间存放。

String s1 = new String("hello");
String s2 = new String("hello");

会产生3个实例对象,s1 != s2,两个在堆中,一个在运行时常量池中,字符串常量池的引用指向运行时常量池。

使用“双引号”的字符串,首先会从字符串常量池中寻找,如果找不到会new;而使用new String则不会寻找,而是直接new对象。

下面的代码的结果是(比较地址):

  • s3 != s6

  • s4 !=s6

  • s5 == s6

  • s9 == s10 == s11

String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = s1 + "world";
String s5 = s1 + s2;
String s6 = "hello"+"world";

final String s7 = "hello";
final String s8 = "world";
String s9 = "helloworld";
String s10 = s7 + "world";
String s11 = s7 + s8;

需要注意的是如果只声明s6,而无s1,s2,字符串常量池中只会存"helloworld"的引用值,而不会存"hello"和"world"。

 

二、.class文件

class文件就是编译器编译之后供虚拟机解释执行的二进制字节码文件,详细内容可见下图(仅作了解,没什么用):

有对Java指令感兴趣的,可以自行百度:Java指令码表

 

 

 

三、垃圾回收算法GC

只做大概框架性的介绍,具体内容以后再填坑。

  1. 引用计数法(JVM中不使用,仅作了解)

  • 对象引用次数为0时,可以立即垃圾回收;

  • 不需要另外的GC线程。

  • 计数器本身有消耗,每次赋值都要做相当大的计算;

  • 无法回收循环引用的对象。

  1. 复制算法(轻GC)

  • 效率高,时间复杂度低;

  • 空间整齐度高。

  • 使用空间扩大一倍,空间复杂度高;

  • 对象存活率高的情况下,复制非常耗时。

  1. 标记清除算法(重GC)

  • 空间利用率高,空间复杂度低。

  • 效率低,时间复杂度高;

  • 空间整齐度低。

  1. 标记压缩算法(重GC)

  • 空间利用率高,空间复杂度低;

  • 空间整齐度高。

  • 效率低,时间复杂度高。

 

四、JVM性能调优

JVM调优是为了使用较小的内存获得较高的吞吐量或者较低的延迟,这里只提一下,具体内容以后填坑。

默认情况下,JVM能分配到的最大内存大概是电脑总内存的1/4,初始化内存大概是电脑总内存的1/64。JVM调优可以自己设置分配大小,也可以分配垃圾回收的策略,也可以Dump下错误记录后进行性能分析(性能分析工具:JProfiler)。

一些常用的调优参数示例:

-Xms1m   // 设置初始化内存为1m
-Xmx8m   // 设置JVM最大内存为8m
-XX:+PrintGCDetails   // 打印GC详情
-XX:+HeapDumpOnOutOfMemoryError  // Dump堆内存溢出错误

 

 

 

posted @ 2021-09-24 00:29  ジョカ  阅读(99)  评论(0)    收藏  举报