梁恒嘉的技术专栏 全栈之路

JVM中的运行时数据区

2022-10-03
NanKe
JVM

主要记录JVM里面的运行时数据区的知识,包括程序计数器,Java虚拟机栈,本地方方法栈,运行时方法区,堆相关知识;Java虚拟机栈中的栈帧介绍,堆中分区划代,各个分区如何工作,垃圾回收机制在各个分区如何工作

知识图谱:

在这里插入图片描述


一、运行时数据区

主要处理Java程序在运行的时候产生的数据,并进行处理

主要包括:

  1. 程序计数器
  2. Java虚拟机栈
  3. 本地方法栈
  4. 运行时方法区

    1.程序计数器

    是Java中占用很小的一块内存区间,也是不会发生内存溢出的区间。主要是用来记录线程将要执行指令的位置,是线程独立的,生命周期与线程保持一致

2.Java虚拟机栈

Java虚拟机栈是一个运行时单位,负责Java程序在执行方法时,记录执行的方法信息,当有一个方法执行的时候,就会发生压栈操作,存储该方法信息的地方称之为栈帧,当方法执行完毕后,就会出栈,是线程独立的

栈帧的内部结构:

  1. 局部变量表 是一组变量值存储空间,用于存放方法参数与方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用数据类型,则存储指向对象的引用
  2. 操作数栈 栈最典型的一个应用是对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,归根结底就是进行计算的过程,所以可以这样子说:计算机所有的计算过程都是由操作数栈实现的
  3. 动态链接 指向运行时常量池的方法引用,方法的调用地址。方法在运行的时候,会用到类中的常量(为类中属性最开始定义的数据),所以必须要有一个引用指向运行时常量
  4. 方法返回地址 当一个方法执行完毕之后,需要返回到调用它的地方,因此必须在栈帧中保存一个方法返回地址

3.本地方法栈

在Java中使用Native修饰的方法就是本地方法,也就是操作系统的方法,如果调用了本地方法,则把这些本地方法放在本地方法栈中运行,是线程独立的;内存的空间大小可以调整;可能会出现栈溢出

4.堆

堆是一块用于存储JVM里面对象的内存空间,物理上不连续,逻辑上连续;是内存管理的核心区域,也是JVM里面管理的最大一块空间,是垃圾回收的重点区域,堆空间是线程共享的

4.1 分区目的

分区的目的是便于垃圾回收,因为对象的存活时间各不相同,当我们进行垃圾回收的时候,应该选择不同的算法回收对象垃圾,实现比较优的垃圾回收效果

堆的分区:分为新生代与老年代

新生代区又分为:

  1. 伊甸园区 用于存储新创建的对象
  2. 幸存者区 用于存储每次经过垃圾回收之后伊甸园区没有被回收的对象;幸存者区域分为两块,一块是s0区域,一块是s1区域,也可以称之为from区与to区

    4.2 垃圾回收在堆中的运行过程

    假设现在的新生代与老年代都是空的,我们此时创建一个对象,堆会在伊甸园区存储这个对象,当发生垃圾回收之后,如果这个对象没有被回收,伊甸园区的这个对象会被放到幸存者区域s0,并且会在这个对象的对象头区域(分配了4bit)存储它的移动次数,此时会将移动次数变为1;下一次发生垃圾回收之后,如果这个对象没有被回收,则会将它从s0区域移动到s1区域。这个过程中,始终保持着幸存者区域的两个分区有一个分区保持空白,并且伊甸园区是始终存储着新创建对象信息

在刚才的假设上发生点变化,我们一共创建两个对象。第一个对象现在已经被存储在了s0区域,此时创建第二个对象,那么它的变化会是怎样的?当第二个对象被创建后,会被存储在伊甸园区,发生垃圾回收之后,是垃圾的对象,会被移出内存,不是垃圾,则会将这个对象移到s1区域,但是此时s0区域还有一个对象,我们还要保证s0与s1存在一个空白区域,则此时,会检测s0区域的对象,是垃圾则回收,不是垃圾则将它从s0区域移入到s1区域。每发生一次移动都会将对象头信息进行修改,如果移动次数成了15,则将幸存者区域的对象存储到老年代区域

