JVM基础一

跨平台

不同操作系统上运行的JVM是不一样的,这才是JVM跨平台的本质!我们写一份Java代码,编译为字节码后,之所以能在不同操作系统上运行,就是因为不同操作系统上的JVM都能运行字节码,相当于不同操作系统上的JVM屏蔽了不同操作系统的底层区别。

字节码文件

JVM会逐行解释执行字节码,那为什么不逐行解释执行Java代码呢?不一样吗,一份Java代码对应的字节码是一样的,一对一的关系,为什么要把Java代码编译为字节码,因为性能,为了提高效率,如果直接把Java代码翻译为机器指令,也不是不行,也就是解释执行,这样就会导致Java代码再运行时效率比较低,一般的解释型语言效率都比较低,而如果我们提前先对Java代码进行编译,编译为字节码,那字节码再翻译为机器指令时,效率比较块,也就导致真正执行字节码时,效率会比较高,这就是字节码的作用,所以Java其实是编译+解释二合一的语言。

JMM内存模型

Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。
这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系

  • 单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
  • 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作
  • volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
  • 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作
  • 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
  • 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C

怎么理解 happens-before 呢?如果按字面意思,比如第二个规则,线程(不管是不是同一个)的解锁动作发生在锁定之前?这明显不对。happens-before 也是为了保证可见性,比如那个解锁和加锁的动作,可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享对象修改的结果。

Java 提供了几种语言结构,包括 volatile, finalsynchronized, 它们旨在帮助程序员向编译器描述程序的并发要求,其中:

  • volatile - 保证可见性有序性
  • synchronized - 保证可见性有序性; 通过管程(Monitor)_保证一组动作的_原子性
  • final - 通过禁止在构造函数初始化给 final 字段赋值这两个动作的重排序,保证可见性(如果 this 引用逃逸就不好说可见性了)

编译器在遇到这些关键字时,会插入相应的内存屏障,保证语义的正确性。
有一点需要注意的是,synchronized 不保证同步块内的代码禁止重排序,因为它通过锁保证同一时刻只有一个线程访问同步块(或临界区),也就是说同步块的代码只需满足 as-if-serial 语义 - 只要单线程的执行结果不改变,可以进行重排序。
所以说,Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性,另外,还确保正确同步的 Java 代码可以在不同体系结构的处理器上正确运行。

整体结构

先将java文件编译为class文件,再利用类加载器将class文件加载到方法区中,然后由解析器逐行执行字节码,每执行一个Java方法,就将方法存入Java栈,每执行一个本地方法,也就是native方法,就将方法存入本地方法栈中,方法执行完后就从栈中移除,程序计数器用来记录待执行的下一条字节码指令地址,方法执行过程中产生的Java对象会存入堆中,垃圾回收器会回收已经没有被使用的Java对象,JIT编译器会在程序运行过程中发现热点代码,并编译为机器指令,从而提高执行效率。

类加载机制

类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。

