jvm的基本结构
-
类加载器(ClassLoader):加载class文件到内存中进行使用。
-
运行时数据区(Runtime Data Area):这是JVM在运行Java程序期间管理的内存区域,包括方法区(Metaspace)、Java堆(Heap)、虚拟机栈(Stack)、程序计数器和本地方法栈等部分。这些区域负责不同的职能,有各自的生命周期。
- 方法区:存储已被加载的类信息、常量、静态变量等。在JDK 1.8及以后版本中,方法区被实现为Metaspace。
- Java堆:这是JVM中最大的内存区域,用于存储对象实例。堆空间是所有线程共享的,并且是垃圾收集的主要区域。
- 虚拟机栈:每个线程在执行方法时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接等信息。
- 程序计数器:记录当前线程执行的字节码指令地址。
- 本地方法栈:用于支持JVM调用本地方法(如C/C++代码)。
-
执行引擎(Execution Engine):将字节码翻译成底层系统指令并执行,处理程序中的各种运算操作。
-
本地库接口(Native Interface):用于支持JVM调用操作系统级别的本地库或其它语言的代码。
双亲委派
首先我来说一下类加载的一个机制,就是我们自己写的java文件,到最终运行它必须要经过编译和类加载这两个阶段,而编译的过程就是.java文件编译成.class文件,而类的加载过程就是把.class文件加载到jvm内存中,装载完成以后会得到一个class对象,就可以使用new关键字来实例化这个对象,而类的加载过程需要涉及到类加载器,jvm在运行的时候会产生三个类加载器,这三个类加载器组成了一个层级关系,每一个类加载器分别去加载不同作用范围的jar包。
比如说Bootstrap Classloader 主要负责java核心类库的加载,也就是%{JDK_HOME}\lib下面的一个rt.jar和resources.jar等等
Extension Classloder主要负责%{JDK_HOME}\lib\ext目录下的一个jar包和class文件
Application Classloder主要负责当前应用里面ClassPath下面的所有jar包和类文件
除了系统自己提供的类加载器以外还可以通过classloder类来实现自定义加载器去满足一些特殊的需求。
双亲委派呢就是按照类加载器的层级关系逐层进行委派,比如说当我们先加载一个class文件的时候首先会去把这个class文件的查询和加载,委派给父加载器去执行,如果父加载器都无法加载,那么再尝试自己来加载这样一个class。
这样设计的好处我认为有两个:
第一个是安全性,因为这种层级关系实际上代表的是一种优先级,也就是所有的类加载优先要给到Bootstrap Classloder,那么对于核心类库中的一些类呢就没有办法被破坏。
第二个我认为这种层级关系的设计可以避免重复加载导致程序混乱的一些问题,因为如果父加载器已经加载过了那么子加载器就没有必要再去加载了
tomcat为什么要使用自定义类加载器
为了进行类的隔离,如果mcat直接使用AppClassLoader类加载类,那就会出现如下情况:
- 应用A中有一个com.name.Hello.class
- 应用B中也有一个com.name.Hello.class
- 虽然都叫做Helo,但是具体的方法、属性可能不一样
- 如果App Class Loader先加载了应用A中的Hello.class
- 那么应用B中的Hello.class就不可能再被加载了,因为名字是一样
- 如果就需要针对应用A和应用B设置各自单独的类加载器,也就是WebappClassLoader
- 这样两个应用中的Hello.class都能被各自的类加载器所加载,不会冲突
- 这就是Tomcat为什么用自定义类加载器的核心原因,为了实现类加载的隔离
- JVM中判断一个类是不是已经被加载的逻辑是:类名+对应的类加载器实例
运行时数据区域由哪些部分组成,每个部分有哪些作用
局部变量表与操作数栈通常配合一起使用
运行时数据区的详细说明
运行时数据区是JVM内存管理的核心部分,主要包括以下几个子区域:
- 方法区:存储类的元数据、常量池等信息。
- Java堆:存储所有对象实例,是垃圾收集的主要区域,分为新生代和老年代,新生代又分为Eden区和两个Survivor区。
- 虚拟机栈:每个线程都有自己的栈,用于存储局部变量和方法调用的信息。
- 本地方法栈:支持JVM调用本地方法(如C/C++代码)。
- 程序计数器:记录当前线程执行的字节码指令地址。
方法区、Java堆多个线程共享的。
虚拟机栈、本地方法栈、程序计数器每个线程都有自己的一块区域,每个线程单独的。
程序计数器的作用
PC Register,程序计数寄存器,简称为程序计数器
- 是物理寄存器的抽象实现
- 用来记录待执行的下一条指令的地址
- 它是程序控制流的指示器,循环、异常处理、线程恢复等都依赖它来完成
- 解释器工作时就是通过它来获取下一条需要执行的字节码指令的
- 它是唯一一个在JVM规范中没有规定任何内存溢出情况的区域
虚拟机栈(Java栈、Java方法栈)
每个线程在创建时都会创建一个虚拟机栈,栈内会保存一个个的栈帧,每个栈帧对应一个方法。
- 虚拟机栈是线程私有的
- 一个方法开始执行栈帧入栈、方法执行完对应的栈帧就出栈,所以虚拟机栈不需要进行垃圾回收
- 虚拟机栈存在内存溢出、以及栈溢出
- 线程太多,就可能会出现内存溢出,线程创建时没有足够的内存去创建虚拟机栈了
- 方法调用层次太多,就可能会出现栈溢出
- 可以通过-Xss来设置虚拟机栈的大小。
栈帧
操作数据栈,也是栈帧中的一部分,操作数据栈是用来在执行字节码指令过程中用来进行计算的。
操作数栈
0:bipush:代码中给的值
2:istore_1:放入到局部变量表1里面
6:iload_1:从局部变量表中拿1这个位置的数据
操作数栈:执行字节码指令的时候帮助我们或者说是辅助我们去进行计算的一个东西
局部变量:表就是专门去记录我每个方法中的每个局部变量 ,在执行的过程中,这个变量所对应的值是什么,去进行一个实时的记录。
return执行完了,对应的栈帧也会消失掉
本地方法栈
本地方法,在Java中定义的方法,但由其他语言实现
虚拟机栈存的是Java方法调用过程的栈帧,本地方法栈存的是本地方法调用过程的栈帧。
也是线程私有的,也可能会出现内存溢出和栈溢出
堆以及堆中的各个区域的作用是什么?
堆是JVM中最重要的一块区域,JVM规范中规定所有的对象和数组都应该存放在堆中,在执行字节码指令时会把创建的对象存入堆中,对象对应的引用地址存入虚拟机栈中的栈帧中,不过当方法执行完之后,刚刚所创建的对象并不会立马被回收,而是要等JVM后台执行GC后,对象才会被回收。
-Xms:ms(memory start),指定堆的初始化内存大小,等价于-XX:InitialHeapSize
-Xmx:mx(memory max),指定堆的最大内存大小,等价于-XX:MaxHeapSize
一般会把-Xms和-Xmx设置为一样,这样JVM就不需要在GC后去修改堆的内存大小了,提高了效率,默认情况下,初始化内存大小=物理内存大小/64,最大内存大小=物理内存大小/4
可以通过-XX:NewRatio参数来配置新生代和老年代的比例,默认为2,表示新生代占1,老年代占2,也就是新生代占堆区总大小的1/3
一般是不需要调整的,只有明确知道存活时间比较长的对象偏多,那么就需要调大NewRatio,从而调整老年代的占比。
Eden:伊甸园区,新对象都会先放到Eden区(除非对象的大小都超过了Eden区,那么就只能直接进老年代)S0、S1:Survivor0、Survivor1区,也可以叫做from区、to区,用来存放MinorGC(YGC)后存在的对象
默认情况下(Eden区:S0区:S1区)的比例关系为(8:1:1),也就是Eden区占新生代大小的8/10可以通过-XX:SurvivorRatio来调整