JavaEE-JVM的学习

目录

  • JVM执行流程
  • JVM内存区域划分
    • 程序计时器
    • 方法区
  • JVM类加载机制
    • 1)Loading环节
    • 2)Linking环节
      • 2.1) Verification
      • 2.2)Preparation
      • 2.3)Resolution
    • 3)Initializing
  • JVM典型面试题
  • JVM的垃圾回收机制(GC)
    • 分代回收

JVM执行流程

程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。

JavaEE-JVM的学习_第1张图片

总结来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:

  1. 类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Native Interface)

JVM内存区域划分

程序计时器

内存中最小的区域。
保存了下一条要执行的指令的地址在哪~

指令=>字节码
程序要想运行,JVM就得把字节码加载起来,放到内存中~
程序就会一条一条把指令从内存取出来,放到CPU上执行
也就需要随时记住,当前执行到哪一条了~~

CPU是并发式执行程序的~CPU不是只给你一个进程提供服务的,要服务所有的进程

正因为操作系统是以线程位单位进行调度执行的,每个线程都得记录自己的执行位置。
程序计数器,会每个线程都有一个~

局部变量和方法调用信息

方法调用的时候,每次都调用一个新的方法,都涉及到“入栈”操作。
每次执行完一个方法,都涉及到“出栈”操作~

JavaEE-JVM的学习_第2张图片
每个线程有一份。

一个进程只有一份,多个线程公用一个堆~
也是内存中空间最大的区域~

new出来的对象,就是在堆中,对象的成员变量也在堆中。

内置类型的变量,在栈上。
引用类型的变量,在堆上。
这个说法正确吗?
不正确,局部变量,在栈上。成员变量和new的对象,在堆上。

方法区

方法区中,放的是“类对象”。
.java -> .class(二进制字节码)
.class会被加载到内存中,也就被JVM构造成了类对象(加载的过程就成为“类加载”)
这里的类对象,就是放到方法区中,类对象就描述这个类长啥样~
在这里插入图片描述
static 修饰的成员,就成为了"类属性"
而普通成员,叫做“实例属性”.

JVM类加载机制

类加载,其实是设计一个运行时环境的一个重要的核心的功能.

类加载是要干啥?
把.class文件加载到内存中,构建成类对象。

JavaEE-JVM的学习_第3张图片

1)Loading环节

先找到对应的.class文件,然后打开并读取.class文件,同时初步生成一个类对象
Loading中的一个关键环节,.class里面到底是什么样的?

JavaEE-JVM的学习_第4张图片
实现编译器,要按照这个格式来构造
实现JVM要按照这个格式来加载
观察这个格式,就可以看到.class文件就把.java文件中的核心信息都表达进去了~

u4就是4个字节的unsigned int
u2就是2个字节的unsigned int
cp_info/field_info都是结构体

magic 标识文件的格式

会把读取和解析到的信息,初步填写到类对象中

2)Linking环节

连接一般就是建立号多个实体之间的联系~

2.1) Verification

主要就是验证读到的内容是不是和规范中规定的格式完全匹配~
如果发现这里读到的数据格式不符合规范,就会类加载失败,并且抛出异常~

2.2)Preparation

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
给静态变量分配内存,并且设置0值。

2.3)Resolution

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
.class文件中,常量时集中放置的,每个常量有一个编号,.class文件的结构体初始情况下只是记录了编号,就需要根据编号找到对应的内容,填充到类对象中

3)Initializing

真正对类对象进行初始化,尤其时针对静态成员.

JVM典型面试题

1.问下面的代码的执行结果是什么?

class A {
    public A() {
        System.out.println("A 的构造方法");
    }

    {
        System.out.println("A 的构造代码块");
    }

    static {
        System.out.println("A 的静态代码块");
    }
}

class B extends A {
    public B() {
        System.out.println("B 的构造方法");
    }

    {
        System.out.println("B 的构造代码块");
    }

    static {
        System.out.println("B 的静态代码块");
    }
}

public class Test extends B {
    public static void main(String[] args) {
        new Test();
        new Test();
    }
}

答案如下:
JavaEE-JVM的学习_第5张图片

原理:
类加载阶段会进行静态代码块的执行,要想创建实例,要先进行类加载。
静态代码块只是类加载阶段执行一次。
构造方法和构造代码块,每次实例化都会执行,构造代码块在构造方法前面~~
父类执行在前,子类执行在后~

2.“双亲委派模型”
这个东西时类加载的一个环节
这个环节处于Loading阶段的。
双亲委派模型,描述的就是JVM中的类加载器,如何根据类的全限定名找到.class文件的过程.

JVM里提供了专门的对象,叫做类加载器,负责进行类加载.
当然找文件的过程也是类加载器来负责的~

