JVM
概述:
作用:把我们的一套程序在不同的平台上运行 可以实现自动的内存管理 自动的垃圾回收
JVM整体结构分为4大块:
1.类加载系统:
负责从硬盘上加载字节码文件
2.运行时数据区:
存储运行时的数据(5个区域 方法区、堆、栈、本地方法栈、程序技术器)
3.执行引擎:
负责将字节码 解释/编译 为真正的机器码
4.本地方法接口:
负责调用操作系统本地方法
类加载系统
1.什么是类加载?
字节码储存在硬盘上,需要时由类加载系统将类的信息加载到内存中(方法区)
为类创建一个Class类的对象,使用ClassLoader进行加载,充当一个快递员角色
2.类加载过程?
(1)加载:将硬盘字节码读入到内存中,生成此类的Class对象,把硬盘上的结构转为内存结构。
(2)验证:验证字节码的格式,是否被修改(污染)
验证语法,例如类是否继承final修饰的类
准备:在准备阶段为类中静态的变量赋予初始值
static int num =123; 在准备阶段static int num =0;在后面初始化阶段才改成123 在准备阶段不为静态常量进行赋值
解析:将符号应用(文件中的逻辑引用)转为直接引用(内存中的实际地址)
(3)初始化:对类中的静态成员进行赋值了。
类什么时候初始化:new对象、使用类的静态成员、反射动态加载类(Class.forName())、子类被加载
只访问了某一个类中的静态常量
3.负责加载类的类
类加载器分类:
站在JVM角度上分为:
1.启动类加载器(引导类加载器)这部分不是用java语言写的 负责加载java核心类
2.其他类加载器(这部分指的是用java语言写的类加载器)
从程序员角度分为:
1.启动类加载器 负责加载java核心类
2.扩展类加载器 负责加载\jre\lib\ext目录下的类,包括应用程序类加器
3.应用程序类加载器 包括自定义的类加载器,负责加载程序中的类(自己写的)
4.双亲委派机制
为了确保加载类的正确性、安全性,在类加载类时,采用双亲委派机制。当需要加载程序中一个类时,会先让加载器的父类去加载,直到最顶级的启动类加载器,如果父级找到了返回使用,如果依旧没有找到,那么就委派给子级去加载,找到了就返回,如果所有的类加载器都没有找到,报类加载器异常。
优点:安全避免了自己写的类替换系统中的类 避免类的重复加载
5.类何时加载
主动使用:将类加载的整个过程完成
new对象,使用类的静态成员,反射动态加载类Class.forName(),子类被加载
被动使用:不会加载初始化
访问类中的静态常量,将类作为类型,例如创建数组,用类作为类型使用。
6.如何打破双亲委派机制
java中提供一个Class.Loader类,定义哪些方法加载类
loadclass(String classpath) 建议 底层使用双亲委派机制加载类 findclass(String classpath) 如果需要自定义,可重写findclass() defineclass() 将读到class文件的数据,构造出一个class对象 再有就是像Tomcat这种服务器软件,里面也会有自己定义的类加载器
JVM运行时数据区
程序计数器:记录线程运行的位置(行号),线程需要切换执行,所以记录执行位置。
虚拟机栈:运行java方法的区域,每个方法生成一个栈帧。
本地方法栈:java经常需要调用一些本地方法(操作系统的方法 hashCode(),read(),start(),arraycopy())
堆:存放程序中产生的对象,也是虚拟机中内存占比最大的一块
方法区:存放类信息
堆、方法区:是线程所共享的
程序计数器、虚拟机栈、本地方法栈:是线程私有的,线程独立的。
堆、方法区、栈、本地方法栈:会出现内存溢出错误。
程序计数器
是一块内存很小的的区域,主要用来记录每个线程中执行的执行位置,便于线程在切换执行记录位置。是线程私有的,生命周期与线程一样,运行速度快,不会出现内存溢出。
虚拟机栈
基本概念:栈是运行单位,存储一个一个的方法,当调用一个方法时创建一个栈帧,将方法中的信息存储到栈帧中。
操作只有两个:调用方法,入栈,方法执行完后,出栈(先进后出)
运行速度快,仅次于程序计数器,当入栈的方法过多时,会出现栈溢出(内存溢出)
线程是独立的,不同线程之间方法不能相互调用。
栈帧的内部结构:
1.局部变量表:方法内部声明的局部变量,方法参数。
2.操作数栈:运算区域 a+b
动态链表:调用的方法地址,字面量地址。
方法返回地址
本地方法栈
本地方法 native修饰的方法,没有方法体。 hashCode() read() start() arrayCopy()
本地方法不是用java语言写的,例如操作系统方法。
如果调用本地方法,那么本地方法在本地方法栈中运行,也会出现内存溢出
堆
创建对象 对象引用
堆空间是jvm内存中一块空间,主要用来存储对象,是jvm中空间最大的块,是线程共享。
jvm启动,堆空间就创建了,大小确定了,但可以通过参数改变大小的,这就是jvm调优。物理上不连续,逻辑上连续的,堆就是垃圾回收的重点区域。
堆的区分
新生代: 伊甸园区 幸存者1 幸存者2
老年代
为什么要分区域?
不同的对象,它的生命周期不同,这样可以将不同的对象存储在不同的区域,不同的区域采用不同的垃圾回收算法。进行处理,扬长避短。
创建的对象在这些区域中如何分布?
当一个对象刚刚创建后,被存放在伊甸园区。
当垃圾回收进行时,会把伊甸园区中存活下来的对象,移动到幸存者区。
幸存者有两个 区域1和区域2
首先把伊甸园区存放的对象放在幸存者1区,当下次垃圾回收到来时,会将伊甸园区和幸存者1区的存活对象移到幸存者2区,清空幸存者1之后再次回收时会将伊甸园区和幸存者2区存活的对象一到幸存者1区,清空幸存者2,交替执行。
什么时候对象去老年代?
当垃圾回收每次对对象进行标记时,在对象头中有一个空间被用来记录被标记的次数。
在对象头中,记录分代年龄只有4bit位空间,只能记录15次。
堆各个空间比例
新生代与老年代默认比例是1:2,但可以通过参数设置
—XX:survivorRatio=8
一个对象通过15次的垃圾回收依然存活就去老年代。可以通过 XX:MaxTenuringThreshold=15设置,最大15次
分待收集思想
jvm垃圾回收可以分为不同的区域进行回收
针对新生代会频繁回收,称为 yong GC
较少回收老年代,称为old GC
当调用System.gc(),老年代内存不足时,方法区内存不足时,会触发 FULL GC(整堆收集)
尽量避免整堆收集,整堆收集,其他用户线程暂停的时间长。
堆的参数
设置整个堆的大小
各个区间比例
对象年龄
字符串常量池位置
jdk7之后将字符串常量池的位置从方法区转移到堆中。因为方法区只有在触发FULL GC时才会进行回收,回收效率低。所以将字符串常量池移到堆中,提高垃圾回收效率。
方法区
存储类信息
类信息:方法、属性、静态常量、静态变量,即时编译器编译后的代码,运行时常量池(字面量值)
方法区是线程共享的,也可能会出现内存溢出,也会涉及到垃圾回收。方法区的生命周期也是虚拟机启动就创建,虚拟机关闭就销毁。
方法区大小设置
—XX:MaxMataspaceSize windows方法中方法区默认最大值为21MB
如果达到21MB就会触发FULL GC 值也可设为-1,没有上限,占用整个计算机内存。
方法区是会涉及到垃圾回收的,主要回收的是静态常量,类信息,类信息何时被卸载,满足3个条件:
1.该类所产生的对象都被回收
2.该类对应的class对象,不在被其他地方引用
3.该类对应的类加载器被回收了
本地方法接口
通过本地方法接口模块来与操作系统接口进行访问
什么是本地方法?
使用native修饰的方法,不是java语言实现的,是操作系统实现的。
为什么要使用native Method
1.例如需要获得硬件的一些信息,如内存地址,启动线程,IO,调用本地方法接口就很方便
2.jvm底层进行字节码解释或编译部分也有用C语言实现
执行引擎
作用:负责装载字节码文件到执行引擎中,字节码不是机器码,只是jvm规范中定义的指令码。
执行引擎:需要将字节码 解释/编译为不同的平台识别的机器码。
Hellow.java-------jdk编译工具 javac-------.class 称为前端编译
.calss --------执行引擎 编译为机器码 称为后端编译
解释器:jvm运行程序时,逐行对字节码指令进行翻译,效率低
JIT(即使)编译期:对某段代码整体编译后执行 效率高 编译需要耗一段时间
为什么是半解释器半编译型?
起初java中只是提供了解释执行的方法,但是解释执行效率低,后来引入编译器,可以对查询执行中的热点代码进行编译,并把编译后的内容缓存起来,后期执行效率高。
热点代码采用计数器方式来记录
程序启动后可以通过解释器立即对代码进行解释执行,不需要等待编译,提高响应速度,之后对热点代码采用编译器编译执行从而提高后续效率。
垃圾回收
概述:java语言提供自动垃圾回收功能的,C++没有垃圾回收,垃圾回收也不是java首创。java在垃圾回收这块一直不断升级。
什么是垃圾:没有被任何引用指向的对象。
例如:Object obj= new Object; obj.hashCode(); //引用 obj=null; //没有引用 垃圾
为什么回收:垃圾对象,如果不回收,他就会一直占用内存空间,垃圾对象越积越多,可能导致内存不够用。(内存溢出)
对内存空间中的内存碎片进行整理,如果不整理,需要存储像数组这样的对象,可能存储不了。
早期怎么回收:早期C++代码,需程序手动销毁对象。
不足:麻烦手动创建,手动回收。有时忘删除,那么就会造成内存泄漏。
内存溢出:内存不够用,报内存溢出错误 OOM
内存泄漏:有一些对象已经不再被使用了,但是垃圾回收对象又不能回收它,这种悄悄占用内存资源的现象,称为“内存泄漏”。OM(OutofMemory)
现在的语言引进了自动的内存管理。
优点:降低了程序员的工作量,降低了内存溢出和内存泄漏的风险。
担忧:自动的垃圾回收,降低了程序员对内存管理能力。一旦出现问题,不能下手解决。
哪些区域回收:方法区,堆,(频繁回收新生代,较少回收老年代,基本不回收方法区)。
垃圾回收算法
标记阶段
标记那些对象,已经是垃圾。
引用计数算法(没有使用)
对象内部有一个计数器,有一个引用指向 计数器+1
obj=null 计数器-1
实现简单,缺点:A里面包含B,B里面包含A(造成内存泄漏)。
不能解决循环引用问题。增加了空间、时间开销。需要维护计数器空间,赋值后对计数器进行更新。
根可达算法(可达性分析算法)
实现思路:从一组GCRoots对象(一组活跃的对象,当前栈帧中使用的对象)开始向下查找,如果与GCRoots对象相关联的,那么就不是垃圾对象,否则判定为垃圾对象。
GCRoots可以是哪些元素:
1.栈中所有使用的对象
2.静态的成员变量所指向的对象
3.Synchronized同步对象
4.JVM系统内部的对象
可达性分析避免对象循环引用的问题。
final finally{} finalize()
finalize()是Object类中定义的方法,子类可以重写,但是不要主动自己调用它。finalize()在对象被回收前由垃圾回收线程调用,只能调用一次。
一般情况下,不需要重写次方法。如果在对象销毁前,执行一些释放资源的操作,可以重写此方法。但是注意,不要将对象复活或出现死循环。
从垃圾回收角度,将对象分为三种状态:
1.可触及的,从根节点开始,可以到达这个对象
2.可复活的,对象标记为垃圾,finalize()方法没有执行(对象可以在finalize中复活)
3.不可触及的:对象中finalize()方法已经执行,而且对象没有被复活,那么进入不可触及状态。
对象回收的一个细节:如果一个对象第一次被标记为垃圾且finalize()没有被执行。将这些对象,放在一个队列中,调用它们的finalize(),如果在finalize()方法;对象与GCRoots中某个对象关联上,从队列中移出。当第二次被标记为垃圾对象时,那么直接就是不可触及的,被回收。
垃圾回收阶段:
复制算法 (针对新生代 对象存活较少,需要移动对象)
使用到两块内存空间(对标两个幸存者区),将正在使用区域中存活的对象复制到另一个区间,排放整齐,清除原来的空间。
优点:内存碎片少 效率快
缺点:使用两块内存,G1的垃圾回收器每一个区域又分成多个小的区域,需要来记录地址
标记清除(针对老年代,存活对象较多,不需要移动对象,不会整理内存)
清除:并非直接清理垃圾对象,而是将垃圾对象的地址记录下来,存在一个列表里面。如果所有新的对象需要分配空间,那么就从空闲列表判断空间是否够用,如果够用,那么用新对象替换垃圾对象。
优点:实现简单,不需要移动对象
缺点:会产生内存碎片
标记—压缩算法
针对于标记清除的不足,将存活的对象进行整理,然后清除垃圾对象,这样内存就不会产生内存碎片。
标记清除:不移动对象,产生碎片
标记压缩:移动对象,不产生内存碎片
分代收集
新生代存活对象生命周期短,需要频繁回收,复制算法效率高,适合新生代。
老年代对象生命周期长,不需要频繁回收,使用标记清除和标记压缩。
STW(stop the world)
在垃圾回收线程标记是,需要在某一个时间点上,让所有的用户线程暂停一下,保证在判定对象是否为垃圾时的准确性。
性能好的垃圾回收器,发生STW次数会少一些。
垃圾回收器
是对垃圾回收的落地实现
JVM中又分为不同种类的垃圾回收器,可以根据使用场景选择对应的垃圾回收器。
分类:
按照线程数量分为:
单线程垃圾回收器(垃圾回收线程只有一个,适用于小型场景)
多线程垃圾回收器(垃圾回收线程有多个同时执行,效率高)
按照工作模式分为:
独占式(垃圾回收线程执行时,用户线程全部暂停 STW)
并发式(垃圾回收线程执行时,可以不用暂停用户线程。从CMS这款回收器引入并发式)
按照内存工作区域分为:
年轻代(年轻代区域的垃圾回收器)
老年代(老年代区域的垃圾回收器)
CMS
CMS是之前的不管线程还是多线程的垃圾回收器,都是独占式的。
并发标记清除首创用户线程可以和垃圾回收线程并发执行,追求低停顿。
初始化标记:单线程独占标记对象。
并发标记:垃圾回收线程和用户线程并发执行。
重新标记:使用多线程独占进行标记对象。
并发清除:垃圾回收线程和用户线程并发执行。
优点:可以做到并发收集
缺点:用户线程和垃圾回收线程并发执行,导致吞吐量降低,无法处理浮动垃圾(并发标记时用户线程不暂停)标记完成后随时产生新的垃圾对象,无法处理,只能等到下次垃圾回收处理。
三色标记算法:将对象标记为不同的状态 黑、灰、白
黑:该对象已经被标记过的,不是垃圾对象且对象下的关联属性也标记过了。
灰:已经被垃圾回收器标记过了,但是还有没有标记过的。
白:没有被垃圾回收器扫描过的,标记是垃圾。
1.刚开始,确定GCRoots的对象为黑。
2.将GCRoots直接关联的对象设为灰。
3.遍历灰色对象所有引用,灰色本身变黑,下面关联对象是灰。
4.重复标记。
5.将白色对象清除。
可能会出现 漏标、错标问题。
漏标
A关联B B关联C 当B是灰色,此时A和B断开联系,但是B已经是灰色的。B和c是浮动垃圾,等待下一次回收。
错标
A关联B B关联C B为灰,B和C断开,A和C联系。A为黑色,不在扫描,C会被回收。
解决:就是将发生变化的关系进行记录,重新在标记。
G1回收器
G1也是使用到了并发标记和清除,将整个堆的每个区域又划分为更小的空间,回收时可以根据每个区域的优先级(由里面的垃圾数量),先回收优先级高的区间。降低了用户线程的停顿,提高了吞吐量,对整堆进行统一管理,没有新生代和老年代。