JVM 之体系结构

文章目录

  • JVM体系结构
  • Java类加载过程
  • 类装载器 ClassLoader
    • 有哪些类装载器
    • 双亲委派和沙箱安全
  • 堆体系结构
    • 对象在堆中的生命周期
    • Minor GC的过程
  • 堆参数调优
  • GC常见算法
  • 栈-线程栈
  • 一个简单的Math运行过程
  • 程序计数器
  • 方法区
  • 堆结构和垃圾搜集过程
  • 什么是 STW
  • JVM调优
  • JVM 调优常用参数
  • JVM 调优经验
  • 面试题:怎么判定对象是否存活
  • 面试题:常见的垃圾收集方法
  • 面试题:什么对象进入老年代

JVM体系结构

JVM 之体系结构_第1张图片
JVM 之体系结构_第2张图片

JVM 之体系结构_第3张图片

Java运行时数据区的内存区域简介:

  • 程序计数器: 指向当前线程正在执行的字节码的地址,行号。(记录线程执行到哪里了,为了防止线程被挂起后,重新唤醒从执行到的地方再次执行)
  • Java 虚拟机栈(线程栈): 一个线程对应一块线程栈空间,每个方法在执行的同时都会在线程栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在线程栈中入栈到出栈的过程。
    本地方法栈: 同虚拟机栈,不同的是,它存的是本地方法的数据。
    堆 Heap: 在JVM启动时创建的一块内存区域,是被所有Java线程共享的,不是线程安全的。堆是存储Java对象的地方,保存了所有的对象实例和数组。也是GC(内存回收)管理的主要区域,可以细分为:新生代、老年代、永久代;新生代又分为Eden空间、From Survivor(幸存)空间、To Survivor空间。
    方法区 Method Area: 是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    常量池 Constant Pool: 是方法区的一个数据结构,用于保存编译期间生成的各种字节码和常量字符串等数据。

Java类加载过程

一个Java类从编码到最终完成执行,包括两个过程:编译运行。
编译:通过javac命令将.java文件编程成.class文件。
运行:将.class文件通过类加载器加载到内存中,并运行。

类在JVM中的生命周期: 加载、链接(验证、准备、解析)、初始化、使用、卸载。

类的加载时机:
JVM运行的时候,并不是一次性加载所有类的,而是使用到哪个就加载哪个,并且只会加载一次。
1、new 一个对象实例的时候。
2、访问类或接口的静态变量,或者给静态变量赋值。
3、调用类的静态方法。
4、反射 Class.forName(“com.demo.ClassA”)。
5、初始化一个子类,首先会初始化父类。

类装载器 ClassLoader

类装载器 ClassLoader 是负责加载class文件的,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构。ClassLoader只负责文件的加载,至于它是否可运行,则由Execution Engine决定。

这里需要区分一下classClass。小写的class,是指使用javac命令编译 Java 代码后所生成的以.class为后缀名的字节码文件。而大写的Class,是 JDK 提供的java.lang.Class,可以理解为封装类的模板。多用于反射场景,例如 JDBC 中的加载驱动,Class.forName("com.mysql.jdbc.Driver");

下图Car.class字节码文件被ClassLoader类装载器加载并初始化,在方法区中生成了一个Car Class的类模板,而平时所用到的实例化,就是在这个类模板的基础上,形成了一个个实例,即car1car2。反过来讲,可以对某个具体的实例进行getClass()操作,就可以得到该实例的类模板,即Car Class。再接着,对这个类模板进行getClassLoader()操作,就可以得到这个类模板是由哪个类装载器进行加载的。
JVM 之体系结构_第4张图片
Tip: 扩展一下,JVM并不仅仅只是通过检查文件后缀名是否是.class来判断是否加载,最主要的是通过class文件中特定的文件标示,即下图test.class文件中的cafe babe
JVM 之体系结构_第5张图片

有哪些类装载器

