jvm基础知识

1.jvm基础知识

说明:jvm除了是压测过程中重点关注的内容,也是面试的重点

1.1 基础回顾

java特点:

跨平台

内存泄漏是过程,内存溢出是结果

jdk,jre,jvm的关系

jdk<jre<jvm

数据类型及存储

基本数据类型:存储在栈内存

引用数据类型:值存在堆内存,栈里存堆内存中存放的值的首地址

堆栈:

堆:线程共有的

栈:运行时单位

1.2 源码到类文件经过了哪些流程

1.2.1 前期编译

Student.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树
-> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> Student.class文件

class文件是16进制编码格式文件

1.3 类加载流程(类文件到虚拟机)

所谓类加载机制就是:

  • 虚拟机把Class文件加载到内存
  • 并对数据进行校验,转换解析和初始化
  • 形成可以虚拟机直接使用的Java类型,即java.lang.Class

 

1.3.1 装载(Loading)

查找和导入class文件
(1)通过一个类的全限定名获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问

Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。在
Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

1.3.2 类加载器ClassLoader

在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载
器完成,顾名思义,就是用来装载Class文件的。

1.3.1.1 类加载器分类

1)Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或
Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。
2)Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中
jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
3)App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和
jar包。
4)Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自
身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

1.3.1.2 图解

 

public class Demo3 {
public static void main(String[] args) {
// App ClassLoader
System.out.println(new Worker().getClass().getClassLoader());
// Ext ClassLoader
System.out.println(new
Worker().getClass().getClassLoader().getParent());
// Bootstrap ClassLoader
System.out.println(new
Worker().getClass().getClassLoader().getParent().getParent());
System.out.println(new String().getClass().getClassLoader());
}
}

 

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3a71f4dd
null
null

1.3.1.3 加载原则(双亲委派)

(1)检查某个类是否已经加载
自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已加载,
就视为已加载此类,保证此类只所有ClassLoader加载一次。
(2)加载的顺序
自顶向下,也就是由上层来逐层尝试加载此类。

 

1.3.1.4 破坏双亲委派

(1)tomcat

 

(2)SPI机制
(3)OSGi 

1.3.3 链接(Linking)

1.3.3.1 验证 (Verify)

保证被加载类的正确性

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

1.3.3.2 准备(Prepare)

为类的静态变量分配内存,并将其初始化为默认值

public class Demo1 {
private static int i;
public static void main(String[] args) {
// 正常打印出0,因为静态变量i在准备阶段会有默认值0
System.out.println(i);
}
}

 

public class Demo2 {
public static void main(String[] args) {
// 编译通不过,因为局部变量没有赋值不能被使用
int i;
System.out.println(i);
}
}

 

1.3.3.3 解析(Resolve)

把类中的符号引用转换为直接引用

符号引用就是一组符号来描述目标,可以是任何字面量。

符号引用和内存无关,只是一个代称
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

直接引用对应内存中的地址

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进
行。

1.3.4 初始化(Initializing)

对类的静态变量,静态代码块执行初始化操作

1.4 Run-Time Data Areas( 运行时数据区)

在装载阶段的第(2),(3)步可以发现有运行时数据,堆,方法区等名词
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
说白了就是类文件被类装载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数据得要
有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)

 1.4.1 方法区(Method Area)

(1)方法区是各个线程共享的内存区域,在虚拟机启动时创建

(2)虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非

堆),目的是与Java堆区分开来 
(3)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 
(4)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常 
 
此时回看装载阶段的第2步,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
如果这时候把从Class文件到装载的第(1)和(2)步合并起来理解的话,可以画个图

 

值得说明的:
JVM运行时数据区是一种规范,真正的实现
在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space 

 1.4.2  Heap(堆) 

  • Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。
  • Java对象实例以及数组都在堆上分配。
  • 生命周期和虚拟机一样
  • 垃圾收集的主要区域
说明:对象内存也可以分配在栈上;逃逸分析
 
此时回看装载阶段的第3步,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法
区中这些数据的访问入口 

 1.4.3 Java Virtual Machine Stacks(虚拟机栈) 

