jvm的整体结构
class文件 —> 类加载子系统 ->jvm内存 -> 解释器,垃圾回收器,本地方法接口
类加载子系统
类加载子系统负责加载class文件。class 文件有特定的文件标识
类的加载过程
加载-> 验证 -> 准备 -> 解析 -> 初始化
( 链接 )
加载
- 通过一个类的全限定名获取二进制字节流
- 生成运行时的数据结构
- 在内存中生成class对象
验证 :检查class文件是否符合jvm规范
准备 :设置变量的初始值
解析 :将符号引用转换为直接引用。(符号引用是用一组符号表示引用的目标,直接引用是指向目标的指针)
初始化 :执行类构造器方法。(类构造器方法不同于构造器方法。它自动执行赋值动作和静态代码块的语句)
类加载器的分类
引导类加载器 : 加载java的核心库,加载包名为 java、javax、sun等开头的类
扩展类加载器 jre等包
自定义类加载器
为什么要自定义类加载器?
隔离加载类
防止源码泄露
双亲委派机制
jvm对class文件采取按需加载的方式,只有使用时才加载。在加载类时,将请求交给父加载器进行处理,如果父类加载器还存在父类加载器,向上递归,直到顶层。如果父类可以完成加载就成功返回,只有当父类加载器无法完成时,子加载器才会自己加载。
优势
避免类的重复加载
保护程序安全(其他人无法自定义Object类,这样可以保证对java核心源代码的保护,这就是沙箱安全机制)
在jvm中表示两个class对象是否为同一个类时存在两个条件:
- 类的完整类型必须一致
- 类加载器必须相同
运行时数据区结构
每个线程独有:程序计数器,本地方法栈,虚拟机栈
线程间共享:方法区,堆
程序计数器
pc寄存器用来存储指向下一条指令的地址。
虚拟机栈
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应一次次java方法的调用。
栈中可能出现的异常
java栈的大小可以是动态的或固定不变的。
如果采用固定大小的虚拟机栈,线程请求分配的栈容量超过虚拟机栈的最大容量,报stackOverflowError异常
如果虚拟机栈可以动态扩展,在申请内存时无法申请到足够的内存,报OutifMemoryError异常
例:无限递归
通过 -Xss 命令来设置线程最大栈空间
栈中存储什么?
栈中的数据以栈帧的形式存在,每个方法都对应一个栈帧。
栈的操作只有两个,入栈和出栈。在一个时间点上,只会存在一个活跃的栈帧,这个栈帧成为当前栈帧。 与之对应的为当前方法,当前类。
java中有两种返回函数的方式,return 或 抛异常。但不论哪种方式,都会弹出栈帧。
栈的内部结构
局部变量表 :负责存储局部变量,局部变量表中的变量只在当前方法调用中有效。当调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
局部变量表最基本的单位时slot(变量槽),32位的类型占用1个slot,64位的占用两个。jvm会为局部变量表中的每一个solt分配一个索引,通过索引即可访问到变量的值。
栈帧中的局部变量表中的槽位是可重用的。
操作数栈 :在方法执行的过程中,往栈中写入数据或提取数据,主要用于保存计算过程的中间结果。
动态链接 :每个栈帧内部都包含着指向运行时常量池中该栈帧所属方法的引用。动态链接的作用就是将这些符号引用转换为直接引用。
方法返回地址
虚方法与非虚方法
如果在编译器就确定了具体的调用版本,则成为非虚方法。常见的非虚方法有:静态方法、私有方法、final方法 、实例构造器、父类方法。
反之则成为虚方法。
为了提高性能,jvm在类的方法区建立了一个虚方法表指定执行的方法。
本地方法栈
本地方法接口
一个本地方法就是一个java代码调用非java代码的接口。
为什么要使用本地方法?
有时java应用需要与Java外面的环境交互,这是本地方法存在的原因。
堆
现代垃圾收集器大部分都基于分代收集理论设计
java7之前分为:新生代+老年代+永久代
java8之后分为:新生代+老年代+元空间
其中年轻代又可划分为 eden+s1+s0
堆空间大小的设置
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-Xmn:设置新生代的大小
-xms: 堆起始内存
-xmx :堆的最大内存 ,通常将xms和xmx设置相同的值,为了能够在gc之后不需要重新分隔计算堆区的大小,提高性能。
新生代与老年代的占比
默认 -XX:newRatio=2 ,新生代占1 ,老年代占2,新生代占整个堆 的1/3
Eden和另外两个 survivor 空间所占比例是 8:1:1
对象分配过程
1.new对象先放eden区,大小有限制
2.当eden区填满时,进行minor gc ,将eden区不使用的对象销毁,再加载新的对象。eden区,survivor放不下的大对象直接晋升至老年代。
3.将eden区的剩余对象放入s0区
4.再次触发垃圾回收,从s0区放到s1区
5.再次回收,从s1区到s0区
6.经历过15次之后,去养老区
7.在养老区满了触发major gc
8.在major gc之后无法进行对象的保存,产生OOM
minor gc 收集年轻代
major gc 收集老年代
full gc 收集整个堆和方法区
full gc 触发机制
- 调用 System.gc()
- 老年代空间不足
- 方法区空间不足
- 通过minor gc 后进入老年代的平均大小大于老年代的可用内存
- 由 eden ,s0 向 s1区复制时,对象大于to区可用的内存 ,会把对象转移到老年代,且老年代的可用内存小于该对象的大小。
为什么要分代?
不同对象生命周期不同,大部分对象是临时对象。
TLAB
jvm为每个线程分配了一个私有的缓存区域,包含在eden区内,提高分配内存的效率。
堆是分配对象存储的唯一选择吗?
是
开启了逃逸分析后,如果对象没有逃逸出方法的话,就可能被优化称栈上分配。 随着方法的结束,栈空间被移除,变量也消失。
但是无法保证逃逸分析的性能消耗小于它的优化。
方法区
方法区是一块独立于java堆的内存空间,是线程共享的,大小可固定可扩展。
如果系统定义了太多的类,导致方法区溢出,也会抛出异常。OutOfMemoryError:PermGen space 或者Metasoace。
在jdk7以前,方法区称为永久代。jdk8开始,元空间取代了永久代。
元空间与永久代最大的区别在于:元空间不设置于虚拟机设置的内存中,而是使用本地内存。
方法区主要存储什么?
存储已被虚拟机加载的类型信息,常量,静态变量等。
运行时常量池与常量池
方法区内部包含了运行时常量池
字节码文件内部包含了常量池
一个有效的字节码文件包括了魔数,版本信息,字段,方法,接口信息,还包括了常量池表(其中包括对类型,方法等的符号引用)。
为什么需要常量池?
一个java源文件编译后的字节码文件大小有限,不能存储所有的数据,这时可以存到常量池中,它包含了指向常量池的引用。
常量池,可以看作是一张表,用于存放编译器生成的各种符号引用。虚拟机指令根据这张表找到要执行的类名,方法名等。这部分将在类加载后存放到方法区的运行时常量池中。
方法区演进的细节
jdk1.6以前 有永久代,静态变量存放在永久代上
jdk1.7 有永久代,字符串常量池,静态变量移除,保存在堆中
jdk1.8 以后 无永久代。类型信息、字段、方法、常量保存在本地内存中,但字符串常量池,静态变量仍在堆。
为什么永久代要被元空间替换?
1.为永久代设置空间大小是很难的。如果在运行过程中需要动态加载的类过多很容易造成内存溢出。
2.对永久代调优是很难的。
字符串常量池为什么要调整?
因为永久代的回收效率很低,只有full gc时才触发。而开发时会有大量字符串被创建,回收效率低,导致永久代不足。
创建对象的步骤
- 判断对象对应的类是否加载、链接、初始化
- 为对象分配内存
- 处理并发安全问题
- 初始化分配的空间,为属性赋默认值
- 设置对象的对象头
- 执行init()方法初始化
执行引擎
将字节码指令解释为对应平台上的机器指令。