1、虚拟机自带的类加载器

  • 启动类加载器(Bootstrap),也叫根加载器,加载%JAVAHOME%/jre/lib/rt.jar
  • 扩展类加载器(Extension),加载%JAVAHOME%/jre/lib/ext/*.jar,例如javax.swing
  • 应用程序类加载器(AppClassLoader),也叫系统类加载器,加载%CLASSPATH%的所有类。

2、 用户自定义的加载器 : 用户可以自定义类的加载方式,但必须是Java.lang.ClassLoader的子类。
JVM 之体系结构_第6张图片

双亲委派和沙箱安全

父类委托机制。
通过下面代码来观察这几个类加载器。首先,我们先看自定义的MyObject,首先通过getClassLoader()获取到的是AppClassLoader,然后getParent()得到ExtClassLoader,再getParent()竟然是null?可能大家会有疑惑,不应该是Bootstrap加载器么?这是因为,BootstrapClassLoader是使用C++语言编写的,Java在加载的时候就成了null

我们再来看Java自带的Object,通过getClassLoader()获取到的加载器直接就是BootstrapClassLoader,如果要想getParent()的话,因为是null值,所以就会报java.lang.NullPointerException空指针异常。
JVM 之体系结构_第7张图片
输出中,sun.misc.Launcher是JVM相关调用的入口程序。

JVM 之体系结构_第8张图片
自定义了一个java.lang.String类,并且创建main方法后运行,发现报错了,提示找不到main方法,但是明明我们定义了main方法啊,引出双亲委派和沙箱安全

(1)双亲委派,当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是,比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委派给顶层的启动类加载器进行加载,确保哪怕使用了不同的类加载器,最终得到的都是同样一个Object对象。

(2)沙箱安全,是基于双亲委派机制上采取的一种JVM的自我保护机制,假设你要写一个java.lang.String的类,由于双亲委派机制的原理,此请求会先交给BootStrapClassLoader试图进行加载,但是BootStrapClassLoader在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏,确保你的代码不会污染到Java的源码。保证了大家使用的类是同一套体系的,统一的class。保证java源代码不受污染,保证源码干净一致,这叫沙箱安全机制。

所以,类加载器的加载顺序如下:

  • AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载。
  • ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

Tip: rt.jar是什么?为什么可以在idea这些开发工具中可以直接去使用String、ArrayList、甚至一些JDK提供的类和方法?因为这些都在rt.jar中定义好了,且直接被启动类加载器进行加载了。

堆体系结构

Java7之前,堆结构图如下,而Java8则只将永久区变成了元空间。
JVM 之体系结构_第9张图片

Minor GC针对的是新生代的垃圾回收。
在新生代中经历了几次Minor GC仍然存活的对象,就会被放到老年代。
Major GC针对的是老年代的垃圾回收。
Full GC是针对整堆(包括新生代和老年代)做垃圾回收的。
永久代(Perm)主要存放已被虚拟机加载的类信息,常量,静态变量等数据。

对象在堆中的生命周期

1、首先,新生区是类的诞生、成长、消亡的区域。一个类在这里被创建并使用,最后被垃圾回收器收集,结束生命。
2、其次,所有的类都是在Eden Spacenew出来的。而当Eden Space的空间用完时,程序又需要创建对象,JVM的垃圾回收器则会将Eden Space中不再被其他对象所引用的对象进行销毁,也就是垃圾回收(Minor GC)。此时的GC可以认为是轻量级GC
3、然后将Eden Space中剩余的未被回收的对象,移动到From Space,以此往复,直到From Space也满了的时候,再对From Space进行垃圾回收,剩余的未被回收的对象,则再移动到To SpaceTo Space也满了的话,再移动至Old Space
4、最后,如果Old Space也满了的话,那么这个时候就会被垃圾回收(Major GC or Full GC)并将该区的内存清理。此时的GC可以认为是重量级GC。如果Old Space被GC垃圾回收之后,依旧处于占满状态的话,就会产生我们场景的OOM异常,即OutOfMemoryError

Minor GC的过程

Survivor 0 Space,幸存者0区,也叫from区;
Survivor 1 Space,幸存者1区,也叫to区。

其中,from区和to区的区分不是固定的,是互相交换的,意思是说,在每次GC之后,两者会进行交换,谁空谁就是to区。
JVM 之体系结构_第10张图片
(1)Eden Spacefrom复制到to,年龄+1。
首先,当Eden Space满时,会触发第一次GC,把还活着的对象拷贝到from区。而当Eden Space再次触发GC时,会扫描Eden Spacefrom,对这两个区进行垃圾回收,经过此次回收后依旧存活的对象,则直接复制到to区(如果对象的年龄已经达到老年(15)的标准,则移动至老年代区),同时把这些对象的年龄+1。
(2)清空Eden Space、from
然后,清空Eden Spacefrom中的对象,此时的from是空的。
(3)fromto互换
最后,fromto进行互换,原from成为下一次GC时的to,原to成为下一次GC时的from。部分对象会在fromto中来回进行交换复制,如果交换15次(由JVM参数MaxTenuringThreshold决定,默认15),最终依旧存活的对象就会移动至老年代。

总结一句话,GC之后有交换,谁空谁是to

堆参数调优

-Xms:初始堆分配大小,默认为物理内存的 1/64
-Xmx:最大分配内存,默认为物理内存的 1/4
-XX:+PrintGCDetails:输出详细的GC处理日志

IDEA中配置JVM内存参数:
在【Run】->【Edit Configuration…】->【VM options】中,输入参数-Xms1024m -Xmx1024m -XX:+PrintGCDetails,然后保存退出。

JVM的初始内存和最大内存一般怎么配:
初始内存和最大内存一定是一样大,理由是避免GC和应用程序争抢内存,进而导致内存忽高忽低产生停顿。

出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够,造成堆内存溢出。原因有两点:

  • Java虚拟机的堆内存设置太小,可以通过参数-Xms-Xmx来调整。
  • 代码中创建了大量对象,并且长时间不能被GC回收(存在被引用)。
  • 存在大对象。

GC常见算法

引用计数法、标记清除法、标记压缩算法、复制算法、分代算法。

栈-线程栈

线程栈,每一个线程运行的时候,Java虚拟机都会给这个线程分配一块独立的栈内存空间,来放线程的局部变量。

栈的组成:
栈实际上就是栈帧组成的,而每一个栈帧又存储着与之对应的方法的局部变量表,操作数栈,动态链接,方法出口。
也就是一个方法对应一个栈帧,栈帧就是Java中每个方法的存放空间。

  • 局部变量表:存放着方法中的局部变量
  • 操作数栈:用来存放操作方法中的数的一个临时栈内存空间
  • 动态链接:把符号引用存在直接应用存在内存空间中
  • 方法出口:记录该方法调用完毕应该回到的地方

一个简单的Math运行过程

JVM 之体系结构_第11张图片

public class Math {
    public static final int initDate = 666;
    public static User user = new User();

    public Math() {
    }

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 3;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        System.out.println("end...);
    }
}

1、将class文件加载进类加载子系统
2、开辟一个包含堆、方法区、栈、本地方法栈、程序计数器的空间
3、字节码引擎开始执行

反编译的结果:
分析compute()函数的指向流程理解帧栈这个空间。

Compiled from "Math.java"
public class com.shen.Main.jvm.Math {
  public static final int initDate;

  public com.shen.Main.jvm.User user;

  public com.shen.Main.jvm.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: aload_0
       5: new           #2                  // class com/shen/Main/jvm/User
       8: dup
       9: invokespecial #3                  // Method com/shen/Main/jvm/User."":()V
      12: putfield      #4                  // Field user:Lcom/shen/Main/jvm/User;
      15: return

  public int compute();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: iconst_3
       8: imul
       9: istore_3
      10: iload_3
      11: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #5                  // class com/shen/Main/jvm/Math
       3: dup
       4: invokespecial #6                  // Method "":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #7                  // Method compute:()I
      12: pop
      13: return
}

栈帧的局部变量其实是用一个数组进行存储的
其中特殊的局部变量0就是this
public int compute();
Code:
0: iconst_1 将局部变量1 放入到操作数栈
1: istore_1 将int类型的值存入局部变量1(将int值赋给在局部变量表的局部变量)
2: iconst_2
3: istore_2
(这就明白了 这两个实际上指行的就是 现在局部变量表中开辟一个b的空间 然后在从操作数栈中弹栈赋给局部变量b)
4: iload_1 //局部变量1压入栈
5: iload_2 //局部变量2 压入栈
6: iadd //弹栈两次 执行int 类型的add
7: iconst_3 //将计算结果 压入栈
8:bipush //将 10压入栈
9: imul //计算乘法结果 在压入栈中
10: istore_3 //将结果存给局部变量3
11: iload_3 //取出局部变量3的值
12: ireturn //return int

程序计数器

属于线程私有,用来存放线程执行代码的位置(就是.class文件中的行号) 由字节码执行引擎来操作。
Java是多线程的,当一个线程执行的过程中,被挂起了,程序计数器就是记录了被挂起时运行到的位置,然后当重新唤醒后,就会从程序计数器记录的位置处开始执行。

方法区

常量 public static final int initDate = 666;
静态变量(指向堆空间) public static User user = new User();
类信息

JDK8方法区使用的是物理内存,叫元空间。

存放各种new出来的对象
局部变量表会指向
方法区中的静态变量也会指向

堆结构和垃圾搜集过程

JVM 之体系结构_第12张图片
垃圾搜集都是字节码执行器在做。

注: 只是Eden 区。对于S区,一个是 s0 存放着存活对象,一个是 s1 空的,等待 Minor GC 完成后,来转移存活的对象,并不用于分配给新生对象。

  1. 首先 new 出来的对象都会放在 Eden 区,如果 Eden 区没有足够的空间分配给新的对象,会触发Minor GC
  2. 存活对象会放到s0中
  3. 如果 Eden 区再次没有足够的空间分配给新的对象,再次搜集s0和Eden存活对象,放到s1中
  4. 清空 s0、Eden 区,让后 s0 和 s1 对换下,这时 s0 存放了存活的对象,s1 是空的
  5. 重复 第4步和第5步
  6. 当 s1 区空间放不下存活的对象或如果有对象的年龄到达15,会直接把对象转移到老年区
  7. 老年区满了,会启动 Full GC,启动 Full GC 时候必须暂停所有执行的代码(所有的用户线程)也就是STW(stop the world), 因为我们的对象必须要有一个确定的状态,不暂停的话就无法确定

老年代存放的对象: 对象类型的静态变量,缓存对象,数据库连接池中的对象,Spring容器中的Bean(Controller,Service),这些对象都一直被 GC Root引用,所以最终都会放到老年代。还有就是大对象会直接放入到老年代,大对象就是一块连续的内存空间在 Eden 区放不下,会直接进入老年代。
JVM 之体系结构_第13张图片

什么是 STW

stop the world 停止整个世界
STW 会停止所有的用户线程。
进行 Minor GC 和 Full GC 前都会触发STW。

Java虚拟机在进行垃圾回收的时候回触发STW:
如果在进行GC的时候,用户线程也在运行中,GC Root和对象是不断变化的,无法确定一个对象的状态,这时Java设计了STW,暂停所有的用户线程,让所有的对象都有一个确定的状态,然后快速的进行GC来回收垃圾对象,一般STW的时候会很短,对用户来说只是卡顿了下,但是频繁的STW也会使用户感觉网站很卡,用户体验不是很好。

JVM调优

JVM虚拟机调优的目的: 减少 full gc 次数,也就是减少STW次数,减少了STW次数也就是减少了暂停用户线程的次数,卡顿次数少,使用户体验更好。

1、评估系统每秒产生对象的大小
2、新产生的对象在一秒后如果变为垃圾对象,是否会被在minor gc清理
3、可以通过对象的年龄和在minor gc时是否占用survivor区50%,判断对象是否需要被清理还是放到老年代
4、如果在minor gc时频繁的通过占用survivor区50%将对象放到老年代,其实这些对象在一秒后也变成了垃圾对象,但是被放到了老年代,需要通过full gc才会被清理,表示年轻代配置不合理,这时需要加大年轻代的占用空间

优化前参数:
在这里插入图片描述
优化后参数:
在这里插入图片描述

JVM 调优常用参数

JVM优化之 -Xss -Xms -Xmx -Xmn 参数设置

新生代占比: 堆的 1/3
老年代占比: 堆的 2/3

新生代内部划分占比: 分成10份,伊甸区:8/10,两个survivor区分别为1/10

-Xms 堆内存的最小大小,默认为物理内存的1/64
-Xmx 堆内存的最大大小,默认为物理内存的1/4
-Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn
-Xss 设置每个线程可使用的内存大小,即栈的大小。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

JVM 调优经验

频繁的 full gc ,会导致网站很卡。
JVM 之体系结构_第14张图片
分析:
在促销的时候每秒产生1000多单,有3台8G内存的订单系统,分配到每台就是300单/秒。
假如每个订单对象是1KB,每秒就是300KB,下单的时候还会涉及到其他对象,比如库存,积分之类的,差不多放大20倍,每秒产生的对象300KB20对象。
可能同时还会有其他的操作,比如订单查询,库存查询,再放大10倍,每秒产生的对象就是 300KB
20*10 = 60M,在一秒后都变为垃圾对象。

JVM设置给堆的大小是3G,默认老年代是2G,年轻代是1G,年轻代按照8:1:1划分,eden就是800M,每个s区分别就是100M。
按照每秒产生60M对象,在13秒的时候就会占满eden区,进行young gc,这时有一秒的对象是存活的,也就是60M,会进入s区,根据如果在一次young gc后,一批存活的对象总大小大于s区内存大小的50%,那么此时这批对象就可以直接进入老年代。那么我们每13秒就会有60M对象进入老年代,其实这批60M的对象在一秒后就变为垃圾对象了,老年代大小是2G,2048/60*13 差不多在7分钟左右就把老年代占满了,这时会进行full gc把所有的垃圾对象清除。

每7分钟左右进行full gc是不合理的,因为full gc是重量级的,stw会比较长,会导致系统很卡。
我们优化的方向因为是在young gc阶段就把垃圾对象清除掉,因为young gc是轻量级的,stw停顿几乎可以忽略,几乎不会对系统产生影响。
根据上面的案例我们就可以吧年轻代的大小调高到2G,这时eden就有1.6G。每个s区就是200M,这时候大概运行25秒的时候回占满eden,然后进行young gc,存活的对象就是60M,这时没有超过s区的一半,所以不会进入老年代,直接放在s区了,在进行下一次young gc的时候,还是只有60M对象存活,保证了垃圾对象在young gc的时候就被清除了,只有少量一直存活的对象会进入老年代,不会频繁的进行full gc了。

优化前的参数:堆大小是3G,老年代2G,年轻代1G,
JVM 之体系结构_第15张图片

调优后的JVM参数:-Xmn2048,设置年轻代为2G
在这里插入图片描述

面试题:怎么判定对象是否存活

通过可达性分析来判定对象是否存活。
通过一系列被 GC Roots 引用的对象,作为起点,从这些起点向下搜索,当一个对象没有被任何一个 GC Roots 引用时,该对象可以回收。

什么对象是"GC Roots"的对象?

  1. 虚拟机栈中栈桢中的局部变量(也叫局部变量表)中引用的对象。
  2. 方法区中类的静态变量、常量引用的对象
    JVM 之体系结构_第16张图片

面试题:常见的垃圾收集方法

复制算法:
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当一块内存用完了,将存活的对象复制到另一块上面,然后把已使用的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等情况,对象按顺序分配内存即可,实现简单,运行高效。
只是这种算法将内存缩小为原来的一半,代价较高。

标准-清除:
分为两个阶段:首先标记出需要回收的对象,在标记完成后统一回收被标记的对象。
标记清除后会产生大量不连续内存碎片。
标记-整理:
标准-清除 算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。解决了内存碎片问题。

年轻代垃圾回收算法: 使用了复制算法,划分了两个 s 区,总有一个 s 区是空的,用来存放存活的对象。因为每次 young gc 存活的对象很少,不需要划分很大的内存空间,减少了代价。
老年代垃圾回收算法: 使用了 标记-整理。

面试题:什么对象进入老年代

  1. 对象优先在 Eden 分区: young gc 后将存活的对象放入 Survivor 空间,如果 Survivor 空间无法放入对象时,只能通过空间分配担保机制提前转移到老年代。
  2. 大对象直接进入老年代: 怎么判断一个对象是大对象呢,需要大量连续内存空间的 Java 对象,如果在 Eden 区分配失败,直接分配到老年代,到底多大才进入老年代呢,提供了一个-XX:PretenureSizeThreshold=参数(默认是0,表示任何对象首先在 Eden 区分配),大于这个值的参数直接在老年代分配,避免新生代中的 Eden 区及两个 Survivor 区发生大量内存复制。
  3. 长期存活的对象进入老年代: 虚拟机会给每个对象定义一个对象年龄计数器。如果对象在 Eden 出生并且经过一次 Minor GC 后任然存活,且能够被 Survivor 容纳,将被移动到 Survivor 空间中,并且对象年龄设为 1。每次 Minor GC 后对象任然存活在 Survivor 区中,年龄就加 1,当年龄到达-XX:MaxTenuringThreshold (默认是15)参数设定的值时,将会移动到老年代。
  4. 动态年龄判断: 虚拟机不是永远要求对象的年龄必须达到 15 才会将对象移动到老年代去。如果 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  5. 空间分配担保: 在 Minor GC 前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果条件成立,那么 进行 Minor GC ,否则将进行一次 Full GC。

参考:
JVM的体系结构及底层原理
jvm诸葛老师笔记
JVM Minor GC 与 FullGC 的触发时机

你可能感兴趣的:(#,JVM,java,开发语言,后端)