虽然过了一遍JVM,但还是感觉很懵,不做笔记整理一下果然还是不行

首先要搞懂几个最基本的问题:

1、Java文件编译的大致过程

  • 程序员编写.java文件

  • 由javac编译成字节码文件.class:(因为JVM只认识.class文件)

  • 再由JVM编译成电脑认识的文件 (二进制码)

2、为什么说java是跨平台语言

  • 这个跨平台是由JVM实现的跨平台

  • java由JVM从软件层面屏蔽了底层硬件、指令层面的细节从而能够兼容各种系统

  • 而C和C++需要在编译器层面去兼容不同操作系统的不同层面

3、Jdk和Jre和JVM的区别

  • 下面是一个图片概述,总的来说,Jdk中包括了Jre,Jre中包括了JVM,JVM在倒数第二层,正是通过JVM才可以在(最后一层的)各种平台上运行,Jre大部分都是 C 和 C++ 语言编写的,它是我们在编译java时所需要的基础的类库,Jdk还包括了一些Jre之外的东西,就是这些东西帮我们编译Java代码的,还有就是监控JVM的一些工具

    alt

2、JDK

  • JDK(Java SE Development Kit),Java标准开发包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等。

  • 下图是JDK的安装目录:

    alt

3、JRE

  • JRE( Java Runtime Environment) 、Java运行环境,用于解释执行Java的字节码文件。普通用户只需要安装 JRE(Java Runtime Environment)来运行 Java 程序。而程序开发者必须安装JDK来编译、调试程序。

  • 下图是JRE的安装目录:里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。

    alt

4、JVM

  • JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分。它是整个java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行java字节码文件的虚拟计算机。所有平台的上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。

  • 当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码只面向JVM。不同平台的JVM都是不同的,但它们都提供了相同的接口。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相应的虚拟机,编译后的Java字节码就可以在该平台上运行。

深入学习JVM

一、前言

1、JVM的好处

  • 一次编译,处处执行

  • 自动的内存管理,垃圾回收机制

  • 数组下标越界检查

2、学习 JVM 有什么用?

  • 面试必备

  • 中高级程序员必备

  • 想走的长远,就需要懂原理,比如:反射是怎么实现的,垃圾回收机制是怎么回事,JVM 是必须掌握的。

3、常见的JVM

alt

本次主要学习的是 HotSpot 版本的虚拟机。

4、学习路线

alt

  • ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。

  • Method Area:类是放在方法区中。

  • Heap:类的实例对象。

  • 当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。

  • 方法执行时的每行代码是有执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口。

二、内存结构

1、程序计数器(Program Counter Register)

1、定义

  • 程序计数器是一块较小的内存空间,用来保存当前线程所正在执行的字节码指令的地址(行号)

2、特点:

  • 是线程私有的。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。

  • 不会存在内存溢出。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

3、作用:

  • 记录下一条 jvm 指令的执行地址行号。

    alt

  • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。

  • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

2、虚拟机栈(Java Virtual Machine Stacks)

1、定义

  • 每个线程运行需要的内存空间,称为虚拟机栈

  • 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

    alt

2、解析栈帧

  • 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址)

  • 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 3+2,它在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去

  • 动态链接:假如方法中有个service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。

  • 出口:出口是什么呢,出口正常的话就是return,不正常的话就是抛出异常

3、问题辨析:

  • 栈指向堆是什么意思?

    栈中不会存储成员变量,只存储引用地址,真实的对象存放在堆中

  • 垃圾回收是否涉及栈内存?

    不会。垃圾回收不涉及栈内存,只是回收堆内存中的无用对象

  • 栈内存分配越大越好吗?

    栈内存并不是分配的越大越好,栈内存分配大了,只是可以进行更多次的方法递归调用,并不提高程序运行效率,同时还会影响线程数量

  • 方法内的局部变量是否线程安全?

    如果方法内部的变量没有逃离方法的作用访问,它是线程安全的

    如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

