程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
总结来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:
内存中最小的区域。
保存了下一条要执行的指令的地址在哪~
指令=>字节码
程序要想运行,JVM就得把字节码加载起来,放到内存中~
程序就会一条一条把指令从内存取出来,放到CPU上执行
也就需要随时记住,当前执行到哪一条了~~
CPU是并发式执行程序的~CPU不是只给你一个进程提供服务的,要服务所有的进程
正因为操作系统是以线程位单位进行调度执行的,每个线程都得记录自己的执行位置。
程序计数器,会每个线程都有一个~
局部变量和方法调用信息
方法调用的时候,每次都调用一个新的方法,都涉及到“入栈”操作。
每次执行完一个方法,都涉及到“出栈”操作~
一个进程只有一份,多个线程公用一个堆~
也是内存中空间最大的区域~
new出来的对象,就是在堆中,对象的成员变量也在堆中。
内置类型的变量,在栈上。
引用类型的变量,在堆上。
这个说法正确吗?
不正确,局部变量,在栈上。成员变量和new的对象,在堆上。
方法区中,放的是“类对象”。
.java -> .class(二进制字节码)
.class会被加载到内存中,也就被JVM构造成了类对象(加载的过程就成为“类加载”)
这里的类对象,就是放到方法区中,类对象就描述这个类长啥样~
static 修饰的成员,就成为了"类属性"
而普通成员,叫做“实例属性”.
类加载,其实是设计一个运行时环境的一个重要的核心的功能.
类加载是要干啥?
把.class文件加载到内存中,构建成类对象。
先找到对应的.class文件,然后打开并读取.class文件,同时初步生成一个类对象
Loading中的一个关键环节,.class里面到底是什么样的?
实现编译器,要按照这个格式来构造
实现JVM要按照这个格式来加载
观察这个格式,就可以看到.class
文件就把.java
文件中的核心信息都表达进去了~
u4就是4个字节的unsigned int
u2就是2个字节的unsigned int
cp_info/field_info都是结构体
magic 标识文件的格式
会把读取和解析到的信息,初步填写到类对象中
连接一般就是建立号多个实体之间的联系~
主要就是验证读到的内容是不是和规范中规定的格式完全匹配~
如果发现这里读到的数据格式不符合规范,就会类加载失败,并且抛出异常~
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
给静态变量分配内存,并且设置0值。
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
.class文件中,常量时集中放置的,每个常量有一个编号,.class文件的结构体初始情况下只是记录了编号,就需要根据编号找到对应的内容,填充到类对象中
真正对类对象进行初始化,尤其时针对静态成员.
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();
}
}
原理:
类加载阶段会进行静态代码块的执行,要想创建实例,要先进行类加载。
静态代码块只是类加载阶段执行一次。
构造方法和构造代码块,每次实例化都会执行,构造代码块在构造方法前面~~
父类执行在前,子类执行在后~
2.“双亲委派模型”
这个东西时类加载的一个环节
这个环节处于Loading阶段的。
双亲委派模型,描述的就是JVM中的类加载器,如何根据类的全限定名找到.class
文件的过程.
JVM里提供了专门的对象,叫做类加载器,负责进行类加载.
当然找文件的过程也是类加载器来负责的~
.class文件,可能放置的位置有很多,有的要放到JDK目录里,有的放到项目目录里,还有在其他特定位置…
因此,JVM里面提供了多个类加载器,每个类加载器负责一个片区~~
默认的类加载器,主要是3个~
1.BootStrapClassLoader 负责加载标准库中的类
2.ExtensionClassLoader 负责加载JDK扩展的类
3.ApplicationClassLoader 负责加载当前项目目录中的类~
垃圾回收的劣势:
1.消耗额外的开销~(消耗资源更多了)
2.可能会影响程序的流畅运行~(垃圾回收经常会引入STW问题(Stop The World))
垃圾回收要回收啥?
内存,有很多种~
1.程序计数器 -> 固定大小,不涉及刀释放,就不需要GC
2.栈 ->函数执行完毕,对应的栈帧就自动释放了,也不需要GC
3.堆 ->最需要GC的,代码中大量的内存都在堆上
4.方法区 ->类对象,类加载来的,进行“类卸载”就需要释放内存,卸载操作是一个非常低频的操作~
如何组织里,人都有三个派别:
1.积极派
2.消极派
3.中间摇摆派
上述三个派别,哪些是要进行回收释放内存的?
对于中间摇摆派,一部分仍在使用,一部分不再使用的对象,整体来说是不释放的
GC中就不会出现,“半个对象”的情况~
垃圾回收的基本单位,是“对象”,而不是“字节”
GC,会提高开发效率,降低程序自身的运行效率。
垃圾回收,具体是如何回收的~
第一阶段:找垃圾/判定垃圾
第二阶段:释放垃圾
如何找垃圾/判定垃圾
主流的思路,有两种方案
1.基于引用计数(不是Java采取的方案)
class Test {
Test t = null;
}
Test t1 = new Test();
Test t2 = new Test();
t1.t = t2;
t2.t = t1;
t1 = null;
t2 = null;
此时此刻,两个对象的引用计数,不为0,所以无法释放,但是由于引用长在彼此的身上,外界的代码无法访问到这两个对象~
此时此刻,这两对象就被孤立了,既不能使用,又不难释放~这就出现了“内存泄露”的问题。
2.基于可达性分析(这是Java采取的方案)
通过额外的线程,定期的针对整个内存空间的对象进行扫描~
有一些起始位置(称为GCRoots),会类似于深度优先遍历一样,把可以访问到的对象都标记一遍(带标记的对象就是可达的对象),没被标记的对象,就是不可达的,也就是垃圾~
回收垃圾
三种基本策略:
1.标记-清除
如果直接释放,虽然内存是还给系统了,但是被释放的内存是离散的(不是连续的)
分散开,带来的问题就是“内存碎片”
空闲的内存,有很多,假设一共是1G
如果要申请500M内存,也是可能申请失败的(因为要申请的500M是连续内存)每次申请,都是申请的连续的内存空间,而这里的1G可能是多个碎片加在一起,才是1G。
用一半,丢一半
直接把不是垃圾的,拷贝到另一半,把原来整个空间整体都释放掉!
复制算法的问题:
1.内存空间利用率低
2.如果保留的对象多,要释放的对象少,此时复制开销大
实际的JVM中的实现,会把很多方案结合起来使用
分代回收,针对对象进行分类(根据对象的“年龄”分类)
1.刚创建出来的对象,就放在伊甸区
2.如果伊甸区的对象熬过一轮GC扫描,就会被拷贝到幸存区
3.在后续的几轮GC中,幸存区的对象,就在两个幸存区里面来回拷贝
4.在持续若干轮之后,对象进入老年代
分代回收中,还有一个特殊情况,有一类对象可以直接进入老年代~(大对象,占有内存多的对象)
大对象拷贝开销比较大, 不适合复制算法~