主要记录JVM里面的运行时数据区的知识,包括程序计数器,Java虚拟机栈,本地方方法栈,运行时方法区,堆相关知识;Java虚拟机栈中的栈帧介绍,堆中分区划代,各个分区如何工作,垃圾回收机制在各个分区如何工作
知识图谱:
一、运行时数据区
主要处理Java程序在运行的时候产生的数据,并进行处理
主要包括:
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- 堆
- 运行时方法区
1.程序计数器
是Java中占用很小的一块内存区间,也是不会发生内存溢出的区间。主要是用来记录线程将要执行指令的位置,是线程独立的,生命周期与线程保持一致
2.Java虚拟机栈
Java虚拟机栈是一个运行时单位,负责Java程序在执行方法时,记录执行的方法信息,当有一个方法执行的时候,就会发生压栈操作,存储该方法信息的地方称之为栈帧,当方法执行完毕后,就会出栈,是线程独立的
栈帧的内部结构:
- 局部变量表 是一组变量值存储空间,用于存放方法参数与方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用数据类型,则存储指向对象的引用
- 操作数栈 栈最典型的一个应用是对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,归根结底就是进行计算的过程,所以可以这样子说:计算机所有的计算过程都是由操作数栈实现的
- 动态链接 指向运行时常量池的方法引用,方法的调用地址。方法在运行的时候,会用到类中的常量(为类中属性最开始定义的数据),所以必须要有一个引用指向运行时常量
- 方法返回地址 当一个方法执行完毕之后,需要返回到调用它的地方,因此必须在栈帧中保存一个方法返回地址
3.本地方法栈
在Java中使用Native修饰的方法就是本地方法,也就是操作系统的方法,如果调用了本地方法,则把这些本地方法放在本地方法栈中运行,是线程独立的;内存的空间大小可以调整;可能会出现栈溢出
4.堆
堆是一块用于存储JVM里面对象的内存空间,物理上不连续,逻辑上连续;是内存管理的核心区域,也是JVM里面管理的最大一块空间,是垃圾回收的重点区域,堆空间是线程共享的
4.1 分区目的
分区的目的是便于垃圾回收,因为对象的存活时间各不相同,当我们进行垃圾回收的时候,应该选择不同的算法回收对象垃圾,实现比较优的垃圾回收效果
堆的分区:分为新生代与老年代
新生代区又分为:
- 伊甸园区 用于存储新创建的对象
- 幸存者区
用于存储每次经过垃圾回收之后伊甸园区没有被回收的对象;幸存者区域分为两块,一块是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在进行垃圾回收的时候,分为部分收集与整堆收集;部分收集主要针对的是新生代与老师代的堆空间,整堆收集朱要针对整个方法区与堆
部分收集: 分别对堆空间里面的新生代区域与老年代区域进行垃圾回收
- 新生代收集(Minor GC/Yong GC):主要收集伊甸园区与幸存者区垃圾
- 老年代收集(Major GC/Old GC):主要收集老年代区垃圾
整堆收集:(Full GC):收集整个Java堆与方法区垃圾
整堆收集出现的情况:
- System.gc()时
- 老年区空间不足
- 方法区空间不足
应该尽量避免出现整堆收集
字符串常量池在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个条件
- 该类的子类都已经被回收
- 改类的java.lang.Class对象已经不被其他地方引用
- 改类的类加载器已经被回收
记录JVM里面的类加载系统的相关知识,包括类加载系统,类加载过程,类加载器,双亲委派机制,打破双亲委派机制
知识图谱:
一、类加载系统
类加载系统负责从文件系统或者网络中加载class文件,classLoader只负责class文件的加载,class文件是否可以运行,则由Execution Engine决定。加载的类信息存放在一块称为方法区的内存空间。并且ClassLoader会为加载进来的class文件创建Class类的对象
二、类加载过程
1.加载
通过类名进行加载
Class.forName("具体的类名称");
使用IO读取字节码文件到内存(运行时方法区),生成此类的Class对象
2.链接
- 验证:验证文件的格式是否一致;元数据验证,相当于Java语法验证,比如类是否继承其他类
- 准备:为类的不含final修饰的静态属性分配内存,并设置默认初始值;不为静态的常量(final修饰的属性)设置默认初始值;比如class文件中存在如下代码:
static int num = 1
,在这个阶段下的num就会被设置初始值为0 - 解析:将类中的二进制数据的符号引用替换成直接引用(符号引用是class中的逻辑符号,直接引用是指内存中的实际地址)
3.初始化
对类中的静态变量进行赋值
类什么时候初始化?
- 创建类的实例的时候,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forNmae(“”))
- 初始化一个类的子类(会首先初始化子类的父类)
- 执行该类的main方法
类什么时候不会初始化?
- 当我们访问一个类的静态常量的时候,我们访问的这个类是不会被加载的,只是将这个静态常量加载到常量池;静态代码块的加载与类的加载是同时进行的,可以根据静态代码中的代码是否执行判断类是否已经加载,如下面代码:
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 } }
运行结果如下图所示:
- 当类被作为数组中的数据类型时,这个类是不会被初始化的
如以下代码: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的角度对类加载器进行分类,类加载器分为两类:
- 启动类加载器(引导类加载器),这部分不是Java语言写的,负责加载Java核心类
- 其他所有类加载器,这些类加载器独立于JVM,存在于JVM外部,并且全部继承于抽象类java.lang.ClassLoader
站在Java开发人员的角度来看,类加载器就应当划分的更加细致,自JDK1.2以来Java一直保持着三层加载器
- 启动类加载器
- 扩展类加载器
- 应用程序类加载器
启动类加载器
这个加载器是使用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);
}
}
类的主动使用与被动使用,主要区别为类是否初始化
主动使用:当类被使用的时候,类会被初始化
- 创建类对象,通过new创建对象
- 访问类中静态变量/方法
- 通过反射机制,动态加载类
- 当子类被加载,会先初始化父类
- 调用类中的main方法
被动使用:当类被使用的时候,不会被初始化
- 访问类中的静态常量
- 当被作为类型存在时,如:
MyClass[] c = new MyClass[10];