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