JVM内存结构
JVM的内存结构大概分为:
1.堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
2.方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。jdk1.7之前对方法区的实现为永久代(PermGen,JVM内存),jdk1.8之后对方法区的实现改为元空间(MetaSpace,直接内存),并且将常量池和字符串池放入堆中,但是运行时的常量池在元空间中。
3.虚拟机栈(JVM Stack):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。
4.本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
5.程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。
1.垃圾回收
- 垃圾回收的目标位置是堆
1.1 判断垃圾
- 引用计数算法
有一个引用计数器,记录了每个对象被引用的次数,为0则被回收,但是若两个对象相互引用,那么这两个对象就算不用了,也不会被回收。
- 可达性分析法
判断对象是否可以通过gcroot强引用,是否被gcroot关联,若没有关联或者非强软引用,那么会直接被判断为垃圾。
- 四种引用
- 强引用
GCroot引用的对象成为强引用对象,不管什么情况,只要引用没有取消,都不会被回收。
- 软引用
当其他的gcroot强引用取消之后,只剩一个软引用时,如果发生垃圾回收,并且内存满了,那么就会将软引用对象回收掉
- 弱引用
当其他的强引用取消了之后,只剩一个弱引用,那么发生垃圾回收时,不管内存是否充足,都会回收掉弱引用对象
- 虚引用
虚引用相当于无引用,使对象无法被引用,需要配合引用队列使用,将虚引用的对象创建并且加入到引用队列等待下次jvm垃圾回收
- 终结器引用
用于实现对象的finalize方法,不推荐使用,因为finallize方法优先级很低,可能会导致对象被加入引用队列但是内存一直得不到释放,不推荐使用。
1.3.垃圾回收算法
1.3.1 标记清除算法
将没有被gcroot关联的对象标记起来,然后统一进行清除。
但是这个算法有一个缺点,若有一个对象要进来,但是对象比两个存活对象之间的剩余空间大,那么就会往后寻找能容纳的碎片空间,长久这样,会存在很多很小的内存碎片,就会导致空间“碎片化”,缩小存储空间。
1.3.2 标记整理算法
此算法首先标记,然后将可回收对象清除之后,对碎片空间进行整理,将存货对象向前移动,这样就不存在碎片空间了,不会造成内存空间碎片化。
但是这个算法的效率会低很多,因为整理阶段牵扯到内存区块的移动,垃圾多且分散的时候速度就会变得很慢。
1.3.3 标记复制算法
这个算法将内存分为两个区域,一个是From区域,一个是To区域,但是to区域必须保持为空。
第一步也是将垃圾对象标记,然后将存活对象复制到to区域,对from区域进行清除,然后将to作为下一个from区域,将from作为下一个to区域,这样使得to区域一直为空。这个方法是以空间换取时间,也不会产生碎片化空间,但是可用的内存空间就减半了,在内存比较小的情况下效率很低。
1.3.4 总结
标记清除算法:速度快,但是会造成内存碎片
标记整理:不会造成内存碎片,但是效率较低
标记复制:不会造成内存碎片,速度比较快,但是可用空间减半
1.4 分代回收
上面讲完了垃圾回收的算法,那么这些在jvm是怎么实现的呢,分代回收就是目前主流的HotSpot的jvm对垃圾回收的实现。
他将内存区域分为两部分:
- 新生代:伊甸园Eden,幸存区From,幸存区To
- 老年代OldGen
朝生夕死的放在新生代,长久存在的放在老年代。并且针对不同区域采用不同算法。
当一个新对象诞生时,首先放在伊甸园Eden区域,
当伊甸园满时,触发一次垃圾回收MinorGCroot,对eden中的垃圾进行标记,然后采用标记复制算法,将eden中的存活对象复制进to区域,并且寿命+1,
然后清除eden区域,交换from和to区域。
当第二次eden满时,触发第二次MinorGC,要在eden和幸存区From中寻找垃圾进行标记,然后将存活对象复制到To区域,并且寿命+1.
当幸存区From中的对象达到一定的寿命时(默认是15),将会进入老年代。
当有一个新对象进来,新生代的空间不足,并且MinorGC之后空间也不足时,会将From区中寿命最长的对象提前放入老年代。
当老年代的空间也不足时,将会触发FullGC,进行全面的垃圾回收,在调优时应尽量避免FullGC,而是将垃圾在MinorGC的时候就回收掉。
若FullGCroot之后空间任然不足,那么会抛出内存空间溢出异常java.lang.OutOfMemoryError: Java heap space
- MinorGC会引发Stop the world,会暂停其他线程的工作,因为在进行复制的时候涉及指针转移等操作,必须暂停其他各线程工作,垃圾回收结束,用户线程才能恢复运行。
- 对象的最大寿命是15(4bit),超过阈值会晋升至老年代。
- 当老年代空间不足触发FullGC也会stop the world,并且暂停时间更长。
1.5 垃圾回收器
1.5.1 串行
- 单线程
- 堆内存较小,cpu线程比较少
- 使用的是标记整理方法
- -XX:+UseSerialGC=Serial+SerialOld
1.5.2 吞吐量优先
- 多线程
- 堆内存较大,需要多核cpu支持
- 让单位时间内,STW暂停时间最短
- 使用的是标记整理算法
- -XX:+UseParallelGC~-XX:+UseParallelOldGC 开启其中一个即可全部开启
- -XX:ParallelGCThreads=n 指定垃圾回收线程数
- -XX:UseAdaptiveSizePolicy 动态调整eden和幸存者区的比例
- -XX:GCTimeRatio=ratio 指定垃圾回收占用时间比例,1/(1+ratio)默认为99,也就是1%
- -XX:MaxGCPauseMillis=ms 指定STW最大暂停毫秒数,默认200ms
1.5.3 响应时间优先
- 多线程
- 堆内存较大,多核cpu
- 实现标记清除算法
- 尽可能让单次STW暂停时间最短
- -XX:+UseConcMarkSweepGC~-XX:+UseParNewGC~SerialOld 并发标记整理(CMS),不同于并行:并发是一个处理器处理多个任务,并行是多个处理器处理多个任务;并行在同一时间可以同时执行多个任务,并发在同一时间只能执行一个任务
- -XX:ConcGCTreads=threads threads建议设置为线程数的1/4
- -XX:CMSInitiatingOccupancyFraction=percent ,堆内存占用达到percent时,进行CMS垃圾回收
- -XX:+CMSScanvengeBeforeRemark 重新标记之前进行一次新生代的垃圾回收+表示打开
过程如下:
- 初始标记只需要标记表层的垃圾,不需要管他们的引用与否,所以时间很短,STW的时间也很短;
- 并发标记阶段需要从GCRoot向下溯源,标记深层的垃圾,耗时长;
- 然后是并发预处理阶段,这个阶段的目的是减少下一个阶段重新标记的时间,由于并发标记阶段用户线程没有挂起,所以标记完之后可能有新生代晋升到了老年代,或者有新生代引用了老年代等情况,这个阶段就是对这些dirty对象进行处理,并且还可能会进行minorGC,减小重新标记阶段遍历新生代的时间;
- 重新标记阶段对预处理之后的垃圾进行标记,该阶段会STW,时间长短取决于并发预处理阶段;
- 最后的并发清理阶段对上面几个阶段标记的垃圾进行清理,会产生碎片空间,并且因为是并发清理,所以在清理过程中会产生新的垃圾,被称为“浮动垃圾”,只有等下一次GC去清理。全部结束后会重置算法内部的相关数据,为下一次GC做准备。
缺点:1.因为是标记清理算法,所以会产生很多碎片化空间,如果碎片过多,又可能因为空间不足触发FullGC,这样会导致STW耗时进一步加长;
2.CMS需要预留空间,保证在CMS回收时用户有足够的空间使用,如果CMS预留空间不够,CMS会引发SerialOld回收老年代垃圾,会导致停顿时间很长。
优点:只有两个STW,并且暂停时间很短,其他标记清理都是并发进行的,不影响程序运行,注重响应速度
1.5.4 G1(Garbage First)
jdk9默认垃圾回收器,对cms
- 适用场景:
-
- 同时注重吞吐量和低延迟,默认的暂停目标是200ms
- 超大堆内存,会将堆划分 为多个大小相等的Region
- 整体是标记整理算法,区域之间是复制算法
- 相关JVM参数
-
- -XX:+UseG1GC 使用G1垃圾收集器
- -XX:G1HeapRegionSize=size 指定区域(Region)的大小
- -XX:MaxGCPauseMillis=time 最大暂停时间
G1的分区思想
- 每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor和老年代Old。存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,可能会暂停所有应用线程。如上图所示,区域可以分配到Eden,survivor和老年代。此外,还有第四种类型,被称为巨型区域(Humongous Region)。Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1的三种回收阶段(Oracle工程师提出)
- Young Collection,YoungCollection,MixedCollection三个阶段循环
- YoungCollection
- 与分代回收的MinorGC相同,会STW
- 当Eden区域满时触发YoungCollection,以拷贝的算法将对象放入Survivor区
- 将S区年龄到阈值的对象存入Old区
- YoungCollection+ConcurrentMarking
- 在YoungGC时会进行GCRoot的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由JVM参数-XX:InitiatingHeapOccupancyPercent=percent(默认45%)
- MixedCollection
- 会对E、S、O区域进行全面垃圾回收
- 首先用YoungCollection将E区的存活对象拷贝到S区中,将S区中没达到年龄阈值的整理到一个或多个S区消除碎片空间,S区达到年龄阈值的复制到O区
- 然后G1会根据用户设置的MaxGCPauseMillis,G1会在所有O区中挑出那些回收价值最高的区域,即耗时短,回收垃圾多的区域,将存活对象复制进一个O区
- 最后将没复制的回收过得区域进行清理
回收的四个阶段
1)G1执行的第一阶段:初始标记(Initial Marking )这个阶段是STW(Stop the World )的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象。
2)G1执行的第二阶段:并发标记从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。当并发标记完成后,开始最终标记(Final Marking )阶段
3)最终标记(标记那些在并发标记阶段发生变化的对象,将被回收)
4)筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)
缺点:与CMS一样需要预留空间,若空间不足或者说垃圾产生的速度比垃圾回收的速度要快,那么也会使用SerialGC回收老年代垃圾,会触发FullGC
- Young Collection跨代引用
- 新生代回收的跨代引用问题(老年代对新生代的引用)
- 在回收新生代对象时,为了确认新生代对象是否存活,除了遍历GCRoots之外,往往需要遍历整个老年代查看是否有引用,而被引用的新生代对象只有极少数,为了极少数对象遍历整个老年代,造成了很大的性能浪费
- Remembered Set记忆集,记录哪些新生代被哪些O区对象引用
- HotSpot使用CardTable卡表实现记忆集,若O区域某个GCRoot对象引用了新生代对象,那么会在CardTable中标记为脏卡,这个新生代对象就变成了脏对象
- 这样在新生代垃圾回收时就不必去遍历老年代,而是遍历GCROOTs和卡表,就可以判断出存活对象,节省了性能和时间
- JDK8u20字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了cpu时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
String s1 = new String("hello");//char[]{'h','e,'l,'l,'o} String s2 = new String("hello");//char[]{'h','e,'l,'l,'o}指向同一个数组
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果他们值一样,让它们引用同一个数组
- 注意,与String.intern()不一样
-
- String.intern()关注的是字符串池中的字符串对象
- 而字符串去重关注的是队列中的char[]
- 在JVM内部,使用了不同的字符串表
- JDK8u40并发标记类卸载
所有对象都经过并发标记后,就知道哪些类不再使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类(无用类的卸载,可以理解为方法区的垃圾清理)
-XX:+ClassUnloadingWithConcurrentMark默认启用
- JDK8u60回收巨型对象
- 一个对象大于Region的一半,称之为巨型对象,放入H区域
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- 如果卡表中不存在对巨型对象的引用,那么会在新生代GC被回收
- JDK9并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为FullGC
- JDK9之前需要使用-XX:InitiatingHeapOccupancyPercent
- JKD9可以动态调整
-
- -XX:InitiatingHeapOccupancyPercent用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
1.6 三色标记算法
简介:
三色标记算法是一种标记算法,标记阶段在垃圾回收中是很重要的,很多垃圾回收算法有标记阶段,如标记清除,标记整理,复制算法,标记一般是通过三色标记算法实现的。
三色标记法中用三个颜色块标记对象:
- 白色:尚未被GC访问过的对象,如果全部标记已完成依旧为白色的,称为不可达对象,既垃圾对象。
- 黑色:本对象已经被GC访问过,且本对象的子引用对象也已经被访问过了(本对象的孩子节点也都被访问过)。
- 灰色:本对象已访问过,但是本对象的子引用对象还没有被访问过,全部访问完会变成黑色,属于中间态(本对象的孩子节点还没有访问)。
基本流程:
1.初始时,所有对象都在 【白色集合】中;
2.将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
3.从灰色集合中获取对象:
3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2. 将本对象 挪到 【黑色集合】里面。
重复步骤3,直至【灰色集合】为空时结束。结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收
存在的问题:
- 产生浮动垃圾:由于标记的时候用户线程也在工作,当一个被访问过的黑对象断开对白色对象的引用时,白色对象就变成垃圾了,但是因为黑色对象被访问过了不会再被访问,所以这一部分白色垃圾就变成了浮动垃圾,不会被本次GC回收,须等待下次GC回收
- 漏标:当一个灰色对象断开对白色对象的引用,并且此时黑色对象引用了该对象,该对象就不会被标记为灰色,一直是白色,最终被当成垃圾回收,影响了程序的正确性
解决方案:
写屏障 + 增量更新,当对象D的成员变量的引用发生变化时,我们可以利用写屏障,当D是黑色G是白色的话将D标为灰色,等待遍历,即增量更新(Incremental Update)。
CMS(ConcurrentMarkSweep)垃圾收集器就实现了这一种解决方案,它的重新标记阶段就是一个增量更新的过程,增量标记那些在并发标记中产生的浮动垃圾,减少浮动垃圾的存活率。
2. 类加载与字节码
2.1 类文件结构
2.1.1 magic魔数
0-3字节,表示它是否是class类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
JAVA的魔数:ca fe ba be
2.1.2 版本
4-7字节,表示类的版本,0034H(52)表示的是java8
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
2.1.3 常量池
8-9字节.表示常量池长度,0023表示35,表示常量池由#1-#34,0不计入,也没有值
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#1项0a表示一个Method信息,0006和0015表示它引用了常量池中#6和#21项来获得这个方法的所属类和方法名
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#2项09表示一个Field,后面的四个字节表示了它的所属类和成员变量名
总之就是说开头一个字节表示他是什么,通过查表可以知道是method信息还是变量还是类型等等
2.2 字节码指令
2.2.1 入门
- public HelloWorld();构造方法的字节码指令
2a b7 00 01 b1
- 2a=>aload_0加载slot0的局部变量,即this,作为下面invokespecial构造方法调用的参数
- b7=>invokespecial 预备调用构造方法,哪个方法呢?
- 00 01引用常量池中#1项,即Object的<init>Method,即构造方法
- b1表示返回
- public static void main主方法的字节码指令
b2 00 02 12 03 b6 00 04 b1
- b2->getstatic 获取静态变量 哪个静态变量呢?
- 00 02引用常量池中的#2项,即System.out
- 12->ldc 加载参数,哪个参数呢?
- 03引用常量池#3项,即String hello world
- b6->invokevirtual预备调用成员方法,哪个方法呢
- 00 04引用常量池中#4项,即println方法
- b1表示返回
2.2.2 javap工具
D:\IdeaProjects\project1\test1\src\main\java>javap -v HelloWorld1.class Classfile /D:/IdeaProjects/project1/test1/src/main/java/HelloWorld1.class Last modified 2022-7-4; size 428 bytes MD5 checksum 61c7e3d88ff7f34652cc5bada469a776 Compiled from "HelloWorld1.java" public class HelloWorld1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // Hello World! #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // HelloWorld1 #6 = Class #22 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 HelloWorld1.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 Hello World! #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 HelloWorld1 #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V { public HelloWorld1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 } SourceFile: "HelloWorld1.java"
2.2.3 图解方法执行流程
- 原始java代码
package cn.itcast.jvm.t3.bytecode; /** * 演示 字节码指令 和 操作数栈、常量池的关系 */ public class Demo3_1 { public static void main(String[] args) { int a = 10; int b = Short.MAX_VALUE + 1; int c = a + b; System.out.println(c); } }
- 编译后的字节码文件
D:\IdeaProjects\project1\test1\src\main\java>javap -v Demo3_1.class Classfile /D:/IdeaProjects/project1/test1/src/main/java/Demo3_1.class Last modified 2022-7-4; size 432 bytes MD5 checksum 82b4efda4ff6e8382a2897a4da98e36c Compiled from "Demo3_1.java" public class Demo3_1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #7.#16 // java/lang/Object."<init>":()V #2 = Class #17 // java/lang/Short #3 = Integer 32768 #4 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; #5 = Methodref #20.#21 // java/io/PrintStream.println:(I)V #6 = Class #22 // Demo3_1 #7 = Class #23 // java/lang/Object #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 main #13 = Utf8 ([Ljava/lang/String;)V #14 = Utf8 SourceFile #15 = Utf8 Demo3_1.java #16 = NameAndType #8:#9 // "<init>":()V #17 = Utf8 java/lang/Short #18 = Class #24 // java/lang/System #19 = NameAndType #25:#26 // out:Ljava/io/PrintStream; #20 = Class #27 // java/io/PrintStream #21 = NameAndType #28:#29 // println:(I)V #22 = Utf8 Demo3_1 #23 = Utf8 java/lang/Object #24 = Utf8 java/lang/System #25 = Utf8 out #26 = Utf8 Ljava/io/PrintStream; #27 = Utf8 java/io/PrintStream #28 = Utf8 println #29 = Utf8 (I)V { public Demo3_1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: bipush 10 2: istore_1 3: ldc #3 // int 32768 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 13: iload_3 14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 17: return LineNumberTable: line 3: 0 line 4: 3 line 5: 6 line 6: 10 line 7: 17 } SourceFile: "Demo3_1.java"
- 常量池载入运行时常量池
- 方法字节码载入方法区
- main线程开始运行,分配栈帧内存
stack=2(main和println方法)locals=4()
- 执行引擎开始执行字节码
bipush 10
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有 sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
2.2.4 i++和++i的区别
i++就是先iload紧接着iinc,++i是先iinc再iload,并且iinc运算不需要读取操作数而是直接在变量上完成的
2.3 多态原理
当执行invokevirtual指令时
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象 的实际class
- Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
- 执行方法的字节码
2.4 语法糖
语法糖,就是指java编译器把*.java编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java给程序员的一个额外福利(给糖吃)
2.4.1 自动构造器
public class Candy1{} //编译成class后的代码为: public class Candy1{ //这个无参构造是编译器为我们加上去的 public Candy1(){ super();//即调用父类Object的无参构造方法 } }
2.4.2 自动拆装箱
代码片段 public class Candy2{ public static void main(){ Integer x = 1; int y = x; } } 这段代码在jdk5之前是无法编译通过的,在jdk5之后添加了这个语法糖才可以编译通过 编译后的代码为: public class Candy2{ public static void main(){ Integer x = Integer.valueOf(1); int y = x.intValue(); } }
2.4.3 泛型集合取值
List<Integer> list = new ArrayList<>(); list.add(1);//其实调用的是List.add(Object e) Integer x = list.get(0);//实际是Object o = List.get(0) //所以在编译的时候,会额外将取值进行转换 Integer x = (Integer)list.get(0); //如果将Integer改为int,那么还要进行拆箱操作,所以最后的代码为: int x = ((Integer)list.get(0)).intValue(); 还好这些麻烦事不用自己做
2.4.4 可变参数
String... args会被转换为String[] args public static void foo(String... args){ System.out.println(args); } public static void main(String[] args){ foo("hello","sugar"); } //编译时实际会被转为 public static void foo(String[] args){ System.out.println(args); } public static void main(String[] args){ foo(new String[]{"hello","sugar"}); //new String[]{}会返回一个空数组,而不会赋值为null }
2.4.5 foreach循环
int[] is={1,2,3,4,5}; for(int i : is){ System.out.println(i); } //会被转变为 int[] is=new int[]{1,2,3,4,5} for(int i=0;i<is.length;i++){ int e = is[i]; System.out.println(e); } //而list集合的foreach是使用iterator实现的 Iterator itr = List.iterator(); while(itr.hesNext()){ Integer e = (Integer)itr.next(); System.out.println(e);
2.4.6 switch字符串
从jdk7开始,switch可作用于字符串和枚举类,但是变量不能为null,这个功能也是语法糖,例如:
switch(str){ case "hello":{ System.out.println("h"); break; } case "world":{ System.out.println("w"); break; } }
public class Candy6_1 { public candy6_1(){} public static void choose(String str) { byte x -1; switch(str.hashCode()) { case 99162322: / / hello 的hashCode if (str.equals("hello")){ //hashcode可能发生碰撞,相同,所以需要进行equals判断 x= 0; } break; case 113318802: i l world 的hashCode if (str.equals( "world") ){ x = 1; } } switch(x){ case 0: System.out.println("h"); break; case 1: System.out.println("w"); } } }
2.4.7 switch枚举类型
enum Sex{ MALE,FEMALE } public class Candy7{ public static void foo(Sex sex){ switch(sex){ case MALE: System.out.println("男");break; case FEMALE: System.out.println("女");break; } } } //转换后代码为: public class Candy7{ /** *定义一个合成类仅jvm使用,对我们不可见,用来映射枚举的ordinal与数组元素的关系 枚举的ordinal表示枚举对象的序号,从0开始 即MALE的ordinal()=0,FEMALE的ordinal=1 */ static class $MAP{ //数组大小即为枚举元素个数,里面存储case用来对比的数字 static int[] map = new int[2]; static{ map[Sex.MALE.ordinal()]]=1; map[Sex.FEMALE.ordinal()]]=2; } } public static void foo(Sex sex){ int x=$MAP.map[sex.ordinal()]; switch(x){ case 1: System.out.println("男");break; case 2: System.out.println("女");break; } } }
2.4.8 枚举类
jdk7新增了枚举类,以前面的性别枚举为例:
enum Sex(){ MALE,FEMALE } //转换后的代码 public final class Sex extends Enum<Sex>{ public static final Sex MALE; public static final Sex FEMALE; public static final Sex[] $VALUES; static{ MALE = new Sex("MALE",0); FEMALE = new Sex("FEMALE",1); $VALUES = new Sex[]{MALE,FEMALE}; } private Sex(String name,int ordinal){ super(name,ordinal); } public static Sex[] values(){ return $VALUES.clone(); } public static Sex valueOf(String name){ return Enum.valueOf(Sex.class,name); } }
2.4.9 带参数的try-catch
jdk7开始新增了对需要关闭的资源处理的特殊语法try(){}catch{}
其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了AutoCloseable,使用try(){}语法可以不用写finally语句块,编译器会帮助生成关闭资源代码,例如:
public class Candy9{ public static void main(String[] args){ try(InputStream is){ System.out.println(is); }catch(IOException e){ e.printStackTrace(); } } }
2.5 类加载阶段
2.5.1 加载
- 将类的字节码载入方法区中,内部采用c++的instanceKlass描述java类,它的重要field有:
-
- _java_mirror即java类的镜像,例如对String来说,就是String.class,String.class和instanceKlass互相拥有对方的内存指针,相当于是类的入口,将instanceKlass暴露给java。
- _super即父类
- _fields即成员变量
- _methods即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
注意
- instanceKlass这样的[元数据]是存储在方法区(1.8后的元空间)内的,但_java_mirror是存储在堆中
- 可以通过HSDB工具查看
2.5.2 链接
- 验证:验证类是否符合JVM规范,安全性检查,验证class文件的魔数格式是否正确,是否是cafebabe
- 准备:为static变量分配空间,设置默认值
-
- static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在之后的初始化阶段完成
- 如果static变量是final的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
- 解析:将常量池中的符号引用解析为直接引用,这样就知道该类在内存中的位置了loadClass方法不会导致类的解析和初始化
2.5.3 初始化
<cinit>()v方法
初始化即调用<cinit>()v,虚拟机会保证这个类的【构造方法】的线程安全
发生的时机
概括得说,类初始化是【懒惰的】
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName()
- new 会导致初始化
不会导致初始化的情况
- 访问类的static final 静态常量(基本类型和字符串String)不会触发初始化
- 类对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
- Class.forName的参数2位false时
初始化的顺序:
- 静态属性,静态方法块,
- 普通属性,普通方法快,构造函数,普通方法
存在继承关系的类:
- 父类的(静态属性,静态方法块),
- 子类的(静态属性,静态方法块),
- 父类的(普通属性,普通方法快,构造函数),
- 子类的(普通属性,普通方法快,构造函数)。
2.6 类加载器
2.6.1 类加载器种类
类加载器的种类有:
- BootstrapClassLoader,加载位于jre/lib/路径下的jar包,这个类加载器是C语言实现的,在java中打印不出来
- ExtensionClassLoader,加载位于jre/lib/ext/路径下的jar包,父类为BootstrapClassLoader,显示为null
- AppClassLoader,加载位于classpath:目录下的jar包,父类为ExtensionClassLoader
- CustomClassLoader,通过ClassLoader的子类自定义加载Class,父类为ApplicationClassLoader
加载顺序,自顶向下加载,自底向上检查是否加载成功。
2.6.2 双亲委派模式
双亲委派就是指调用类加载器的loadClass方法时,查找类的规则
加载一个类时若有父类,自下而上验证,首先会用ApplicationClassLoader验证是否加载过父类,加载过不再加载,若没有加载过则到父类加载器去进行同样的操作,直到BootStrap类加载器,若BootStrap类加载器也没有加载过,那么从上而下检查是否可以加载,无法加载则让Extension类加载器检查是否可以加载,以此类推直到Application类加载器
总结:自下而上检查是否加载过,加载过的类不再加载;自上而下检查能否加载,能加载则由本身加载类。
2.6.3 线程上下文类加载器
线程上下文类加载器的一般使用模式:获取-使用-还原
ClassLoader calssLoader = Thread.currentThread().getContextClassLoader(); try { //设置线程上下文类加载器为自定义的加载器 Thread.currentThread.setContextClassLoader(targetTccl); myMethod(); //执行自定义的方法 } finally { //还原线程上下文类加载器 Thread.currentThread().setContextClassLoader(classLoader); }
我们在使用JDBC的时候,会有一个注册驱动的代码Class.forName(com.mysql.jdbc.Driver),但是其实不写这个语句也可以正常使用JDBC,追踪源码,发现DriverManager的类加载器是BootStrapClassLoader。而DriverManager中有一个静态代码块,里面有一个loadInitialDrivers(),这个方法就是注册数据库驱动的,那么问题来了,DriverManager的类加载器是BootStrap,但是显然mysql-connector-java-5.1.47.jar包不在lib目录下,那么DriverManager是如何正确加载jar包的呢?其实在方法中使用了Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());而getSystemClassLoader返回的是ApplicationClassLoader,这样就打破了双亲委派的规则。
ContextClassLoader的作用就是为了破坏双亲委派机制。
public class DriverManager{ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } public void loadInitialDrivers(){ //1.使用ServiceLoader机制加载驱动,即SPI //ServiceLoader.load就使用了线程上下文类加载器,获取了线程上下文的类加载器。 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); .... //2.使用jdbc.drivers定义的驱动名加载驱动 for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } ..... } }
2.6.4 SPI机制(Service Provider Interface)
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 一个服务(Service)通常指的是已知的接口或者抽象类,服务提供方就是对这个接口或者抽象类的实现,然后按照SPI 标准存放到资源路径META-INF/services目录下,文件的命名为该服务接口的全限定名。如有一个服务接口:
package spi; public interface HelloService { public void hi(); }
以及其实现类:
package spi.impl; import spi.HelloService; import static utils.Utils.*; public class HelloServiceImpl implements HelloService { @Override public void hi() { print("hello spi"); } }
那此时需要在resources的META-INF/services中创建一个名为spi.HelloService的文件,其中的内容就为该实现类的全限定名:spi.impl.HelloServiceImpl。 如果该Service有多个服务实现,则每一行写一个服务实现(#后面的内容为注释),并且该文件只能够是以UTF-8编码。 然后,我们可以通过ServiceLoader.load(Class class); 来动态加载Service的实现类了。 许多开发框架都使用了Java的SPI机制,如java.sql.Driver的SPI实现(mysql驱动、oracle驱动等)、common-logging的日志接口实现、dubbo的扩展实现等等。
SPI机制的约定
- 在META-INF/services/目录中创建以Service接口全限定名命名的文件,该文件内容为Service接口具体实现类的全限定名,文件编码必须为UTF-8。
- 使用ServiceLoader.load(Class class); 动态加载Service接口的实现类。
- 如SPI的实现类为jar,则需要将其放在当前程序的classpath下。
- Service的具体实现类必须有一个不带参数的构造方法。
测试:
public class test { public static void main(String[] args) { ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class); for (HelloService helloService : loader) { helloService.hi(); } //也可以使用Iterator迭代器接受loader.iterator(); } }
ServiceLoader.load就使用了线程上下文类加载器,获取了线程上下文的类加载器。
2.6.5 自定义类加载器
什么时候需要自定义类加载器?
- 想加载非classpath随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤:
- 继承ClassLoader父类
- 要遵从双亲委派机制,重写findClass方法
-
- 注意不是重写loadClass方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的DefineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
public class CustomClassLoader extends ClassLoader{ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String path="D://myclasspath/"+name+".class"; ByteArrayOutputStream os=new ByteArrayOutputStream(); try { Files.copy(Paths.get(path), os);//读入类文件的字节码 byte[] bytes=os.toByteArray(); return defineClass(name,bytes,0,bytes.length);//定义类名对应的字节码文件 } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类文件未找到",e); } } }
2.7 运行期优化
2.7.1 即时编译
分层编译
JVM将执行状态分成了5个层次:
- 0层,解释执行(Interpreter)
- 1层,C1即时编译器编译执行(不带profiling)
- 2层,C1即时编译器编译执行(带基本的profiling)
- 3层,C1即时编译器编译执行(带完全profiling)
- 4层,用C2编译器编译执行
profiling是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的次数】等
即时编译器(JIT)与解释器的区别
- 解释器是将字节码转换为机器码,下次即使遇到相同的代码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并存如code cache,下次遇到相同的代码不会重复编译,直接执行
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用代码,我们不用将其编译为机器码,而是直接以解释器方式运行;另一方面,对于仅占据一小部分的热点代码,我们则可以将其交给JIT编译为机器码运行,以达到理想的运行速度。执行效率上简单比较一下Interpreter<C1<C2,总的目标是发现热点代码(hotspot的由来),优化之。
2.7.2 逃逸分析
关于 Java 逃逸分析的定义:
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。
我们了解了 Java 中的逃逸分析技术,再来了解一个对象的逃逸状态。
逃逸状态:
1.全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
- 对象是一个静态变量
- 对象是一个已经发生逃逸的对象
- 对象作为当前方法的返回值
2.参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。
3.没有逃逸
即方法中的对象没有发生逃逸。
开启和关闭逃逸分析:
- 开启逃逸分析:-XX:+DoEscapeAnalysis
- 关闭逃逸分析:-XX:-DoEscapeAnalysis
- 显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析默认为启用状态。
逃逸分析优化
针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化。
- 锁消除
-
- 我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
- 例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。
- 锁消除的 JVM 参数如下: 开启锁消除:-XX:+EliminateLocks 关闭锁消除:-XX:-EliminateLocks 锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。
- 标量替换
-
- 首先要明白标量和聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象。
- 对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。
- 这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。
- 标量替换的 JVM 参数如下: 开启标量替换:-XX:+EliminateAllocations 关闭标量替换:-XX:-EliminateAllocations 显示标量替换详情:-XX:+PrintEliminateAllocations 标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上。
- 栈上分配
-
- 当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。
- 也就是说如果一个对象没有被本方法以外的方法使用,那么这个对象就会被放在栈内存而不是堆中,并且栈中只有对象包含的变量。
2.7.3 方法内联
如果一个方法是热点方法并且长度不是很长,会进行内联,就是将方法内代码拷贝到调用者的位置,如:
public int square(final int i){ return i*i; } System.out.println(square(9)); //优化后 System.out.println(9*9);
还能够进行常亮折叠的优化变成:
System.out.println(81);