类的生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d5qksDuB-1678169889314)(https://cdn.nlark.com/yuque/0/2022/jpeg/22211120/1669421991104-9a8609bf-3890-4293-9eca-5c1e99ce5818.jpeg#averageHue=%23f8f8f4&clientId=u68319174-34bd-4&from=paste&height=166&id=uea6439de&name=32b8374a-e822-4720-af0b-c0f485095ea2.jpg&originHeight=208&originWidth=641&originalType=binary&ratio=1&rotation=0&showTitle=false&size=22528&status=done&style=none&taskId=u5b93289d-cde7-44a5-9f78-ffdbb4dfea8&title=&width=512.8)]

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

类加载过程

包含了加载、验证、准备、解析和初始化这 5 个阶段。

1. 加载

加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
  • 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。

其中二进制字节流可以从以下方式中获取:

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

2. 验证

确保Class文件的字节流中包含的信息符合虚拟机规范,保证在运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证

3. 准备

类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在堆中。
注意,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。

public static int value = 123;

如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。

public static final int value = 123;

4. 解析

将常量池的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

5. 初始化

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:

public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

由于父类的 () 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:

static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

public static void main(String[] args) {
     System.out.println(Sub.B);  // 2
}

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。
虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

类初始化时机

1. 主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

2. 被动引用

以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

  • 通过子类引用父类的静态字段,不会导致子类初始化。
System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义
  • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
SuperClass[] sca = new SuperClass[10];
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
System.out.println(ConstClass.HELLOWORLD);

类文件结构

Class 文件结构如下:

ClassFile {
    u4             magic; //类文件的标志
    u2             minor_version;//小版本号
    u2             major_version;//大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//类的访问标记
    u2             this_class;//当前类的索引
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//字段属性
    field_info     fields[fields_count];//一个类会可以有个字段
    u2             methods_count;//方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}

主要参数如下:
魔数:class文件标志。
文件版本:高版本的 Java 虚拟机可以执行低版本编译器生成的类文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的类文件。
常量池:存放字面量和符号引用。字面量类似于 Java 的常量,如字符串,声明为final的常量值等。符号引用包含三类:类和接口的全限定名,方法的名称和描述符,字段的名称和描述符。
访问标志:识别类或者接口的访问信息,比如这个Class是类还是接口,是否为 public 或者 abstract 类型等等。
当前类的索引:类索引用于确定这个类的全限定名。

类加载器分类

实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
  • 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度看,类加载器可以划分得更细致一些(Launcher类中的代码):

  • 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
  • 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 自定义类加载器:通过继承java.lang.ClassLoader类的方式实现。比如Tomcat中的WebAppClassLoader

类只需加载一次就行,因此要保证类加载过程线程安全,防止类加载多次。
**对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类必定不相等。**这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。

双亲委派模型

应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
image.png

1. 工作过程

一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

2. 好处

防止内存中出现多份同样的字节码,防止核心API被篡改。
如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。

3. 实现

以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

打破双亲委派机制

  • JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
  • Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。
    • 对于各个 webapp中的 classlib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。
    • jvm一样的安全性问题。使用单独的 classloader去装载 tomcat自身的类库,以免其他恶意或无意的破坏;
    • 热部署。
  • OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
  • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。

Tomcat为什么要自定义类加载器?

为了进行类的隔离,如果Tomcat直接使用AppClassLoader类加载类,那就会出现如下情况:

  • 应用A中有一个com.zhouyu.Hello.class
  • 应用B中有一个com.zhouyu.Hello.class
  • 如果AppClassLoader先加载了应用A中的Hello.class
  • 那么应用B中的Hello.class就不可能再被加载了
  • 如果就需要针对应用A和应用B设置各自单独的类加载器,也就是WebappClassLoader
  • 这样两个应用中的Hello.class都能被各自的类加载器所加载,不会冲突

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XyQdYJUf-1678169889319)(https://cdn.nlark.com/yuque/0/2023/png/22211120/1678070803844-d8559486-9b21-4be6-9efe-969a288b29a4.png#averageHue=%23d1e0ef&clientId=udcd2b299-a153-4&from=paste&height=509&id=uaa6d3caf&name=image.png&originHeight=636&originWidth=720&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=19586&status=done&style=none&taskId=u91f5b2a4-abc3-4f7b-8bae-406e098c22b&title=&width=576)]
这就是Tomcat为什么用自定义类加载器的核心原因,为了实现类加载的隔离。
JVM中判断一个类是不是已经被加载的逻辑是:类名+对应的类加载器实例

自定义类加载器实现

FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

运行时数据区域

image.png

程序计数器

线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。程序计数器主要有两个作用:

  1. 当前线程所执行的字节码的行号指示器,通过它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道它上次执行的位置。

程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9lWhiKMm-1678169889321)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665411668306-88c6d1e9-b41d-412a-b3af-574c736389d2.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_47%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10#averageHue=%23151515&from=url&id=pU8ax&originHeight=605&originWidth=1641&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

Java 虚拟机栈

Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表操作数栈动态链接方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。
局部变量表是用于存放方法参数和方法内的局部变量。
每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。

  • 部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接
  • 部分符号引用在运行期间转化为直接引用,这种转化就是动态链接

![image (1).png](https://img-blog.csdnimg.cn/img_convert/2e6f8e8e6cf36a7ac9787c445164a584.png#averageHue=#181614&clientId=uee00ddf4-49d0-4&from=paste&height=585&id=u05dd407f&name=image (1).png&originHeight=731&originWidth=1106&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53785&status=done&style=none&taskId=u777c087d-a4bf-40da-ad15-bb28f43d360&title=&width=884.8)

可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:

java -Xss512M

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

栈帧

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8E5WNag9-1678169889322)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665412002433-b285ba6f-92b0-4a85-a1c0-d02e2ac77692.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_33%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10#averageHue=%2370ad47&from=url&id=CZnhI&originHeight=709&originWidth=1163&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

局部变量表

局部变量表:local variables,其实是列表,是数组的意思,主要用来保存方法参数和方法内的局部变量,要么是基本数据类型的值,要么是引用地址,局部变量表所需的内存大小在编译器就确定下来了,根据参数类型和变量类型就能确定下来,可以在字节码中看到局部变量表,不过字节码中的局部变量表中只存了局部变量的位置,还没有存值,局部变量表中基本的存储单位是Slot,每个Slot可以存储一个占32bit的值,像double类型的数字就需要两个Slot才能存,对象的引用地址也是存在Slot中的,占一个Slot,比如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TECwCsmP-1678169889324)(https://cdn.nlark.com/yuque/0/2022/png/22211120/1669424817645-f68b8612-1800-4293-a0ba-92d565684e29.png#averageHue=%23b3b2b2&clientId=uee53b1ab-3ce4-4&from=paste&height=423&id=u12672122&name=image.png&originHeight=529&originWidth=1022&originalType=binary&ratio=1&rotation=0&showTitle=false&size=48543&status=done&style=none&taskId=ua10acf63-0c94-44d7-ae68-e20334dce3c&title=&width=817.6)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NWV0I4zd-1678169889324)(https://cdn.nlark.com/yuque/0/2022/png/22211120/1669424833350-eacb534d-53e1-4044-aabf-ee11e1fb20f5.png#averageHue=%23f9f7f6&clientId=uee53b1ab-3ce4-4&from=paste&height=258&id=u0627152b&name=image.png&originHeight=322&originWidth=951&originalType=binary&ratio=1&rotation=0&showTitle=false&size=31656&status=done&style=none&taskId=ub5798d4a-8659-4a04-a8be-64069e9c2f5&title=&width=760.8)]
image.png
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hyfdhFXt-1678169889327)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665413008697-ac9a3589-8e6f-4ab7-b795-14583f0a19f6.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_25%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10#averageHue=%232f2f2e&from=url&id=Wk9yF&originHeight=302&originWidth=867&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