4.3 分区大小

新生代与老年代默认的比率是1:2-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆空间的1/3;如果生命周期长的对象较多时,可以调整这个参数进行调优

伊甸园区存储着新生的对象,这块区域较大;幸存者区域不停地发生着清空与移动,所以这块区域内存大小较小

伊甸园区与两个幸存者区默认是8:1:1-XX:SurvivorRatio=8,表示伊甸园区占新生代区域的8/10

在默认参数下对象经过15次垃圾回收依然存活会去老年代;参数最大值是15

4.4 分代收集思想:

JVM中的垃圾回收可以根据不同的区域进行回收,主要包括Minor GC,Major GC,Full GC

JVM在进行垃圾回收的时候,分为部分收集与整堆收集;部分收集主要针对的是新生代与老师代的堆空间,整堆收集朱要针对整个方法区与堆

部分收集: 分别对堆空间里面的新生代区域与老年代区域进行垃圾回收

  1. 新生代收集(Minor GC/Yong GC):主要收集伊甸园区与幸存者区垃圾
  2. 老年代收集(Major GC/Old GC):主要收集老年代区垃圾

整堆收集:(Full GC):收集整个Java堆与方法区垃圾

整堆收集出现的情况:

  1. System.gc()时
  2. 老年区空间不足
  3. 方法区空间不足

应该尽量避免出现整堆收集

字符串常量池在JDK1.7之后的位置由方法区转移到了堆中,因为方法区垃圾回收只会在Full GC的时候才会被回收,效率较低

4.5 堆空间参数设置

官网地址: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+PrintFlagsInitial 查看所有参数的默认初始值 -XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值) -Xms:初始堆空间内存(默认为物理内存的 1/64) -Xmx:最大堆空间内存(默认为物理内存的 1/4) -Xmn:设置新生代的大小(初始值及最大值) -XX:NewRatio:配置新生代与老年代在堆结构的占比 -XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例 -XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄 -XX:+PrintGCDetails 输出详细的 GC 处理日志

5.运行时方法区

主要存储类信息,如静态常量,静态变量,方法属性;运行时常量池

运行时常量池主要记录类中的常量数值信息(字面量值,符号引用)

方法区是线程共享,也可能出现内存溢出,也会涉及垃圾回收

方法区在JVM启动后就会被创建,方法区的大小决定了系统中可以保存多少类

方法区默认的大小是21M,当内存大小达到21M后,会触发FUll GC,进行回收内存,方法区大小可以设置为 -1,表示内存大小无限制

方法区垃圾回收主要涉及:静态常量以及类信息

类信息回收要满足3个条件

  1. 该类的子类都已经被回收
  2. 改类的java.lang.Class对象已经不被其他地方引用
  3. 改类的类加载器已经被回收

记录JVM里面的类加载系统的相关知识,包括类加载系统,类加载过程,类加载器,双亲委派机制,打破双亲委派机制

知识图谱: 类加载系统


一、类加载系统

类加载系统负责从文件系统或者网络中加载class文件,classLoader只负责class文件的加载,class文件是否可以运行,则由Execution Engine决定。加载的类信息存放在一块称为方法区的内存空间。并且ClassLoader会为加载进来的class文件创建Class类的对象

二、类加载过程

1.加载

通过类名进行加载

Class.forName("具体的类名称");

使用IO读取字节码文件到内存(运行时方法区),生成此类的Class对象

2.链接

  1. 验证:验证文件的格式是否一致;元数据验证,相当于Java语法验证,比如类是否继承其他类
  2. 准备:为类的不含final修饰的静态属性分配内存,并设置默认初始值;不为静态的常量(final修饰的属性)设置默认初始值;比如class文件中存在如下代码:static int num = 1,在这个阶段下的num就会被设置初始值为0
  3. 解析:将类中的二进制数据的符号引用替换成直接引用(符号引用是class中的逻辑符号,直接引用是指内存中的实际地址)

    3.初始化

    对类中的静态变量进行赋值

类什么时候初始化?

  1. 创建类的实例的时候,也就是new一个对象
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(Class.forNmae(“”))
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. 执行该类的main方法

