一、JVM(Java Virtual Machine)基础知识

        JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class 后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。

1、java程序的运行过程

        java文件通过javac命令编译成.class字节码文件,然后JVM将其加载到方法区(jdk7以及之前叫永久代,jdk8叫元空间),JVM执行引擎会将字节码翻译成机器码,操作系统执行机器码。

过程:Java 文件->字节码->机器码

java启动的时候默认分配的内存大小 java jar 默认jvm内存_java启动的时候默认分配的内存大小

2、JVM、JRE、JDK

  • JVM:JVM可以理解成一个虚拟化的操作系统,翻译工具,把class字节码文件翻译成机器识别的代码。JVM不会自己生成代码,同时需要很多依赖类库,这个时候就需要JRE了
  • JRE:除了包含JVM外,还有JavaAPI类库(就是我们常用的jar包)。JVM+基础类库就组成了Java的运行环境,即JRE(Java Runtime Environment)
  • JDK:包含JRE,但是我们写完要编译代码,调试代码,打包代码,有时候还要反编译代码。所以我们会用到JDK,JDK还给我们提供了javac,jar,java,javap等。

JVM的作用:从软件层面屏蔽不同操作系统在底层硬件和指令的不同。即JVM是一个虚拟化的操作系统,类似于Linux或者Windows的操作系统,只是它架在操作系统上,把class字节码翻译成操作系统识别的机器码。

3、JVM特性

java启动的时候默认分配的内存大小 java jar 默认jvm内存_java启动的时候默认分配的内存大小_02

  • JVM的跨平台性---JVM可以装在很多操作系统上,windows,linux,unix,andriod,mac等OS)
  • JVM的语言无关性---JVM只识别class字节码,所以和语言是解耦的,支持java,scala,kotlin,groovy等语言。Java 的跨语言性一定程度上奠定了非常强大的 java 语言生态圈

4、JVM的知识体系

  • 内存结构
  • 垃圾回收
  • 性能调优
  • JVM自身优化技术
  • 执行引擎
  • 监控工具
  • 类文件结构
  • 类加载

        所有的知识体系中,都或多或少跟内存结构有一定的关系,所以内存结构处于 JVM 中核心位置。同时 JVM 是一个虚拟化的操作系统,所以除了要虚拟指令之外,最重要的一个事情就是需要虚拟化内存,这个虚拟化内存就是 JVM 的内存区域。

二、JVM的内存区域

内存划分

java启动的时候默认分配的内存大小 java jar 默认jvm内存_常量池_03

 1、大类划分:运行时数据区(被虚拟化的内存)和直接内存

        运行时数据区的定义:java虚拟机在执行java程序的过程中会把它所需要的内存划分为若干不同的数据区域

        直接内存:未被虚拟机虚拟化的内存

2、运行时数据区划分:线程私有和线程共享

        线程共享:堆、方法区

        线程私有:虚拟机栈、本地方法栈、程序计数器

3、直接内存:

        虽然不是运行时数据区的一部分,但也是会频繁使用。可以理解成还没有被虚拟化的操作系统的上的其他内存。jvm借助一些其他工具可以使用这块内存。可以看一下unsafe类,这个类可以直接操作未虚拟化的内存,NIO中的DirectByteBuffer等

三、虚拟机栈和java方法的运行

java启动的时候默认分配的内存大小 java jar 默认jvm内存_java_04

        线程私有内存,生命周期与线程一样。

        虚拟机栈存储当前线程运行java方法所需的数据、指令、返回地址

        虚拟机栈描述的是java方法执行的内存模型,每一个方法在执行的同时都会打包成一个栈帧(Stack Frame)压入虚拟栈。栈帧包含,局部变量表,操作数栈,动态链接,完成出口(返回地址)等信息。每一个方法从调用至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

        比如方法调用方法。main方法-->A方法-->B方法-->C方法

public class MethodAndStack {
    public static void main(String[] args) {
        A();
    }
    public static void A(){
        B();
    }
    public static void B(){
        C();
    }
    public static void C(){

    }
}

入栈示意图,如下

