jvm作用
把一套程序在不同的平台上运行,可以实现自动的内存管理,自动的垃圾回收。
jvm分为四大结构:
1.类加载系统:负责从硬盘上加载字节码文件。
2.运行时数据区:存储时的数据,分为栈、方法区、堆、本地方法栈、程序计数器
3.执行引擎:负责将字节码解释为真正的机器码。
4.本地方法接口:负责调用操作系统。本地方法。
1.类加载系统
问:什么是类加载?
答:字节码存在硬盘上,需要运行时,由类加载系统负责将类的信息加载到内存中(方法区),为每个类创建一个Class类的对象,使用的是ClassLoad进行加载,相当于是一个快递员。
问:类加载过程是什么样的?
答:
1.加载:将硬盘字节码读到内存中,生成此类的Class对象,半硬盘上的结构转化为内存结构。
2.链接:
验证:
(1)验证字节码格式是否被修改
(2)验证语法,例如类是否继承了final
准备:在准备阶段为类中静态的变量赋初始值,在准备阶段static int num=0,在后面的初始阶段static int num =123,在准备阶段不为静态的常量赋值。
解析:将符号引用转为直接引用。
3.初始化:
对类中的静态成员进行赋值
类初始化时间:new对象,调用类的静态成员,反射动态加载类,子类被加载
问:初始化的顺序?
答:父类static->子类static->父类构造方法->子类构造方法
类加载器
负责加载类的类
类加载器的分类:
站在jvm的角度上分为:
1.启动类加载器(引导类加载器),这部分不是java语言写的
2.其他类加载器,这部分指的是用Java语言写的类加载器
从程序员角度看:
1.启动类加载器,负责加载Java核心类
2.扩展类加载器,负责加载\lar\lib\ext目录下的类,包含应用程序类加载器
3.应用类加载器,负责加载自己写的程序的类
双亲委派机制
为了确保类加载的正确性,安全性,在加载类时使用双亲委派机制,当需要加载程序中的一个类时,会先让它的父级去加载,直到最顶级的启动类加载器,如果父级找到了就返回使用,如果没找到就委托给子级去加载,找到了就返回,如果所有的类加载器都没有找到,那就返回异常找不到类。
优点:安全,避免了自己写的类替换了系统中的类,避免了类重复加载。
问:双亲委派机制是否会被打破?
答:Java提供了一个ClassLoader类,定义了哪些方法加载类
loadClass(Stringpath);//建议低层使用的双亲委派机制加载类
findClass(Stringpath);//如果需要自定义,可以重写findClass()
defineClass();读到class文件数据,构建出一个Class对象,再有像tomcat这种服务器软件,里面有类加载器
问:类何时加载
答:
主动使用:将类加载的整个过程完成。new对象,使用类的静态成员,反射动态加载类,子类被加载
被动使用:不会加载初始化,访问类中的静态常量,将类作为类型,把类作为类型使用,例如创建数组
2.运行时数据库
程序计数器:记录数据运行的位置,线程需要切换执行,所以需要记录执行的过程
虚拟机栈:运行java方法的区域,每个方法生成一个栈帧
本地方法栈:java将经常需要调用一些本地方法(操作系统的方法hashcode,read(),start(),arraycopy())
堆:存放程序中产生的对象,也是虚拟机中内存占比最大的一块
方法区:存放类信息
堆,方法区:是线程共享的
程序计数器,虚拟机栈,本地方法栈:是线程私有的,线程独立的
堆,方法区,栈:会出现数据溢出错误
程序计数器:
是一块内存很小的区域,主要用来记录每个线程中执行的位置,便于线程在切换执行时的记录位置,是线程私有的,生命周期与线程一致,运行时速度快,不会出现内存溢出
虚拟机栈:
栈是运行单位,存储一个个方法,当调用一个方法时,创建出一个栈帧,将方法中的信息存到栈帧中·
操作只有两个,调用方法入栈,方法执行完成后出栈,先进先出的结构,运行速度非诚快,仅次于程序计算器,当入站的方法过多时会出现栈溢出,(内存溢出),线程是独立的,不同线程之间的方法不能相互调用
栈帧的内部结构:
局部变量表:方法内声明的局部变量,方法参数
操作数栈:运算区域a+b
动态链接:调用的方法地址,自变量地址
本地方法栈:
本地方法 native修饰的方法,没有方法体,hashcode,read(),start(),arraycopy(),本地方法不是用Java语言写的例如操作系统方法
如果调用本地方法,那么本地方法在本地方法栈运行,也会出现内存溢出
堆:
创建对象,对象引用
堆空间,是java内存中的一块空间,主要用来存储对象,是jvm中空间最大的一块,是线程共享的
jvm启动,堆空间就创建了,大小已经确定,但是可以通过参数改变大小,这就是我们所说的jvm优化
物理上不连续,逻辑上连续,堆是垃圾回收的重点区域
问:堆的区分
答:新生代:伊甸园区,幸存者1,幸存者2
老年代
问:为什么分区?
答:不同的对象,生命周期不同,所以将不同的对象存储在不同的区域通过不同的垃圾回收算法进行处理,扬长避短
当一个对象刚刚创建,存放在伊甸园区
当垃圾回收进行回收时,会把伊甸园中存活的对象移到幸存者区
幸存者区有两个1和2
首先把伊甸园区存活的对象存在幸存者1区,当下一次垃圾回收时,会将伊甸园区和幸存者1区存活的对象移动到幸存者2区,清空幸存者1区,之后再次回收时会把伊甸园区和幸存者2区存活的对象移到幸存者1区,清空幸存者2区,交替执行
问:什么时候去老年区?
答:当垃圾回收每次对对象进行标记时,在对象头中有一块空间来回记录被标记的次数,在对象头中,记录分代年龄只有4个bit位空间,只能记录15次(最大上限为15)
堆各个空间比例
新生代与老年代默认比例是1:2,但是可以通过参数进行设置
伊甸园区和两个幸存者区的比例默认为8:1:1,液可以通过参数进行设置
一个对象经过15次的垃圾回收既然存活去老年代,最大是15次分代回收思想
jvm垃圾回收可以分为不同的区域进行回收
针对新生代会频繁回收,成为yongCG
较少回收老年代,称为oldCG
当调用System.gc(),老年代内存不足,方法区内存不足时,会触发FULL GC(整堆收集)
尽量避免整堆收集,整堆收集时,其他用户线程暂停的时间较长
堆的参数
设置整个堆的大小
各个区间的比例
对象年龄
字符串常量池
jdk7之后的字符串常量池从方法区中转移到了堆中
因为方法区只有在触发了FILL GC时才会回收,回收率低,所以将字符串常量池转移到堆中,提高回收效率
方法区
存储类信息
类信息:方法,属性,静态常量,静态变量,即时编译器编译后的代码,运行时常量池
方法区是线程共享的,也可能会内存溢出,也会涉及到垃圾回收
方法区生命周期也是虚拟机启动就创建,虚拟机关闭就销毁
方法区是会涉及到垃圾回收的:
主要回收的是静态常量,类信息
类信息何时被卸载,满足三个条件
1.该类产生的对象都被回收
2.该类产生的Class对象不再被其他地方引用
3.该类对应的类加载器也被回收
本地方法区
问:什么是本地方法?
答:使用native的方法,不是JAVA语言实现的,是操作系统实现的,通过本地方法接口模块
问:为什么使用Native Method?
答:1.例如获取硬件的一些信息,如内存地址,启动线程IO,调用本地方法接口就很方便
2.jvm底层进行字节码解释编译,也有用C语言实现
执行引擎
作用:负责装载字节码文件到执行引擎中,字节码文件不是机器码,只是jvm规范中定义的指令码,执行引擎需要将字节码解释/编译为不同平台识别的机器码
hello.java——jdk 编译工具 javac——.class称为前端编译
.class执行引擎 编译为机器码 称为后端编译
解释器:jvm运行程序时,运行时字节码指令进行翻译,效率低
(即时)JIT编译器:对某段代码整体编译后执行,效率高,编译需要一段时间
问:为什么是半编译半解释型?
答:起初java中只提供了解释执行的方式,但是解释执行效率低,后来引入编译器,可以对程序执行中的热点代码进行编译,并把编译后的内容缓存起来,后期执行效率高,热点代码采用计数器方式来记录,程序启动后可以通过解释器立即对代码进行解释执行,不需要等待编译,提高响应速度,之后对热点代码采用编译器编译执行从而提高后续效率
垃圾回收GC
概论:Java语言是提供自动垃圾回收功能的,C++没有自动垃圾回收,垃圾回收也不是java首创的,Java在垃圾回收这里不断地在更新升级
问:什么是垃圾?
答:没用任何引用指向的对象就是垃圾
问:为什么要回收?
答:垃圾对象如果不回收,会一直占用内存空间,垃圾对象越来越多可能会导致内存溢出,对内存碎片进行整理,如果不整理,需要存储像数组这样的对象
问:早期是怎么回收的?
答:早起像C++代码,需要程序员手动销毁垃圾对象,可能会忘记删除,导致内存溢出
内存溢出:内存不够用,报内存溢出错误,OOM
内存泄露:有些对象已经不能被使用,但垃圾回收对象还不能回收他,这种悄悄占用资源的方式叫内存泄漏
现在的语言都引进了自动的内存管理
优点:降低了程序员的工作量,降低了内存溢出和内存泄露的风险
缺点:自动垃圾回收降低了程序员对内存管理的能力,一旦出现问题无从下手
问:哪些区域需要垃圾回收?
答:方法区,堆。频繁回收新生代,较少回收老年代,几乎不回收方法区
垃圾回收算法
标记阶段算法:标记那些对象已经是垃圾
引用计数算法(没有使用的)
Object obj =new Object();//对象内部有一个计数器,有一个引用指向 计数器+1
Object o=obj;//有有一个引用指向,计数器+1
obj=null;//计数器-1
o=null;//计数器-1
优点:实现简单
缺点:不能解决循环引用问题,需要维护计数器空间,赋值后要对计数器进行更新操作
可达性分析算法(根可达算法)
List list =new ArrayList();
while(true){
list.add(new String());
}
实现思路:从一组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中氛围中类不同的垃圾回收器,可以根据使用的场景选择对应的垃圾回收器
垃圾回收器分类为
按照线程数量分类为:
1.单线程垃圾回收器:垃圾回收线程只有一个,适合小型场景
2.多线程垃圾回收器:垃圾回收线程有多个同时执行,效率高
按照工作模式分类为:
1.独占式:垃圾回收线程工作时,用户线程全部暂停,STW
2.并发式:垃圾回收线程执行时,用户线程不用暂停,从CMS这款垃圾回收器引用了并发执行
按照内存工作区域分为:
1.新生代区域的垃圾回收器
2.老年代区域的垃圾回收器
CMS(Cincurrent Mark Sweep),并发标记清除
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是黑色,不会再次从A开始扫描,就会将C清理
解决:就是将发生的关系进行记录,重新标记
G1
G1也使用了并发标记和清除的
将整个堆的每个区域又划分成为更小的区间,回收时可以根据每个区间的优先级(由里面的垃圾数量),先回收优先级高的区间
降低了用户线程的停顿,提高了吞吐量,对整堆进行统一的管理,没有新生代和老年代