4、栈内存溢出

  • 栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出java.lang.stackOverflowError

  • 使用 -Xss256k 指定栈内存大小

5、线程运行诊断

案例一:cpu 占用过多

解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

  • top 命令,查看是哪个进程占用 CPU 过高

  • ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高

  • jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是16进制的,需要转换。

3、本地方法栈(Native Method Stack)

  • 虚拟机栈是为了虚拟机能够执行java方法,而本地方法栈则是为虚拟机所使用到的native方法服务。

  • native方法即用native关键字修饰的方法,都是由操作系统实现的,Java只能调用,所以java中的native方法都没有方法体

  • native方法使得Java通过JNI(Java Native Interface)直接调用c/c++库,可以认为native方法相当于c/c++库暴露给java的一个接口,java通过这个接口调用到c/c++方法

4、堆(Java Heap)

1、定义

  • java堆是java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。所有的对象实例以及数组都要在堆上分配。

  • 它是线程共享,堆内存中的对象都需要考虑线程安全问题

  • 有垃圾回收机制

2、堆的扩展

  • java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”

  • 从内存回收角度来看java堆可分为:新生代和老年代

  • 从内存分配的角度看,Java堆中可能划分出多个线程私有的分配缓冲区

  • 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中

3、堆内存溢出

  • java.lang.OutofMemoryError :java heap space. 堆内存溢出

  • 可以使用 -Xmx 和 -Xms 来指定堆内存大小

4、堆内存诊断

  • jps 工具

    查看当前系统中有哪些 java 进程

  • jmap 工具

    查看堆内存占用情况 jmap - heap 进程id

    jhsdb jmap --heap --pid 19760

  • jconsole 工具

    图形界面的,多功能的监测工具,可以连续监测

  • jvisualvm 工具

5、方法区(Method Area)

1、定义

  • Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化,方法区是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!

2、方法区组成

alt

alt

3、方法区内存溢出

  • 1.8 之前会导致永久代内存溢出

    使用 -XX:MaxPermSize=8m 指定永久代内存大小

  • 1.8 之后会导致元空间内存溢出

    使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

4、常量池(constant pool)

  • 二进制字节码包含(类的基本信息,常量池,类方法定义,包含虚拟机指令)

  • 编译如下代码

package jvm.constantpool;
//二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class pool {
    public static void main(String[] args) {
        System.out.println("hello,world!");
    }
}
  • 使用 javap -v pool.class(绝对地址),对javac编译的文件进行反编译 alt

  • 每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。

    alt

  • JVM助记符:

    ldc:表示将int,float或者String类型的常量值从常量池中推送至栈顶

    astore_n:将引用存储到一个局部变量中,并存放在局部变量表中索引为n的位置

    aload_n:从局部变量表索引为n的位置加载一个引用

    invokedynamic:动态调用方法。

  • 常量池:

    常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息

  • 运行时常量池:

    常量池存在于 .class 文件中,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

6、字符串常量池(String Table)

1、特点

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder,会在堆中创建新的字符串对象
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

2、可以使用debug和idea中的Memory工具查看字符串对象的数量

alt

2、intern方法(jdk1.8)

  • 调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功

  • 如果有该字符串对象,则放入失败 无论放入是否成功,都会返回串池中的字符串对象

  • 注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

3、StringTable 的位置

  • jdk1.6 StringTable 位置是在永久代中

  • jdk1.8 StringTable 位置是在堆中。

4、StringTable 垃圾回收

  • -Xmx10m:指定堆内存大小
  • -XX:+PrintStringTableStatistics 打印字符串常量池信息
  • -XX:+PrintGCDetails
  • -verbose:gc 打印gc的次数,耗费时间等信息

5、StringTable 性能调优

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
  • -XX:StringTableSize=桶个数(最少设置为 1009 以上)
  • 考虑是否需要将字符串对象入池
  • 可以通过 intern 方法减少重复入池