java启动的时候默认分配的内存大小 java jar 默认jvm内存_JVM_05

        C 方法运行完了,C 方法出栈,接着 B 方法运行完了,B 方法出栈、接着 A 方法运行完了,A 方法出栈,最后 main 方法运行完了,main 方法这个栈帧就出栈了。

        这个就是 Java 方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。而每一个方法对应一个栈帧。

总结:

栈的数据结构:先进先出(FILO)的数据结构
虚拟机栈的作用:在JVM运行的过程中存储当前线程运行JAVA方法所需的数据,指令,返回地址。
虚拟机栈是基于线程的:哪怕你只有一个main方法,也是以线程的方式运行的 。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期和线程一样的。
虚拟机栈的大小默认为1M,取决于是哪个操作系统的虚拟机,可用参数 -Xss调整大小。如:-Xss1M
参数官方文档(JDK1.8):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html栈帧:在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。

栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)

1、局部变量表:

        顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。存放编译期可知的 Java 八大基础数据类型数据、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)

        其中64位长度的long和double类型的数据会占用2个局部变量表空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时就确定了。

        局部变量表的第一个位置默认是this引用,static方法不存储。

        一句话,存储方法的变量,基础数据类型和对象的引用。

2、操作数栈:

        存放 java 方法执行时执行引擎的操作数(字节码指令),它就是一个栈,先进后出的栈结构。操作数栈就是用来操作的,操作的元素可以是任意的 java 数据类型。所以一个方法刚刚开始的时候,这个方法的操作数栈就是空的。可以把执行引擎看成cpu,把操作数栈看成缓存,把局部变量表看成内存

        操作数栈本质上是 JVM 执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不执行,操作数栈其实就是空的。

3、动态连接:
        Java 语言特性多态(需要执行引擎的知识)

4、返回地址:
        正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)

        同时,虚拟机栈这个内存也不是无限大,它有大小限制,默认情况下是 1M。如果我们不断的往虚拟机栈中入栈帧,但是就是不出栈的话,那么这个虚拟机栈就会爆掉。会抛出Exception in thread "main" java.lang.StackOverflowError

如果虚拟机栈只动态扩展的,若果扩展时无法申请到足够的内存,就会抛出OurOfMemoryError异常

问:虚拟机栈的构成
答:局部变量表、操作数栈、动态连接、返回地址(完成出口)

问:虚拟栈为什么要放一个操作数栈
答:因为操作引擎只负责执行jvm命令不负责存储,在计算的时候必定有些数据需要存储在操作数栈里。操作数栈有点类似于高速缓存,操作引擎类似cpu。比如在执行1+1操作的时候,class字节码文件反汇编后的三条命令是 iload1,iload2,iadd,然后操作引擎会依次执行iload1压栈一个1,执行iload2压栈一个1,执行iadd弹出操作数栈最上面两个数求和再压入。

对 class 进行反汇编 javap –c XXXX.class

字节码助记码解释地址 https://cloud.tencent.com/developer/article/1333540

四、程序计数器

        线程私有内存,指向当前线程正在执行的字节码指令地址,各线程之间独立存储,互不影响。

        程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。

        因为 JVM 是虚拟机,内部有完整的指令与执行的一套流程,所以在运行 Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果是遇到本地方法(native 方法),这个方法不是 JVM 来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址,所以在执行 native 方法时,JVM 中程序计数器的值为空(Undefined)。

        程序计数器也是 JVM 中唯一不会 OOM(OutOfMemory)的内存区域。

五、本地方法栈

        线程私有内存,和虚拟机栈相似,不过是为JVM使用到的本地(Native)方法服务,运行一些本地的方。因为jvm不能直接调用操作系统,但是c和c++可以,本地方法是由C语言实现的(比如 Object.hashcode 方法)。
        本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot 直接把本地方法栈和虚拟机栈合二为一 。
        异常:StackOverflowError和OurOfMemoryError

六、方法区

        线程共享内存,方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息,常量,静态变量,即时编译器编译后的代码,运行时常量池,字符串常量池,类加载器引用,方法表等。

        Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆),目的应该和java堆区分开。请注意方法区只是java虚拟机的规范,而具体实现由厂商决定。在JDK1.7及之前的方法区实现称为“永久代”,是因为在HotSpot虚拟机中,设计人员使用堆的永久代来实现了JVM规范的方法区。在JDK1.8及以后使用了元空间来实现方法区,存在于直接内存。元空间存储类的元信息,而静态变量和常量池等并入堆中。

        JVM在执行某个类的时候,必须先加载。在加载(加载、验证、准备、解析、初始化)类的时候,JVM会先加载class文件,而在class文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table,也叫calss文件常量池),用于存放编译期间生的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中存放

