虚拟机
HotSpot 默认虚拟机
JRockit HotSpot融合了JRockit jdk8初步融合完成 没有解释器,只有编译器
IBM J9
结构图
类加载子系统Q
1.类加载器
启动类加载器(引导类加载器)Bootstrap ClassLoader
加载java 核心类库,没有父类加载器,只加载包名为java,javax,sun等开头的类
扩展类加载器 Extension ClassLoader
父类加载器为启动类加载器
应用程序加载器 AppClassloader
父类加载器为扩展类加载器,用户编写的类由它加载
自定义类加载器
想加载自己的class文件,需要自定义类加载器,也可以对class文件进行加密和解析,tomcat就有自己的自定 义类加载器
2.双亲委派机制
当类加载器加载一个class文件,类加载器不会立即加载,而是交给自己的父类加载,一直传递给启动类加载器,父类加载不了,再交给子类加载器加载。
作用:避免核心API被修改
3.沙箱安全机制
自定义java.lang.String类(带main方法),加载自定义String类的时候由于双亲委派机制,会由启动类加载器加载,加载系统自带的String类,就会报错没有main方法,这就是沙箱安全机制
4.类加载过程
一、加载阶段
由引导类加载器,扩展类加载器,系统类加载器加载.class文件
二、链接阶段
链接阶段分为验证,准备,解析
验证:是为了保证字节码文件的安全性
准备:为类变量分配内存空间并初始化零值
解析:将常量池的符号引用转换为直接引用(真实的地址)
三、初始化阶段
初始化主要是初始化类变量,执行方法,这个方法会自动按顺序收集所有类变量的赋值动作以及 静态代码快的语句,如果有父类,会先加载父类
运行时数据区
1.PC寄存器
PC寄存器线程私有,储存下一条指令的地址,由执行引擎读取。分支,循环,跳转,线程切换,异常都依赖寄存器完成。没有OutofMemoryError,内存占用忽略不计,是运行最快的区域。
2.虚拟机栈
线程私有,由一个个栈帧组成。主管Java程序的运行,栈帧由局部变量表,操作数栈,动态链接,方法返回地址组成。如果固定大小,会抛出StackOverFlow 动态扩展会抛出OutofMemoryError
栈帧的运行原理
每个栈帧对应一个方法,调用就入栈,方法结束就出栈,先进后出,后进先出。运行时,只有顶层 的栈帧是有效的,执行引擎只操作这个栈帧。当前栈帧调用新的方法,新的栈帧会被创建并压入栈顶成为有效栈帧,此栈帧调用结束后会出栈,前一个栈帧重新成为有效栈帧。方法的返回要么使用return指令返回,要么抛出异常,这两种情况该栈帧都会出栈。
局部变量表
为一个数组,存储方法参数和局部变量,包括基本数据类型,对象引用,方法返回值。局部变量表编译器就被确定下来的。局部变量表只在当前方法中有效,栈帧出栈后就会被销毁。
- Slot槽
Slot为局部变量表中的基本储存单元,每个Slot就是数组中的一个位置。32为的变量占用1个slot,包括对象引用,方法返回值,64位的占用2个slot,访问的时候通过第一个slot访问。构造方法,局部变量有序复制在slot上,如果当前方法为实例方法,下标为0的slot存放this。局部变量表是可达性分析算法GCRoot重要的根节点。
操作数栈
操作数栈主要用于保存计算过程的中间结果,为计算过程中变量的临时储存空间。当一个栈帧被创建的时候,一个空的操作数栈也会一同被创建出来,栈的最大深度在编译期就已经确定下来了。操作数栈也是通过入栈和出栈完成数据的访问。
动态链接
每个栈帧内部都包含一个执行运行时常量池中该栈帧所属的方法引用,持有这个引用是为了支持方法调用过程中的动态链接。class文件中的常量池符号引用,在链接阶段的解析过程中会把符号引用转换为直接引用,存到运行时数据区中。
方法返回地址
方法返回有两种情况,一种通过return指令返回,另一种发生异常退出。前者方法返回地址会放入里,PC寄存器的值就是方法的返回地址,后者,需要通过异常表确定方法返回的位置。
方法退出首先恢复上层方法的局部变量表和操作数栈,如果有返回值,把返回值压入操作数栈,执行引擎执行当前指令,调整PC寄存器指向下一条指令。
3.本地方法栈
同虚拟机栈,执行本地方法(native)
4.堆
堆为虚拟机内存管理最大的一块区域,此区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,这里是垃圾收集器管理的主要区域。
堆的内存划分
年轻代和老年代默认比例1:2
Eden和S0,S1比例 8:1:1
对象分配的过程
1.new的新对象放在Eden区
2.Eden区满时触发Young GC,将Eden区的对象进行垃圾回收,销毁没有引用的对象,把没有回收的对象年龄加1放在To区(s1)
3.如果再次发生垃圾回收,把From区(s1)的对象和Eden区的对象进行垃圾回收,把没有回收的对象年龄加1放在To区(s2),谁空谁是To
4.在Young GC后如果对象的年龄等于15时,进入老年代
5.如果老年代的空间不足就会触发Full GC
Young GC、Major GC 和 Full GC
Young GC Eden和From区的垃圾回收,回收比较频繁,会发生STW
Major GC 为老年代的垃圾回收,只有CMS单独回收老年代
Full GC 老年代和方法区混合回收
Java堆分代的原因
Java对象有的存活时间短,有的存活时间长,不分代每次回收都扫描整堆,存活时间长的对象是没必要每次都扫描的。把朝生夕死的对象归为年轻代,存活时间长,大的对象归为老年代,这样就能提高垃圾回收的效率。 垃圾回收算法有标记清除,复制算法,标记整理,分代后对不同的区域使用不同的算法,发挥各自的优势。
逃逸分析,栈上分配
当一个对象只在方法内部使用,则认为没有发生逃逸分析,在栈上分配一小块空间,可以把该对象分配到栈上,随着栈帧的弹出而一同销毁。
堆空间常用参数
-Xms 初始堆空间内存(默认为物理内存的1/64)
-Xmx 最大堆空间内存(默认为物理内存的1/4)
-Xmn 设置新生代的大小(初始大小,也是最大大小)
-XX:NewRatio:配置新生代和老年代的结构占比
-XX:SurvivorRatio 设置新生代中Eden和S区的空间占比
-XX:MaxTenuringThreshold 设置新生代S区对象最大年龄
-XX:PrintGCDetails 输出详细的GC处理日志
5.方法区
方法区是Java虚拟机的规范,HotSpot的具体实现在jdk8之前叫永久代,jdk8及之后叫元空间。 永久代 在JVM中,元空间使用的是堆外内存,不在占用虚拟机的内存。方法区和堆一样是线程共享的区域。方法区存放类型信息,运行时常量池,静态变量,JIT代码缓存、域信息、方法信息。方法区的大小决定可以保存多少类,保存太多类型一样报OutofMemoryError.
方法区大小设置
jdk7以前:
-XX:PermSize=size 初始大小 默认20.75M
-XX:MaxPermSize 最大可分配空间 默认64位机器82M
jdk8及之后:
-XX:MetaspaceSize 默认21M
-XX:MaxMetaspaceSize 默认-1.没有限制
演进细节
字符串常量此为什么要移到堆区
永久代的回收效率比较低,字符串的使用率比较高,生命周期短,回收比较频繁,但字符串在永久代中不能及时回收,永久代被字符串占满,就会触发Full GC,影响垃圾回收效率。移到堆中就能够及时回收。
方法区的垃圾收集
常量池中废弃的常量(易回收)和不再使用的类型(难回收)
执行引擎
是java虚拟机的核心组成部分。执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令。
解释器
解释器会逐条解释PC寄存器的指令,将字节码指令翻译为对应平台的机器指令。
编译器
虚拟机将一段热点代码直接编译为本地机器平台相关的机器语言。
解释器和编译器的比较
由于解释器是逐条执行,执行效率低下,但响应速度快。编译器是将一段字节码指令直接编译位对应平台的机器码,编译需要时间,响应速度慢,执行速度快。解释器就是步行,想走就走,编译器的公交车,要等车来,坐上车速度起飞。
HotSpot虚拟机采用解释器和编译器并存,就是为了同时兼顾编译代码的时间和代码运行的时间。
热点代码及探测方式
JIT及时编译器不是所有的代码都会编译为机器码,是有选择的编译热点代码。一个调用多次的方法或方法体内部循环较多的循环体称为热点代码。JVM设定了一个阈值,达到这个阈值JIT编译器就会把这段代码编译为机器码存放在元空间中。用于统计调用次数的算法有两种:方法调用计数器和回边计数器。方法调用计数器用于统计方法的调用次数。回边计数器则用于统计循环体执行的循环次数。
字符串常量池
String基本特性
声明:String s1 = “hello” Stirng s2 = new String(“hello”)
String 声明为final,不可被继承,值不可变
String实现了Serializable接口,表示字符串支持序列化
String实现了Comparable接口,表示String可以比较大小
String jdk8以前底层是char[] jdk9是byte[] 节省储存空间
String的字符串常量池
String pool 是一个HashTable,默认长度为1009,如果放入的String非常多,会造成hash冲突严重,会造成链表非常长(jdk8会转红黑树)调用String.intern()方法时的效率低下
在jdk7中,StringTable的长度默认值是60013
jdk8开始,1009是StringTable长度可设置的最小值
StringTable的存放位置
jdk6及之前,字符串常量池存放在永久代
jdk7后,字符串常量池移到了堆里
StringTable调整的原因
永久代的空间比较小,字符串在程序中比较常见,永久代垃圾回收频率低,移到堆中提高回收频率
字符串拼接
常量与常量拼接结果在常量池,编译器优化
拼接的字符串中有一个是变量,拼接的原理就是StringBuilder
StringBuilder的toString()方法
在new String(“aa”) 时创建了两个对象
StringBuilder的toString()方法创建了一个对象,没有加入字符串常量池
String的intern()
jdk1.6
如果字符串常量池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
jdk1.7
如果字符串常量池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
垃圾回收
标记阶段
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
引用计数算法
对每个对象保存一个整型 的引用计数器属性。用于记录对象被引用的次数。无法处理循环引用的问题。
可达性分析算法
以根对象集合为起始点,递归搜索根集合所连接的对象是否可达。如果对象不可达该对象就是要回收的对象。
GC Root
虚拟机栈中引用的对象 、本地方法栈引用的对象、类变量引用的对象、 方法区中常量引用的对象 、所有被同步锁synchronized持有的对象、虚拟机内部的引用
清除阶段
标记-清除算法
有效内存被耗尽时,停止整个程序(STW)。先进行标记,从根节点递归遍历标记被引用的对象。然后线性遍历,清除没有标记的对象。
效率不高,清理后内存空间不连续,易产生内存碎片。
复制算法
将内存空间分为两块,每次只用其中的一块。标记阶段标记被引用的对象。然后把有引用的对象复制到另一块中。
这种方法不会产生内存碎片,但要用两倍的空间。由于对象的地址改变了,需要维护引用关系。而且存活对象特别多,复制起来效率会很低。
标记-压缩(整理,Mark-Compact)算法
先进行标记,从根节点递归遍历标记被引用的对象。然后所有存活的放在内存的一端,按顺序排放。之后清理边界外的所有空间。解决了标记清除算法的空间碎片问题。
垃圾回收器
评估GC的性能指标
- 吞吐量 : 运行用户代码的时间占总运行时间的比例
垃圾收集开销 :垃圾收集时间占总运行时间的比例
- 暂停时间:执行垃圾收集时,程序工作线程被暂停的时间
收集频率: 相对于程序的执行,收集操作发生的频率
- 内存占用:java堆所占内存的大小
三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
7款经典的垃圾收集器与垃圾分代之间的关系
新生代收集器: Serial、 ParNeW、Parallel Scavenge;
老年代收集器: Serial 0ld、 CMS、Parallel 0ld;
整堆收集器: G1
Serial回收器:串行回收
ParNew回收器:并行回收
CMS回收器:低延迟
G1回收器:区域化分代式