类什么时候不会初始化?

  1. 当我们访问一个类的静态常量的时候,我们访问的这个类是不会被加载的,只是将这个静态常量加载到常量池;静态代码块的加载与类的加载是同时进行的,可以根据静态代码中的代码是否执行判断类是否已经加载,如下面代码:
    public class ClassInit{
    	
     final static int num = 10;
     static{
         System.out.println("static静态代码块执行了!");
     }	
    }
    
    class Test{
     public static void main(String[] args){
         System.out.println(num);//10
     }
    }
    

    运行结果如下图所示:

在这里插入图片描述

  1. 当类被作为数组中的数据类型时,这个类是不会被初始化的

如以下代码:int[ClassInit] demo = new int[5]

类的初始化顺序

对static修饰的变量或语句块进行赋值

如果同时包含多个静态代码块和静态变量,则按照从上到下的顺序依次初始化

如果初始化的时候,一个父类没有初始化,则优先初始化父类

顺序是:父类static => 子类static => 父类构造器方法 => 子类构造方法

public class ClassInit{
	static{
		num = 20;
	}
	static int num = 10;
	public static void main(String[] args){
		//num从准备到初始化值的变化过程 num=0 => num=20 => num=10
		System.out.println(num);//10
	}
}

三、类加载器

指的是负责加载类的类

站在JVM的角度对类加载器进行分类,类加载器分为两类:

  1. 启动类加载器(引导类加载器),这部分不是Java语言写的,负责加载Java核心类
  2. 其他所有类加载器,这些类加载器独立于JVM,存在于JVM外部,并且全部继承于抽象类java.lang.ClassLoader

站在Java开发人员的角度来看,类加载器就应当划分的更加细致,自JDK1.2以来Java一直保持着三层加载器

  1. 启动类加载器
  2. 扩展类加载器
  3. 应用程序类加载器

启动类加载器

这个加载器是使用C/C++语言实现的,嵌套在JVM内部

启动类加载器加载位于jdk/lib的类,具体位置如下图所示:

在这里插入图片描述

扩展类加载器

Java体系类加载器是ClassLoader,扩展类加载器派生于ClassLoader,包含应用程序类加载器

扩展类加载器加载位于jdk/jre/lib/ext的类,具体位置如下图所示:

在这里插入图片描述

应用程序类加载器

负责加载环境变量classpath或path路径下的类库

加载我们自己定义的类,用于加载用户类路径上所有的类,应用程序类加载器由扩展类加载器加载

该类加载器是程序中默认的加载器

应用程序类加载还包括自定义类加载器

双亲委派机制

Java虚拟机对class文件的加载是按需加载,只会加载需要使用的class类,在查找我们需要的class类的时候,Java会采用双亲委派机制去查找。默认是使用的应用程序类加载器,但是此时应用程序类加载器会委托扩展类加载器去查找,扩展类加载器又会委托启动类加载器,启动类加载器此时会在自己的范围类查找class类,如果没有找到则会让扩展类加载器去查找,如果扩展类加载器在其范围类没有找到需要的class类,此时就会让启动类加载器去查找,如果在这个期间找到则返回找到的class类,如果最终找不到则出现ClassNotFound异常。

确保类加载的正确性,安全性

双亲委派机制如何打破

Java类加载器的超类是ClassLoader类,通过继承ClassLoader类实现自定义类加载器,重写loadClass和findClass方法会打破双亲委派机制,代码如下所示:

public class MyClassLoader extends ClassLoader{

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        //该方法默认使用双亲委派机制
        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //该方法默认不会使用双亲委派机制
        return super.findClass(name);
    }
}

类的主动使用与被动使用,主要区别为类是否初始化

主动使用:当类被使用的时候,类会被初始化

  1. 创建类对象,通过new创建对象
  2. 访问类中静态变量/方法
  3. 通过反射机制,动态加载类
  4. 当子类被加载,会先初始化父类
  5. 调用类中的main方法

被动使用:当类被使用的时候,不会被初始化

  1. 访问类中的静态常量
  2. 当被作为类型存在时,如:MyClass[] c = new MyClass[10];

下一篇 JVM垃圾回收

 

Content