字节码由五个部分组成:基础信息 常量池 字段 方法 属性
基础信息:
魔数、字节码文件对应的版本号、访问标识(public final)、该类的父类索引、该类实现哪些接口的索引
魔数:文件无法根据文件拓展名来确定文件类型的,文件拓展名可以随意修改,但不影响文件的内容,软件使用文件的头几个字节来校验文件类型,如果软件不支持该类型就会出错,在JAVA字节码文件中,将文件头几个字节称为魔数
主副版本号:主版本号用于表示大版本号,JDK1.0~1.1
使用了45.0~45.3,JDK1.2
是46之后每升级一个大版本就加1,副版本号是用作于当主版本号相同时区分不同版本的表示。
所以说JDK1.2
以后版本计算方式为主版本号 - 44
版本是用作于检查字节码文件与当前的JVM版本是否兼容,高版本字节码无法使用低版本JVM编译
常量池:为了避免相同的内容被重复定义 常量池里面的数据都有一个编号,字段可以通过编号快速的找到对应的数据。字节码指令中通过编号引用到常量池的过程被称作符号引用。
字符串常量、类名、接口名、字段名 主要用于字节码指令中使用
字段:当前类或者接口声明的字段信息
方法:将当前类或者接口声明的方法信息转换成字节码指令
int i = 0;
i = i++;//请问最后 i 是几?
想要解决这个问题就要通过字节码指令来看了
字节码文件中方法是将方法信息转换成字节码指令的,这里也是一样对应的字节码指令
iconst_0 //表示将 常量0 放到 操作数栈中 进行操作
istore_1 //表示将 操作数栈中的数 加载到 局部变量表数组 中
iload_1 //表示将 局部变量表中下标为1的数 存放到 操作数栈中
iinc 1 by 1 //表示 将局部变量表中 对应的下标 增加操作
istore_1
return
所以最后返回的是0,因为最后操作栈中的0覆盖了自增操作的1
如果是将代码修改成
int i = 0;
i = ++i;//请问最后 i 是几?
那么字节码文件就会变成
iconst_0 //表示将 常量0 放到 操作数栈中 进行操作
istore_1 //表示将 操作数栈中的数 加载到 局部变量表数组 中
iinc 1 by 1 //表示 将局部变量表中 对应的下标 增加操作
iload_1 //表示将 局部变量表中下标为1的数 存放到 操作数栈中
istore_1
return
最后返回1 没有覆盖操作
属性:类的属性,比如源码的文件名、内部类的列表等
第一步是将类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取到字节码信息
类加载器加载完类以后,Java虚拟机会将字节码中的信息保存到方法区中
生成InstanceKlass
对象,保存类里面的信息,包含实现特定功能比如多态的信息
在堆里面生成一份与方法区中数据类似的Class对象,用于去获取类的信息以及存储静态字段的数据
在堆中的Class对象包含的信息只有方法区的Class对象的一部分,对于开发者来说只需要方法Class对象而不需要访问方法区中的所有信息,所以这么做是为了保证安全性才让开发者获取到一部分数据。
验证 -> 准备 -> 解析
验证:校验Java字节码文件是否遵循Java虚拟机规范中的约束
准备阶段:
解析:
初始化阶段会执行静态代码块中的代码并且为静态变量赋值
会执行字节码文件中 clinit
部分的字节码指令
clinit
是类的初始化方法 init
是构造方法 main
是主方法
首先将1放入到操作栈中,然后对Demo1中的value进行赋值
然后再将2放入到操作栈中,在对Demo1中的value进行赋值操作
执行Main方法的当前类先加载静态代码块然后再执行代码块最后执行构造方法
静态代码块是类的属性所以在类加载的时候就会被执行,同样的只会被执行一次
实例代码块和构造方法只有在创建对象的时候会被执行,执行顺序是 实例代码块->构造方法
类加载器是Java虚拟机提供给应用程序区实现获取类或者接口字节码数据的技术
类加载器只参与加载过程中的字节码获取并且加载这一部分内容类加载器的分类
Java代码中实现
扩展类加载器 - 允许扩展Java中比较通用的类
应用程序类加载器 - 加载应用使用的类
Java虚拟机底层源码实现
启动类加载器 - 加载Java中最核心的类
由Hotspot虚拟机提供使用 C++编写的类加载器
作用:默认加载Java安装目录/jre/lib下的类加载器 为Java开发提供基础环境
使用类加载器去加载用户jar包,使用参数进行扩展 使用 -Xbootclasspath/a:jar 包目录/jar包名 进行扩展
启动类加载器和扩展类加载器都是由JDK提供的,都继承自URLClassLoader
都具备通过目录或者指定jar包将字节码文件加载到内存中
默认加载Java安装目录下/jre/lib/ext下的类文件 加载通用但是不重要的类
使用类加载器去加载用户jar包,使用参数进行扩展 使用 -D/Java.ext.dirs=jar 包目录/jar包名 进行扩展
双亲委派模型的过程是:从下向上不断的检查该类有没有被加载过,如果没有就继续交给上一个类,不断检查。然后再从上到下不断地尝试去加载该类,只有当该类在自己的加载目录底下的时候,这个类才能被加载
自定义类加载器
在继承类加载的方法中:
loadClass方法是类加载的入口提供双亲委派机制内部会调用findClass
findClass由类加载器子类实现,获取二进制数据调用defineClass,比如URLClassLoader会根据文件路径获取类文件中二进制数据
defineClass做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
resolveClass执行类生命周期中的连接阶段
所以说双亲委派机制的核心代码就在loadClass方法中
核心代码
if(parent != null){
c = parent.loadClass(name,false);
}else{
c = findBootstrapClassOrNull(name);
}
if(c == null){
c = findClass(name);
}
所以说使用自定义类加载器打破双亲委派机制的方法就是自定义一个类加载器并且重写loadClass方法
自定义类加载器的父加载器还是应用程序类加载器
如果两个自定义类加载器加载相同限定名的类不会发生冲突,只有在同一个JAVA虚拟机中使用相同类加载器 + 相同类限定名才会被认为是同一个类
线程上下文类加载器
以JDBC为例子,启动类加载器加载DriverManager,在从根源上谷歌呀DriverManger时,通过SPI机制加载Jar包中的mysql驱动,SPI中利用线程上下文加载器去加载类并且创建对象,这种由启动类加载器委派应用程序类加载器去加载类的方式,打破了双亲委派机制
Osgi框架的类加载器
允许同级的类加载器相互委托加载,OSGI还使用类加载器实现热部署的功能
热部署指的是服务不停止的 情况下动态地更新字节码到内存中
JDK8以及之前的版本中,扩展类加载器和应用程序类加载器的源码位于rt.jar中的sun.misc.launcher.java中
JDK9以后引入了mdoule的概念,类加载器在设计上发生了很多变化,启动类加载器使用Java编写,位于jdk.internal.loader.Classloaders中,Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到了要加载的字节码资源文件,启动类加载器依然无法通过java代码获取到,返回依然的是null保持统一
扩展类加载器替换成了平台类加载器
平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader实现了从模块中加载字节码文件,平台类加载器的存在更多的是为了与老版本设计方案兼容,自身没有特殊逻辑
java虚拟机在运行java程序过程中管理的内存区域称之为运行时数据区
线程不共享 - 程序计数器、Java虚拟机栈、本地方法栈
每个线程会通过程序计数器记录当前要执行的字节码指令的地址
在代码执行过程中,程序计数器会记录下一行字节码指令的地址。执行完当前指令以后,虚拟机的执行引擎根据程序计数器执行下一行指令。
作用:
根据程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑
在多线程环境下Java虚拟机通过程序计数器记录CPU切换前解释执行到那一句指令并继续解释运行
注意:程序计数器是不会发生内存溢出的,因为每个程序计数器只会记录固定长度的指令
采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存
Java虚拟机栈随着线程创建而创建,而回收是在线程的销毁时进行的,由于方法可能会在不同线程中执行,每一个线程都会包含一个自己的虚拟机栈
局部变量表 - 是在运行过程中存放所有的局部变量
这就是局部变量,会存放到局部变量表中
局部变量表实际上是一个数组,数组中的每一个位置都被称为一个槽,long和double占用两个槽,其他类型占用一个槽
实例对象中序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存里面存放实例对象的地址
局部变量表中保存的内容还有方法参数,顺序和方法中参数定义的顺序一致
综上:局部变量中保存的内容有:实例方法的this对象、方法的参数、方法体中声明的局部变量
同时为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽可以再次被使用
操作数栈 - 栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值
在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小
帧数据 - 包含动态连接、方法出口、异常表的引用
Java虚拟机栈会出现内存溢出的问题,如果内存溢出了就会报错StackOverFlow表示栈溢出了
如果我们不指定栈的大小,JVM会创建一个默认大小的栈,大小取决于操作系统和计算机体系结构
可以使用递归模拟栈溢出
可以使用 -Xss(栈大小) 来修改虚拟机栈的大小
Hotspot JVM 对栈的大小的最大值和最小值有要求
最小为180k最大为1024k
本地方法栈存储的是native本地方法栈帧,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,零食保存方法的参数同时方便出现异常时也把本地方法打印出来
方法区是存放基础信息的位置,线程共享,包含三部分:
类的元信息 - 保存了所有类的基本信息
这部分会生成一个instanceklass文件存储在方法区中,在类的加载阶段完成
运行时常量池 - 保存了字节码文件中的常量池内容
通过编号查表的方式找到常量,这种常量被称作静态常量池
当常量池加载在内存中,通过内存地址快速的定位到常量池中的内容,这种常量池被称作运行时常量池
JDK7及以前的版本将方法区存放在堆区域中的永久代空间,堆大小由虚拟机参数来控制
JDK8及以后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限可以一直分配,为了防止把内存占用完,可以使用 -XX:MAXMetaspaceSize = 值来限制元空间的大小
方法区中除了类的元信息,运行时常量池外,还有一块字符串常量池
字符串常量池存储在代码中定义的常量字符串内容。比如”123“的123就会被存储在字符串常量池中
字符串常量池时属于运行时常量池的一部分,他们存储的位置是一致的,后续做出了调整,将字符串常量池和运行时常量池做了拆分
使用.intern()方法是可以手动将字符串放入字符串常量池中,分别在JDK6和JDK8下执行代码
JDK6中的结果false false 而JDK8的结果时true false
JDK6 的intern()方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代里面这个字符串实例的引用。JVM启动时会把java加入到常量池中。
JDK7及以后版本中由于字符串常量池在堆上,所以intern()方法会把第一次遇到的字符串引用放入到字符串常量池中
堆内存是空间最大的一块内存区域,创建出来的对象都存在堆上
栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的应用,通过静态变量可以实现对象在线程之间的共享
堆内存也是有上限的,如果超过了堆内存那么就会报内存溢出的错误
堆空间有三个需要关注的值
随着堆中的对象增多,当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆
但也不能无限扩张,total最多与max持平
并不是used = max = total的时候,堆内存才会溢出,内存溢出的条件判断比较复杂,在max还没到total的时候就会内存溢出了
如果不设置任何虚拟机参数,max默认时系统的1/4,total的默认值是系统内存的1/64。在实际应用中一般要设置max和total的值使用 -Xmx -Xmx 来进行设置,通过将total和max设置为相同的值,这样程序启动之后可使用的总内存就是最大内存,无需向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况
直接内存并不属于Java运行时的内存区域,在JDK1.4中引入了NIO机制,使用直接内存,主要是为了解决
如果使用到NIO就需要设置直接内存的大小了, -XX:MaxDirectMemorySize = 内存大小
在C++中这类没有自动回收机制的语言里面,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏
内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出
Java为了简化对象释放,引入了自动的垃圾回收机制,通过垃圾回收器来堆不再使用的对象完成自动回收,垃圾回收器不再使用的对象完成自动回收,垃圾回收器主要负责对堆上的内存进行回收。
优点:降低程序员实现难度,降低对象回收BUG的可能
缺点:程序员无法控制内存回收的及时性
优点:回收及时性高,由程序员自己把握回收的时机
缺点:编写不当容易造成悬空指针,重复释放,内存泄漏等问题
由于程序计数器 本地方法栈 虚拟机栈是线程独享的所以不会被垃圾回收器回收,他们随着线程创建而创建,线程销毁而销毁
而堆和方法区是存放公共对象所以会被垃圾回收器回收
方法区回收的主要是不再使用的类,如何判断在方法区上的一个类是否被回收需要满足三个条件
如何判断在堆上的对象是否可以被回收
根据堆里面的对象是否被引用来决定,如果对象被引用了,说明该对象还在被使用不能被回收
引用计数法:引用计数法回味每一个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
可达分析法:某个到GC Root对象是可达的,对象就不可被回收
GC Root对象:
可达性算法中描述的对象引用,一般指的是强引用,如果有引用关系只要这层关系存在就不会被回收。除了强引用,还有其他的引用方式
找到内存中存活的对象,释放不再存活的内存,使得程序能够再次利用这部分空间
垃圾回收是通过GC线程完成的,不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这部分时间被称作STW
垃圾回收算法评判:
将所有的存活对象进行标记,使用可达性分析算法,从 GCRoot 开始通过引用链遍历出所有的存活对象 清除阶段,将内存中删除没有被标记也就是非存活对象
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可
缺点:碎片化问题,由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。我如果需要的是一个比较大的内存空间,很有可能这些内存单元的大小过小无法进行分配
准备两块空间From空间和T空间,每次在对象分配阶段,只能使用一部分空间
完整的复制算法的例子:
将堆内存分割成两块From空间和To空间,对象分配阶段,创建对象
在垃圾回收GC阶段,将From中存活对象复制到To空间
将GC Root关联的对象,搬运到To空间
优缺点:
优缺点:
分代回收时,创建出来的对象,首先会被放入伊甸园区
随着对象在伊甸园区越来越多,如果伊甸园区满了,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC
Minor GC会把需要的eden中和From需要回收的对象回收,把没有回收的对象放入To区
如果Minor GC后对象的年龄达到法制,最大值为15,对象就会被晋升成老年代
当老年代中空间不足时,无法放入新对象,先尝试Minor GC如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收
通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存利用率和性能
新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员选择灵活度较高
分代设计中允许回收新生代,如果能满足对象分配的要求就不需要对整个堆进行回收,STW时间就会减少
单线程串行回收年轻代的垃圾回收器
单线程串行回收老年代的垃圾回收器
使用多线程进行垃圾回收
关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少用户线程的等待时间
步骤:
1.初始标记,用极短的时间标记出GC Roots能直接关联到的对象
2.并发标记,标记所有对象,用户线程不需要停止
3.重新标记,由于并发标记阶段有些对象会发生变化,存在错标,漏标等情况,需要重新标记
4.并发清理,清理死亡对象,用户线程不需要暂停
缺点:
由于使用了标记清除算法,在垃圾收集结束之后会出现大量的内存碎片,会在FullGC进行碎片整理
无法一次性处理浮动垃圾
是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量,具备自动调整堆内存大小的特点
是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量,具备自动调整堆内存大小的特点
在JDK9之后默认的垃圾回收器就是G1垃圾回收器
整个堆会被划分成多个大小相等的区域,称之为区Region,区域要求是不连续的
年轻代回收:回收伊甸园区和Survivor区不用的对象,会导致STW
新创建的对象会存放在Eden区。当G1判断年轻代区不足时,无法分配对象时需要回收时会执行YoungGC
标记处Eden区和Survivor区中的存活对象
根据最大的暂停时间选择某些区域将存活对象赋值到一个新的Survivor区,清空这些区域
G1会在Young GC的过程中去记录每次垃圾回收时每个区和Survivor区的平均耗时,以作为下次回收时的参考依据
混合回收:
多次回收后,会出现很多个Old老年代区,此时总堆占有率达到阈值就会触发MixedGC,回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成