.class文件,可能放置的位置有很多,有的要放到JDK目录里,有的放到项目目录里,还有在其他特定位置…
因此,JVM里面提供了多个类加载器,每个类加载器负责一个片区~~
默认的类加载器,主要是3个~
1.BootStrapClassLoader 负责加载标准库中的类
2.ExtensionClassLoader 负责加载JDK扩展的类
3.ApplicationClassLoader 负责加载当前项目目录中的类~

JavaEE-JVM的学习_第6张图片
JavaEE-JVM的学习_第7张图片
JavaEE-JVM的学习_第8张图片

JVM的垃圾回收机制(GC)

JavaEE-JVM的学习_第9张图片
垃圾回收的劣势:
1.消耗额外的开销~(消耗资源更多了)
2.可能会影响程序的流畅运行~(垃圾回收经常会引入STW问题(Stop The World))

垃圾回收要回收啥?
内存,有很多种~
1.程序计数器 -> 固定大小,不涉及刀释放,就不需要GC
2.栈 ->函数执行完毕,对应的栈帧就自动释放了,也不需要GC
3.堆 ->最需要GC的,代码中大量的内存都在堆上
4.方法区 ->类对象,类加载来的,进行“类卸载”就需要释放内存,卸载操作是一个非常低频的操作~

JavaEE-JVM的学习_第10张图片
如何组织里,人都有三个派别:
1.积极派
2.消极派
3.中间摇摆派

上述三个派别,哪些是要进行回收释放内存的?
对于中间摇摆派,一部分仍在使用,一部分不再使用的对象,整体来说是不释放的
GC中就不会出现,“半个对象”的情况~

垃圾回收的基本单位,是“对象”,而不是“字节”

GC,会提高开发效率,降低程序自身的运行效率。

垃圾回收,具体是如何回收的~

第一阶段:找垃圾/判定垃圾
第二阶段:释放垃圾

如何找垃圾/判定垃圾

主流的思路,有两种方案
1.基于引用计数(不是Java采取的方案)

针对每个对象,都会额外引入一小块内存,保存这个对象有多少个引用指向它
JavaEE-JVM的学习_第11张图片

class Test {
	Test t = null;
}
Test t1 = new Test();
Test t2 = new Test();

JavaEE-JVM的学习_第12张图片

t1.t = t2;

JavaEE-JVM的学习_第13张图片

t2.t = t1;

JavaEE-JVM的学习_第14张图片

t1 = null;
t2 = null;

JavaEE-JVM的学习_第15张图片
此时此刻,两个对象的引用计数,不为0,所以无法释放,但是由于引用长在彼此的身上,外界的代码无法访问到这两个对象~
此时此刻,这两对象就被孤立了,既不能使用,又不难释放~这就出现了“内存泄露”的问题。

2.基于可达性分析(这是Java采取的方案)
通过额外的线程,定期的针对整个内存空间的对象进行扫描~
有一些起始位置(称为GCRoots),会类似于深度优先遍历一样,把可以访问到的对象都标记一遍(带标记的对象就是可达的对象),没被标记的对象,就是不可达的,也就是垃圾~
在这里插入图片描述
回收垃圾

三种基本策略:
1.标记-清除

JavaEE-JVM的学习_第16张图片
如果直接释放,虽然内存是还给系统了,但是被释放的内存是离散的(不是连续的)
分散开,带来的问题就是“内存碎片”
空闲的内存,有很多,假设一共是1G
如果要申请500M内存,也是可能申请失败的(因为要申请的500M是连续内存)每次申请,都是申请的连续的内存空间,而这里的1G可能是多个碎片加在一起,才是1G。

2.复制算法
为了解决内存碎片,引入的复制算法
JavaEE-JVM的学习_第17张图片

用一半,丢一半
直接把不是垃圾的,拷贝到另一半,把原来整个空间整体都释放掉!

复制算法的问题:
1.内存空间利用率低
2.如果保留的对象多,要释放的对象少,此时复制开销大

3.标记-整理
JavaEE-JVM的学习_第18张图片

分代回收

实际的JVM中的实现,会把很多方案结合起来使用
分代回收,针对对象进行分类(根据对象的“年龄”分类)

JavaEE-JVM的学习_第19张图片

1.刚创建出来的对象,就放在伊甸区
2.如果伊甸区的对象熬过一轮GC扫描,就会被拷贝到幸存区
3.在后续的几轮GC中,幸存区的对象,就在两个幸存区里面来回拷贝
4.在持续若干轮之后,对象进入老年代

分代回收中,还有一个特殊情况,有一类对象可以直接进入老年代~(大对象,占有内存多的对象)
大对象拷贝开销比较大, 不适合复制算法~

你可能感兴趣的:(JavaEE,jvm,java-ee,学习)