字面量:包括字符串字面量(String a = "b",字符串字面量(stringliteral)是指双引号引住的一系列字符)、final修饰的基本类型的常量(成员变量)。字符串字面量还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符

符号引用:包括类和方法的全限定名(例如String这个类,它的全限定名就是Java/Lang/String)、字段的名称和描述符以及方法的名称和描述符。

符号引用

        一个 java 类(假设为 People 类)被编译成 class 文件时,如果 People 类引用了 Tool 类,但是在编译时 People 类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。而在类装载器装载 People 类时,此时可以通过JVM获取 Tool 类的实际内存地址,因此便可以既将符号 org.simple.Tool 替换为 Tool 类的实际内存地址,及直接引用地址。

        即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址

        以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。

常量池和运行时常量池

        在类加载到内存后,JVM就会将class文件常量池中的内容存放到运行时常量池中;在解析阶段,JVM会把符号引用直接替换为直接引用(对象的索引值)

        例如,类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的;在 JVM 加载完类之后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件常量池多个相同的字符串在运行时常量池只会存在一份。

        常量池有很多概念,包括运行时常量池、class 常量池、字符串常量池。

        严格来说是静态常量池和运行时常量池。静态常量池是存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。字符串常量池jvm没有此定义,只不过是众开发人员定义的。

        运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。

        运行常量池相对于class文件常量池的另外一个特征是具备动态性,java并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入将方法区运行常量池,运行期间也可能将新的常量放入池中,比如String类的inter()方法。

        运行时常量池在 JDK1.7 版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。

java启动的时候默认分配的内存大小 java jar 默认jvm内存_常量池_06

元空间

        方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。

        在 HotSpot 虚拟机,Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地内存,也就是说JDK8版本方法区在直接内存

        元空间大小参数:

        jdk1.7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;

        jdk1.8 以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize

j        dk1.8 以后大小就只受本机总内存的限制(如果不设置参数的话)

JVM 参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

Java8 为什么使用元空间替代永久代,这样做有什么好处呢?

官方给出的解释是:

        移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。

        永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

七、堆

        线程共享内存,存储实例对象、数组、class对象。

        堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。

        堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。

        随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。

        那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置(逃逸分析)。

        Java 的对象可以分为基本数据类型和普通对象。

        对于普通对象,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。

        对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。

        当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

堆大小参数:

-Xms:堆的最小值;

-Xmx:堆的最大值;

-Xmn:新生代的大小;

-XX:NewSize;新生代最小值;

-XX:MaxNewSize:新生代最大值;

例如- Xmx256m

所有的对象都在堆上存放并不是绝对的,因为逃逸分析技术成熟虚拟机栈上可以存放对象。

八、直接内存(堆外内存)

        直接内存有一种更加科学的叫法,堆外内存。

        JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也就是堆外内存。

        它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也有可能导致oom

        NIO(New Input/Ouput)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配对外内存,然后通过一个存储在java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作;

        这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常。

小结:

1、直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小。

2、其他堆外内存,主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。

堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡。

同时,要注意 Oracle 之前计划在 Java 9 中去掉 sun.misc.Unsafe API。这里删除 sun.misc.Unsafe 的原因之一是使 Java 更加安全,并且有替代方案。

3、java文件经过工具javac编译成class字节码文件,虚拟机识别class文件,再编译成机器码

4、JVM特性:跨平台,语言无关性

5、虚拟机是一个规范

6、javap -c Persion.class 反汇编查看字节码

7、javap -v Persion.class 查看字节码结构

8、栈帧里面的局部变量表的第一个this指的是哪个类的引用

9、动态连接是java语言多态动态的特性

10、native关键字的方法不是在java实现的是在c或者c++ ,是在本地方法栈执行的,可见java是可以直接调用c或者c++。 因为c或者c++可以直接调用操作系统,java不可以。