经过上面的分析,类加载机制的装载过程已经完成,后续的链接,初始化也会相应的生效。
假如目前的阶段是初始化完成了,后续做啥呢?肯定是Use使用咯,不用的话这样折腾来折腾去有
什么意义?那怎样才能被使用到?换句话说里面内容怎样才能被执行?比如通过主函数main调用
其他方法,这种方式实际上是main线程执行之后调用的方法,即要想使用里面的各种内容,得要
以线程为单位,执行相应的方法才行。那一个线程执行的状态如何维护?一个线程可以执行多少个
方法?这样的关系怎么维护呢
(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的
运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创
建。
(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。 
调用一个方法,就会向栈中压入一个栈帧(入栈);一个方法调用完成,就会把该栈帧从栈中弹出(出栈)。
 
void a(){
b();
}
void b(){
c();
}
void c(){
}

 图解栈和栈帧

 

栈帧

每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A
reference to the run-time constant pool)即动态链接、方法返回地址(Return Address)和附加信息。
局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中
局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使
用。
操作数栈:以压栈和出栈的方式存储操作数的
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用
过程中的动态连接(Dynamic Linking)。
方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇
见异常,并且这个异常没有在方法体内得到处理。

结合字节码指令理解栈帧 

javap -c Person.class > Person.txt 
Compiled from "Person.java"
class Person {
...
public static int calc(int, int);
Code:
0: iconst_3 //将int类型常量3压入[操作数栈]
1: istore_0 //将int类型值存入[局部变量0]
2: iload_0 //从[局部变量0]中装载int类型值入栈
3: iload_1 //从[局部变量1]中装载int类型值入栈
4: iadd //将栈顶元素弹出栈,执行int类型的加法,结果入栈
5: istore_2 //将栈顶int类型值保存到[局部变量2]中
6: iload_2 //从[局部变量2]中装载int类型值入栈
7: ireturn //从方法中返回int类型的数据
...
}

 栈溢出:java.lang.StackOverflowError

常见:递归调用没结束条件、调用链过长

1.4.4  The pc Register(程序计数器)

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU
调度来的。
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得
CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位
置。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。 

1.4.5  Native Method Stacks(本地方法栈) 

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
那如果在Java方法执行的时候调用native的方法呢?

 1.4.6 小知识

1.4.6.1 栈指向堆

如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象。 
 

1.4.6.2 方法区指向堆 

方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。
private static Object obj=new Object();

1.4.6.3 堆指向方法区

堆还能指向方法区?
注意,方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢? 

一个对象怎么知道它是由哪个类创建出来的?怎么记录?这就需要了解一个Java对象的具体信息咯。

1.4.6.4 Java对象内存模型 

 

一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充

 1.5 JVM内存模型 

1.5.1 与运行时数据区

上面对运行时数据区描述了很多,其实重点存储数据的是堆和方法区(非堆),所以内存的设计也着重从这
两方面展开(注意这两块区域都是线程共享的)。
对于虚拟机栈,本地方法栈,程序计数器都是线程私有的。
可以这样理解,JVM运行时数据区是一种规范,而JVM内存模式是对该规范的实现 

1.5.2 图形展示 

一块是非堆区,一块是堆区
堆区分为两大块,一个是Old区,一个是Young区
Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区
S0和S1一样大,也可以叫From和To 

 1.5.3 对象的创建过程

一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。
一般的普通对象,创建后会被分配到Eden区,随着Eden区对象越来越多,Eden会触发Minor GC,
未被回收的对象会进入到s0区,每触发一次YGC,未被回收的对象分带年龄+1,
并且会在s0和s1区来回切换。
直到分带年龄到达15岁(分带年龄可设置)就会被放入Old区,准备下一次FGC

 

 1.5.4 常见问题

1.5.4.1 如何理解Minor/Major/Full GC 

Minor GC:新生代
Major GC:老年代
Full GC:新生代+老年代

1.5.4.2 为什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full
GC)。
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。
可能你会说,那就对老年代的空间进行增加或者较少咯。
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一
旦发生Full GC,执行所需要的时间更长。
假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保
证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

1.5.4.3 为什么需要两个Survivor区? 

最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置
Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor
区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些
存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,
也就导致了内存碎片化。
永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

1.5.4.4 新生代中Eden:S1:S2为什么是8:1:1? 

新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区为8:1
即新生代中Eden:S1:S2 = 8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是
“朝生夕死”的

1.5.4.5 堆内存中都是线程共享的区域吗? 

JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread
Local Allocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
posted @ 2023-07-15 18:11  万溪汇海  阅读(28)  评论(0编辑  收藏  举报