操作数栈

操作数栈,Operand Stack,也可以叫操作栈,也是栈帧中的一部分,操作数栈是用来在执行字节码指令过程中用来临时存数据并用来计算的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJD2CBqD-1678169889327)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665489055674-514264a0-c1d9-4d8c-9da5-8476c77afc55.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_51%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10%2Fresize%2Cw_937%2Climit_0#averageHue=%23181717&from=url&id=OgwvV&originHeight=407&originWidth=937&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tl9yW5rP-1678169889328)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665489078697-1509b05f-560c-4b9f-834e-a1a79b816426.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_43%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10%2Fresize%2Cw_937%2Climit_0#averageHue=%23111110&from=url&id=bkUAG&originHeight=531&originWidth=937&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-710pwPWi-1678169889330)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665489103533-941b06ef-1cab-4e12-aaea-863343236435.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_43%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10%2Fresize%2Cw_937%2Climit_0#averageHue=%23111010&from=url&id=Y6gZE&originHeight=526&originWidth=937&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B6BiubUO-1678169889331)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665489110448-7462df28-5713-43a0-a432-91cbe1a8abbb.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_43%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10%2Fresize%2Cw_937%2Climit_0#averageHue=%23111010&from=url&id=CacJN&originHeight=526&originWidth=937&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-igWfqhsN-1678169889332)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665489122092-18091b47-b279-4f7e-8fc7-562d68be5163.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_43%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10%2Fresize%2Cw_937%2Climit_0#averageHue=%23111010&from=url&id=ZApJx&originHeight=526&originWidth=937&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]



