这里写自定义目录标题
JVM
虚拟机
谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
为什么要学JVM
学习理解更高层次内容的需要(项目管理,性能调优)
作用:实现了代码的一次编写到处运行,自动内存管理,自动垃圾回收;现在的 JVM 不仅可以执行 java 字节码文件,还可以执行其他语言编译后的字节码文件,是一 个跨语言平台。
JVM位置
JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
详细图
类加载器
加载
将硬盘上的字节码文件读入到内存中,生成class对象,把硬盘上的结构转为内存结构,使用的是classloader进行加载,充当快递员的角色
链接
-
验证:
验证字节码格式是否被修改
验证语法,eg:是否继承了final修饰的类 -
准备
为类中静态的变量赋默认值,不为静态常量进行赋值,这是jvm的一种调优机制
补充:静态常量是编译阶段赋值
实例变量将会在对象初始化的时候随着对象一起分配在java堆中
-
解析
将符号引用转为直接引用~(文件中的逻辑引用到内存中的实际地址)
初始化
对类中的静态成员进行赋值
类什么时候初始化:new对象,反射机制,子类被加载,访问类中的静态变量或静态方法, main方法
-
分类
jvm角度:
启动类加载器(非java语言);
其他类加载器(java语言)
程序员角度:
启动类加载器:加载\jre\lib目录下的类
扩展类加载器:加载\jre\lib\ext目录下的类
应用程序类加载器:包含自定义的类加载器
-
双亲委派机制
为了保证类加载的正确性,安全性,在加载类时,会让其父类先去加载。知道顶级的类加载器,如果找到则返回,如果仍然没有找到则委派给其子级去加载,如果找到则返回,如果都没有找到则抛出异常(ClassNotFoundException)
优点:安全,避免我们自己写的类替换系统中的类;避免类的重复加载(使用了findLoadClass()判断当前类是否已加载)
-
如何打破类的双亲委派机制
java中提供了一个ClassLoader类,定义那些方法加载类;
像Tomcat这种服务器,里面就会自定义类加载器
loadClass(String classpath)//建议的 底层使用双亲委派机制加载类; findClass(String classPath) // 如果需要自定义,可以重写findClass() defineClass() //将读到的class文件的数据构造出一个Class对象
-
类的主动使用与被动使用
-
主动:new对象,反射机制(Class.forName(“”)),子类被加载,使用静态变量,静态方法 main方法
-
被动:访问静态常量,作为引用类型使用(创建数组)
-
运行时数据区
程序计数器
是一块内存很小的区域,用来存放下一条要执行指令的地址,便于在线程切换执行时记录位置,是线程私有的,生命周期与线程一样,运行数度快,不会出现内存溢出
虚拟机栈
栈是运行单位,存储一个一个的方法,当调用一个方法时,创建一个栈帧,将方法中的信息存储到栈帧.
操作只有两个,调用方法入栈,方法执行完成后出栈. 先进后出的结构
运行速度非常快,仅次于程序计数器, 当入栈的方法过多时,会出现栈溢出(内存溢出)
线程独立的,不同线程之间的方法不能相互调用
栈帧的内部结构:
局部变量表:方法内声明的局部变量,方法参数;对于基本类型的变量,直接储存它的值,对于引用类型的变量则存储的是指向对象的引用
操作数栈:运算区域
动态链接:指向运行时常量池的方法引用
方法返回地址:当一个方法执行完毕后,要返回之前调用它的地方
附加信息
本地方法栈
native修饰的方法,没有方法体,eg:hashCode(),read(),start(),arraycopy()
本地方法不是用java语言写的,例如操作系统的方法,如果调用本地方法则在本地方法栈中运行,也可能出现内存溢出问题
堆
-
概述:
是jvm管理的最大的一块空间,主要用来储存对象,是线程共享的,JVM启动堆空间就创建了,大小就确定了,但是可以通过参数改变大小,物理上不连续,逻辑上连续,是GC的重点区域
-
区域划分:
新生代(伊甸园区,S0,S1),老年代
-
为什么要区域划分?
不同对象,它的生命周期不同,这样可以将不同的对象存放在不同的区域,不同的区域采用不同的垃圾回收算法进行处理,扬长避短
-
创建对象在区域中的分布:
一个对象刚刚被创建时存放在伊甸园区,当垃圾回收时会把伊甸园区存活下来的对象移动到幸存者区,幸存者区有区域1和区域2,假设先把存活下来的对象放在幸存者1区,当下次垃圾回收时,会把伊甸园区和幸存者1区存活的对象移动到幸存者2区,这时幸存者1区就被清空了,当下次垃圾回收时,会将伊甸园区和幸存者2区存活下来的对象放在幸存者1区,清空幸存者2区,交替执行,当垃圾回收时,每次会对对象进行标记,在对象头中有一块空间会记录被标记的次数,在对象头中记录分代年龄只有4bit,也就是最大15次进入老年代
-
新生代与老年代配置比例:
默认1:2 可以通过参数来设置
默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整 个堆的 1/3
当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年 代的大小,来进行调优
Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如-XX:SurvivorRatio=8新生区的对象默认生命周期超过 15 ,就会去养老区养老
-
分代收集思想:
jvm垃圾回收机制可以通过不同的区域进行回收,针对新生代会频繁回收,称为yong(Minor) GC;j较少回收老年代,称old(Major) GC
当调用System.gc();老年代内存不足;方法区内存不足时,会触发FULL GC(整堆收集),尽量避免整堆收集,因为整堆收集时,其他用户线程暂停时间较长 -
字符串常量池为什么要调整位置:
JDK7以后将字符串常量池的位置由方法区调整到了堆中,因为方法区只用在触发FULL GC 时才会进行回收,回收效率低,所以将字符串常量池移到堆中,提高垃圾回收效率
方法区
存储类信息(方法、属性、静态常量、静态变量、编译器、编译后的代码、运行时常量池(字面量值))
线程共享的,也会出现内存溢出,也会涉及到垃圾回收,生命周期也是JVM启动就创建,关闭就销毁
方法区大小设置:-XX:MaxMataspaceSize: window默认是21MB,满后触发full GC;也可以是-1,没有上限,可以占用整个计算机内存
方法区的垃圾回收:主要回收的是静态常量,类信息
3条件:
- 该类产生的对象都被回收了
- 该类的class对象不再被其他类使用
- 该类对应的类加载器也被回收了
执行引擎
-
装载字节码文件到执行引擎中,字节码不是机器码,只是jvm规范定义的指令码,执行引擎需要将字节码 解释/编译为不同平台所能识别的机器码
-
解释器:jvm运行程序时逐行对字节码进行翻译,效率低
-
JIT编译器:对某段代码整体编译后执行,效率高,编译需要一定的时间
-
java为什么是半编译半解释型?
起初java中只提供了解释执行的方式,但是解释执行的效率较低,后来引入编译器,可以对程序执行的热点代码进行编译,并把编译后的内容缓存起来,后期执行效率高,热点代码是采用计数器的方式来记录的。
程序启动后可以通过解释器立即对代码进行解释执行,不需要等待编译,提高了响应速度,之后对热点代码采用编译执行,从而提高了后续效率
本地方法接口
什么是本地方法
使用native修饰的方法,不是java语言实现的,是操作系统实现的
为什么要使用本地方法
- 需要获取一些硬件信息,如内存地址,启动线程,IO,调用本地方法接口
- jvm底层进行字节解释/编译部分有C语言实现
垃圾回收
java提供自动垃圾回收功能,c++没有自动垃圾回收功能,自动垃圾回收功能并不是java首创的,但是java在垃圾回收这一块一致在更新升级
什么是垃圾:没有被任何引用指向的对象
为什么要回收:
- 内存方面:垃圾对象如果一直不回收就会一直占用空间,垃圾对象越存越多,从而导致内存溢出
- 内存碎片:对内存空间的碎片进行整理,如果不整理,需要储存像数组这种需要连续内存空间的对象就有可能存不下
早期怎么回收:
早期c++代码,需要程序员手动销毁垃圾对象;
不足:麻烦,程序员可能忘记删除就会造成内存泄漏
内存溢出:内存不够用,报内存溢出错误
内存泄漏:有些对象已经不再被使用了,但是垃圾回收对象又不能回收它,这种悄悄占用内存资源的情况叫做内存泄漏
现在怎么回收:
-
方式:
现在引进了自动内存管理;
优点:降低了程序员的工作量,降低了内存溢出和内存泄露的风险 -
担忧:自动内存管理降低了程序员内存管理的能力,一旦出现问题无法下手解决
那些区域回收:方法区和堆(在新生代频繁回收,较少回收老年代,几乎不回收方法区)
标记阶段算法
引用计数算法(未使用)
对每个对象保存一个整型 的引用计数器属性。用于记录对象被引用的情况。
缺点:
- 不能解决循环引用问题
- 需要维护计数空间
- 赋值后要对计数器进行更新操作
根可达分析算法
从一组GCRoots对象(一组活跃的对象,当前栈帧中使用的对象),开始向下查找,如果与GCRoots是相关联的,那么就不是垃圾对象,否则判定为垃圾对象
GCroots对象:
- 栈中所使用的的对象
- 静态成员变量所指向的对象
- synchronized同步锁对象
- jvm系统内部的对象
避免了循环引用问题
补充
对象的finalization机制:
finalize() 是Object类中定义的方法,子类可以重写它,但是不要主动去调用它,finalize()在对象被回收前,由垃圾回收线程调用且只调用一次,一般情况下不需要重写此方法,如果在对象销毁之前需要执行一些释放资源的操作可以重写此方法 ,比如关闭文件、套接字和数据库 连接等
但是要注意,不要在此方法将对象复活,或者出现死循环
对象状态
- 可触及的:从根节点开始可以达到这个对象
- 可复活的:已经被标记为垃圾finalize()方法还没有被执行(可能调用该方法将对象复活)
- 不可触及的:当对象的finalize()方法已经被执行了,而且对象没有被复活,那么对象就进入到了不可触及状态
细节:
如果一个对象第一次被标记为垃圾且finalize()方法没有被执行,则会将这些对象放在队列中,调用他们的finalize()方法,如果在执行次方法时,对象与GCroots中的某个对象关联上了则将其从列表中移除,
当第二次被标记为垃圾对象,就是不可触及的状态会直接被回收掉
回收阶段算法
复制算法
使用两块内存空间,将正在使用区域中的存活对象,复制到另一个区间,按序排放,清除原来的空间
优点:适用于新生代,效率高,无碎片
缺点:需要两块空间,不适用于G1垃圾回收器,G1垃圾回收器每块又分为很多小的区域,需要记录其地址
标记清除
并非真正意义上的清除,而是将垃圾对象地址记录在一个列表里面,当有新的对象需要分配空间时,判断列表中的空间是否够用,够用就用新对象把原来的旧对象覆盖掉
优点:实现简单不需要移动对象,适用于老年代
缺点:会产生内存碎片
标记压缩
针对于标记清除算法去的不足,将存活对象进行了整理,清除垃圾对象,不会产生内存碎片,适用于老年代
分代收集思想
新生代存活对象生命周期短;需要频繁回收,复制算法效率高,适用于年轻代
老年代对象生命周期较长,不需要频繁回收,使用标记清除和标记压缩
垃圾回收相关概念
System.gc()相关概念
通过 System.gc()者 Runtime.getRuntime().gc() 的调用,会
显式触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用
的内存。
无法保证对垃圾收集器的调用(不能确保立即生效)。
JVM 实现者可以通过 System.gc() 调用来决定 JVM 的 GC 行为。而一般情况
下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些
特殊情况下,我们可以在运行之间调用 System.gc()。
Stop the World
在垃圾回收线程标记时,需要在某个时间点上让所有的用户线程暂停一下,保证在判断对象是否为垃圾的准确性,性能好的垃圾回收器发生STW次数少。
垃圾回收器
对垃圾回收的落地实现,jvm中分不同的垃圾回收器,可根据不同场景选择使用
分类:
-
按线程数量:
- 单线程(Serial):垃圾回收线程只有一个,适用于小场景
- 多线程(Parallel):垃圾回收线程有多个,效率高
-
按工作模式:
- 独占式:垃圾回收线程工作时,用户线程全部暂停STW
- 并发式:垃圾回收线程执行时可以不用暂停用户线程
-
按内存工作区域:
- 新生代:新生代的垃圾回收器
- 老年代:老年代的垃圾回收器
GC性能指标
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运 行时间+内存回收的时间)
垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
内存占用:Java 堆区所占的内存大小。
快速:一个对象从诞生到被回收所经历的时间。
HotSpot 垃圾收集器
CMS垃圾回收器
CMS概述
CMS之前不管是单线程还是多线程的垃圾回收器都是独占式的,并发标记清除首创用户线程可以和垃圾回收线程并发执行,追求低停顿
- 初始标记::Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
- 并发标记:垃圾回收线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。
- 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
- 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。 这个过程非常耗时。
优点:可以做到并发收集
缺点:基于标记清除算法实现,回产生内存碎片;用户线程和垃圾回收线程并发执行,导致吞吐量低;无法处理浮动垃圾(垃圾并发清理时,用户线程不暂停,随时会产生新垃圾,无法处理,只能等下次垃圾回收处理)
三色标记算法
将对象分为三个状态:黑色,灰色,白色
黑色:已经标记过的不是垃圾对象,且该对象下的关联属性也标记过了
灰色:已经被垃圾回收器标记过,但是还有没有被标记过的
白色:没有被垃圾回收器扫描过的,标记是垃圾
步骤:
- 刚开始,确定GCRoots对象为黑色
- 将GCRoots直接关联的对象设置为灰色
- 遍历灰色对象,下面如果还有关联的对象,灰色变为黑色,下边关联对象变为灰色
- 重复标记
- 将白色对象清除
可能会出现漏标和错标问题
漏标:A关联B,B关联C,A是黑色,B是灰色,C是白色,此时A断开与B的联系,但是B已经是灰色,B和C就成为了浮动垃圾,需要等到下次回收。
错标:A关联B,B关联C,A是黑色,B是灰色,C是白色,此时B与C断开联系,A关联C,但因为A是黑色,不会再进行扫描,就会清理掉不是垃圾的C
解决:将发生变化的关系进行记录,重新标记
G1垃圾回收器
G1也是用的是并发标记和清除,将整个堆的内存空间分为更小的区间,回收时可以根据每个区间的优先级(里面垃圾数量),优先回收优先级较高的区间,降低了用户线程的停顿,提高了吞吐量。
对整堆进行统一的管理,没有年轻代和老年代