浅析 JVM 知识点(一)
一.类加载
1.基本类型和 String 类型被 static final 修饰时不会触发类的加载,当子类访问父类成员触发类加载时,父类为主动加载,子类为被动加载,被动加载不会执行代码块.
什么是双亲委派机制
双亲委派机制:首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
-
BootstrapClassLoader(启动类加载器)由 C++ 编写,加载 Java 核心库
java.*
,同时构造 ExtClassLoader 和 AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作. -
ExtClassLoader (标准扩展类加载器)由 Java编写,加载扩展库,如classpath 中的 jre ,
javax.*
或者 java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。 -
AppClassLoader(系统类加载器)由 Java编写,加载程序所在的目录,如user.dir 所在的位置的 class
-
CustomClassLoader(用户自定义类加载器)由 Java 编写,用户自定义的类加载器,可加载指定路径的 class 文件
类加载
类加载:类的加载指的是将类的 .class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class
对象,用来封装类在方法区内的数据结构
- 类加载过程:类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
1、加载
”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:
-
通过一个类的全限定名来获取其定义的二进制字节流
-
将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
-
在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。我们在最后一部分会详细介绍这个类加载器。在这里我们只需要知道类加载器的作用就是上面虚拟机需要完成的三件事,仅此而已就好了。
2、验证
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的 .class
文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:
-
文件格式的验证:验证
.class
文件字节流是否符合 class 文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class
文件里面包含的数据信息、在这里可以不用理解)。 -
元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
-
字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的事。
-
符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用 -Xverfity:none
来关闭大部分的验证。
3、准备
准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:
-
类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
-
这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如
public static int value = 1
; 在这里准备阶段过后的 value 值为 0,而不是 1。赋值为 1 的动作在初始化阶段。
注意,在上面 value 是被 static 所修饰的准备阶段之后是 0,但是如果同时被 final 和 static 修饰准备阶段之后就是 1 了。我们可以理解为 static final在编译期就将结果放入调用它的类的常量池中了。
4、解析
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
5、初始化
这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()
方法的过程。
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:
-
声明类变量是指定初始值
-
使用静态代码块为类变量指定初始值
JVM初始化步骤
-
假如这个类还没有被加载和连接,则程序先加载并连接该类
-
假如该类的直接父类还没有被初始化,则先初始化其直接父类
-
假如类中有初始化语句,则系统依次执行这些初始化语句
Class初始时机:
-
创建类的实例(四种方式) new/反射instance/序列化/clone
-
访问类中的某个静态变量,或者对静态变量赋值
-
主动调用类的静态方法
-
class.forName(“全限定类名”)
-
完成子类的初始化,也会对本类的初始化(接口例外)
-
该类是程序引导入口(main入口或者test入口)
双亲委派机制的作用
-
防止重复加载同一个
.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 -
保证核心
.class
不能被篡改。通过委托方式,不会去篡改核心.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个 Class 对象。这样保证了 Class 执行安全。
自定义类加载器
在这一部分第一小节中,我们提到了 Java 系统为我们提供的三种类加载器,还给出了他们的层次关系图,最下面就是自定义类加载器,那么我们如何自己定义类加载器呢?这主要有两种方式
-
遵守双亲委派模型:继承
ClassLoader
,重写findClass()
方法。 -
破坏双亲委派模型:继承
ClassLoader
,重写loadClass()
方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。
我们看一下实现步骤:
-
创建一个类继承
ClassLoader
抽象类 -
重写
findClass()
方法 -
在
findClass()
方法中调用defineClass()
二.JVM 内存模型
-
JVM 可以分为内存模型、类加载子系统、字节码执行引擎三部分,其中主要分析内存模型。
-
内存模型由虚拟机栈,堆,程序计数器,本地方法栈组成,其中方法区在 jdk8 中由元数据区替代,并且堆中的永久代也移至元数据区,此块空间使用的是 os 中的直接内存.
虚拟机栈
虚拟机栈由局部变量表,操作数栈,动态链接,方法出口组成,每执行一个方法都会对应的在栈中创建一个栈帧,方法的执行和结束对应着虚拟机栈的入栈出栈.生命周期与线程相同,线程私有。堆和元数据区为线程共享。栈帧中局部变量表 数量,如果是类方法(static)是从0开始的,如果是从1开始的话,0的被this当前对象使用
堆
堆空间由年轻代,老年代组成其中年轻代占 1/3 ,老年代占 2/3 ,永久代在 JDK8 移至元数据区.其中年轻代分为 eden 和 from、to,占比为 8/10 , 2/10.
在创建对象时会在 eden 中初始一次,当 Eden 区满的时候,执行 Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区 Survivor0(此时,Survivor1是空白的,两个 Survivor 总有一个是空白的);
下次 Eden 区满了,再执行一次 Minor GC,将消亡的对象清理掉,将存活的对象复制到 Survivor1中,然后清空 Eden 区,将 Survivor0 中消亡的对象清理掉,将其中可以晋级的对象晋级到 Old 区,将存活的对象也复制到 Survivor1 区,然后清空 Survivor0 区;
当两个存活区切换了几次(HotSpot虚拟机默认15次,用 -XX:MaxTenuringThreshold
控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代.
当老年代存满后会执行 FULLGC
,此时停止所有线程等待清除.
本地方法栈
本地方法栈:用于调用本地方法
程序计数器
程序计数器:用于存储 JVM 指令,因为多线程环境下,CPU 频繁的切换,需要记录 JVM 指令的位置
元数据区
元数据区:使用操作系统内存,用于存储字符串常量池,常量,静态方法,类字节码信息,因为 JDK 厂商的不同,在 JDK8 时将永久代并入元数据区.
三.四大引用
-
强引用:如 Object o=new Object() 此类引用即使内存溢出也不会被gc回收掉
-
弱引用:如 WeakRefence<T> sr=new WekRefence(Object o) 此类引用触发GC时会回收此对象
-
软引用:如 SoftRefence<T> wr=new SoftRrfence(Object o) 此类引用内存溢出时直接被回收
-
虚引用:不常用,用于gc时通知,随时可以被回收
四.逃逸分析
- JVM 开启逃逸后会分析创建的对象是否分配在堆中.开启后有以下好处:
-
锁消除:如果该对象只在当前线程使用那么它可以是不同步的
-
标量替换:基本类型和对象的引用可以理解为标量而对象可理解为聚合量,标量不可再分解,而聚合量可以再分解为标量,当这个对象的访问方式不需要是一块连续的内存结构时,它可以存储在 CPU 寄存器中
-
栈上分配:如果该对象的引用永远不会逃逸,那么它可以分配在栈上
volatile关键字作用
-
防止jvm指令重排序(jvm在编译时会进行指令重排序)
-
保证可见性(一个线程的修改对另个线程是可见的)
-
不保证原子性(可由 syhcnhronized 保证,即保证原子性也可以保证可见性或使用 JUC 包中,如原子类, AtomicInteger)
- 点赞
- 收藏
- 关注作者
评论(0)