[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xc2p8hF4-1678169889336)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665489149075-bfdc60ee-de75-4d04-b4e3-66f70543c06d.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_43%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10%2Fresize%2Cw_937%2Climit_0#averageHue=%23111010&from=url&id=vjhln&originHeight=526&originWidth=937&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zl4n3dDy-1678169889337)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665489171116-728002ad-6f19-4360-b1a5-7232e7afd0b5.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_16%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10%2Fresize%2Cw_355%2Climit_0#averageHue=%232d2d2c&from=url&id=ozj96&originHeight=526&originWidth=355&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
image.png

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OfcjiytA-1678169889339)(https://cdn.nlark.com/yuque/0/2022/png/22211120/1669214577548-f6d6ad65-d543-4be1-9764-053946269a70.png#averageHue=%23538bcd&clientId=uee00ddf4-49d0-4&from=paste&height=351&id=ue8fc12b1&name=image%20%282%29.png&originHeight=439&originWidth=1807&originalType=binary&ratio=1&rotation=0&showTitle=false&size=32536&status=done&style=none&taskId=uc29d4590-4f6d-462a-a552-a4af3db42f9&title=&width=1445.6)]
堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作GC堆。堆可以细分为:新生代(Eden空间、From Survivor、To Survivor空间)和老年代。
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
一般会把-Xms和-Xmx设置为一样,这样JVM就不需要在GC后去修改堆的内存大小了,提高了效率,默认情况下,初始化内存大小=物理内存大小/64,最大内存大小=物理内存大小/4 。
-Xms:ms(memory start),指定堆的初始化内存大小,等价于-XX:InitialHeapSize
-Xmx:mx(memory max),指定堆的最大内存大小,等价于-XX:MaxHeapSize

可以通过-XX:NewRatio参数来配置新生代和老年代的比例,默认为2,表示新生代占1,老年代占2,也就是新生代占堆区总大小的1/3 。

年轻代

所有新生成的对象首先都是放在年轻代。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代一般分3个区,1个Eden区,2个Survivor区(from 和 to)。
大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当一个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当另一个Survivor区也满了的时候,从前一个Survivor区复制过来的并且此时还存活的对象,将可能被复制到年老代。
2个Survivor区是对称的,没有先后关系,所以同一个Survivor区中可能同时存在从Eden区复制过来对象,和从另一个Survivor区复制过来的对象;而复制到年老区的只有从另一个Survivor区过来的对象。因为需要交换的原因,Survivor区至少有一个是空的。特殊的情况下,根据程序需要Survivor区是可以配置为多个的(多于2个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。可以通过-XX:SurvivorRatio来调整。
针对年轻代的垃圾回收即 Young GC(Minor GC、Scavenge GC)。

老年代

在年轻代中经历了N次(可配置)垃圾回收后仍然存活的对象,就会被复制到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
老年代也满了的话,就将触发Full GC,针对整个堆(包括新生代、老年代、持久代)进行垃圾回收。

永久代( 方法区 )

方法区是 JVM 的规范,而永久代PermGen是方法区的一种实现方式,不属于堆。
持久代如果满了,将触发Full GC。
造成Full GC的原因:
1.年老代被写满
2.持久代被写满
3.显示调用System.GC
4.上一个GC后个heap的各域分配策略动态变化。
image.png

内存申请过程

  1. JVM会试图为相关Java对象在年轻代的Eden区中初始化一块内存区域。
  2. 当Eden区空间足够时,内存申请结束。否则执行下一步。
  3. JVM试图释放在Eden区中所有不活跃的对象(Young GC)。释放后若Eden空间仍然不足以放入新对象,JVM则试图将部分Eden区中活跃对象放入Survivor区。
  4. Survivor区被用来作为Eden区及年老代的中间交换区域。当年老代空间足够时,Survivor区中存活了一定次数的对象会被移到年老代。
  5. 当年老代空间不够时,JVM会在年老代进行完全的垃圾回收(Full GC)。
  6. Full GC后,若Survivor区及年老代仍然无法存放从Eden区复制过来的对象,则会导致JVM无法在Eden区为新生成的对象申请内存,即出现“Out of Memory”。

对象流转过程

新对象在Eden区,如果Eden区满了,需要进行YGC,YGC会对Eden区和S0或S1区进行垃圾回收,先找到垃圾对象,再清空垃圾对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yz9FATVg-1678169889341)(https://cdn.nlark.com/yuque/0/2022/png/365147/1665755675105-3bb0491c-ec0e-45f3-940a-ca39b5e3fc54.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_47%2Ctext_5Zu-54G15a2m6ZmiLeWRqOeRnA%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10#averageHue=%235a99d3&from=url&id=HSDkE&originHeight=276&originWidth=1643&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

把剩余存活的对象转移到S0区,表示存活对象,并且记录一下这些对象经历过的YGC次数。

如果后续Eden区又满了,则又会进行YGC垃圾回收,同样会针对Eden区和S0和S1区进行垃圾回收,先找到Eden区的垃圾对象并清除,然后对S0区进行垃圾回收,可能还会剩下一些存活对象,把存活对象转移至S1区,并且表示这些对象经历了2次YGC,并且把Eden剩余的对象也转移至S1区,并且记录这些对象经历1次YGC。


后续只要Eden区满了就会进行YGC,就会导致S0和S1区中的对象不断横跳,每跳一次就会加1,最高到15。后续只要Eden区满了就会进行YGC,就会导致S0和S1区中的对象不断横跳,每跳一次就会加1,最高到15。

OOM

除了程序计数器,其他内存区域都有 OOM 的风险。

  1. 年老代溢出,表现为:java.lang.OutOfMemoryError:Javaheapspace
    这是最常见的情况,产生的原因可能是:设置的内存参数Xmx过小或程序的内存泄露及使用不当问题。
    例如循环上万次的字符串处理、创建上千万个对象、在一段代码内申请上百M甚至上G的内存。还有的时候虽然不会报内存溢出,却会使系统不间断的垃圾回收,也无法处理其它请求。这种情况下除了检查程序、打印堆内存等方法排查,还可以借助一些内存分析工具,比如MAT。
  2. 持久代溢出,表现为:java.lang.OutOfMemoryError:PermGenspace
    通常由于持久代设置过小,动态加载了大量Java类而导致溢出,将参数 -XX:MaxPermSize 调大(一般256m能满足绝大多数应用程序需求)。

  • 栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM。
  • Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;
  • 堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;
  • 方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等;
  • 直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。

排查方法

  • 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
  • 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
  • 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。

GC分类

GC(Garbage Collection),垃圾回收,是Java与C++的主要区别之一。一般不需要专门编写内存回收和垃圾清理代码。这是因为在Java虚拟机中,存在自动内存管理和垃圾清理机制。对JVM中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,保证JVM中的内存空间,防止出现内存泄露和溢出问题。

  • Young GC / Minor GC:负责对新生代进行垃圾回收
  • Old GC / Major GC:负责对老年代进行垃圾回收,目前只有CMS垃圾收集器会单独对老年代进行垃圾收集,其他垃圾收集器基本都是整堆回收的时候对老年代进行垃圾收集
  • Full GC:整堆回收,也会堆方法区进行垃圾收集

Eden区满了之后,就会进行YGC,YGC时会STW(Stop The World),会占停用户线程,YGC执行的频率一般会比较高,但是执行的速度会比较块。
老年代满了,就会触发Full GC,一般会在Full GC之前先进行一次 YGC,Full GC也会STW,并且速度比较慢,所以要尽可能的避免Full GC,如果Full GC后,内存还是不足,那么就会报OOM了。

方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是 JVM 的规范,而永久代PermGen是方法区的一种实现方式,并且只有 HotSpot 有永久代。对于其他类型的虚拟机,如JRockit没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。
JDK 1.8 的时候,HotSpot的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。
为什么要将永久代替换为元空间呢?
永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。

JVM 中的常量池

JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。

  • Class文件常量池。class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
  • 运行时常量池:运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
  • 全局字符串常量池:字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
  • 基本类型包装类对象常量池:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
NIO的Buffer提供了DirectBuffer,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。
直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。

参考

[https://blog.csdn.net/youth_lql/category_10603719.html]
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值