JVM的初步学习
一、jvm概述
面试需要
二、jvm作用
jvm负责把编译后的字节码翻译成机器码。
三、jvm内部构造
类加载部分:
负责把硬盘上的字节码加载到内存中
运行时数据区:
负责存储运行时产生的各种数据,类信息、对象信息、方法信息...
执行引擎:·
负责将字节码转为机器码
本地方法接口:
调用本地方法,启动线程start0() Object中的hashCode()--默认拿对象内存地址
public native int hashCode(); private native int read0() throws IOException;
垃圾回收部分
类加载系统
类加载系统负责将硬盘上的字节码文件加载到jvm中,生成类的Class对象,存储在方法区
类就是有一个模板,获得类的信息,都是通过class对象获得的
1、类加载过程
1.加载
以二进制文件流进行的读取
在内存中为类生成class对象
2、链接
验证:验证字节码的结构是否正确
准备:为类中的静态属性分配内存,进行初始赋值
public static int n=88;在准备阶段后n的初始值为0而不是88,(final修饰的static的常量,在编译时就进行了初始化)
解析:将类中的符号引用转换为直接引用(把字节码的符号引用(就是字节码中的逻辑符号)替换成内存的直接引用地址)
3、初始化
为类中的静态变量赋予正确的初始值
初始化是为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,
主要对类变量进行初始化。初始化阶段就是执行底层类构造器方法<clinit>()的
过程。
类加载执行完初始化阶段,才说明类加载完成了
类在哪些情况下被加载?
1、调用类中的静态成员(变量 、方法)
2、new 类的对象
3、在类中执行main()
4、反射加载类 Class c= class.forName("地址");
4、子类加载父类也会被初始化
类在以下俩种情况下是不会被加载的
1、只是访问类中的静态的常量(final),不加载整个类了。
2、类作为数组类型:
2、类加载器
类加载器就是实际负责读取类的功能
类加载器分类:站在jvm角度
1、引导类加载器:(启动类加载器 BootStrap ClassLoader)
使用c/c++写的,负责读取加载java底层的系统库
2、java写的类加载器(用来读取我们写的应用程序)
再细分类加载器
1、启动类加载器
c/c++语言实现。负责 加载Java核心类库(系统库java.lang)
2、扩展类类加载器
用Java语言实现的继承ClassLoader类,加载jre下面扩展类的(jre/lib/extd的目录下)
3、应用程序类加载器
用java语言实现的继承ClassLoader类,加载我们自己开发的应用程序类
3、双亲委派机制
当加载一个类时,总是先让他的父级加载器去加载,确保把系统中的类优先加载。直到父类加载器找不到类时,在逐级向下让子级类加载器加载,如果子级加载器也加载不到,最终抛出异常。
为什么这样做?
防止我们自己写的类替换了系统中的类,优先加载系统类
如何打破双亲委派机制
自己定义类加载器
jvm运行时数据区
存储运行时产生的各种数据
1、程序计数器
程序计数器用来记录每一个线程执行的指令位置(因为cpu要切换线程),他的速度是最快的,是线程私有的(每一个线程都回有一个程序计数器)
生命周期和线程生命周期一样,不存在内存溢出
此区域不会出现内存溢出(不够用)、也不会出现垃圾回收。
2、虚拟机栈
栈是运行的结构,程序方法执行,在虚拟机中,运行我们Java自己写的方法,调用方法,方法入栈,运行结束方法出栈,一个方法就是一个栈帧,在栈帧中存储局部变量,运行结果....
栈顶的方法称为当前栈帧
虚拟机栈也是线程私有的,线程之间互相隔离,栈区域不存在垃圾回收,但是会出现内存溢出问题
栈帧中存储内容:局部变量**Local Variables** int a=10
操作数栈**Operand Stack**(计算过程) int c=a+b
方法返回地址**Retuen Address**:当一个方法执行完之后,要返回之前调用它的地方,因此栈中必须保存一个方法返回地址
3、本地方法栈:
本地方法栈是用来调用本地方法的。
线程私有的,不会存在垃圾回收,会出现内存溢出的问题
4、堆
1、堆的概述
堆的作用是用来存储Java语言产生的实例化对象的,是运行是数据区中最大的一块内存空间,空间大小可以设置,对空间是所有线程共享的,堆空间是垃圾回收的重点区域,堆中没有被使用垃圾对象会被垃圾回收器回收。
2、堆空间的区域划分
堆分为
新生区(新生代、年轻代):
包含伊甸园区:存放新创建的对象
幸存者0区:伊甸园区经历垃圾回收时,将存活的对象转移到某一个幸存者区(俩个幸存者区每次都会空闲一个)
幸存者1区
老年区(老年代):当对象经历15次垃圾回收后仍然存活或者内存较大时(为了减少移动次数)转移到老年代
为什么是15次?
因为在对象头中有4个bit为,每一次垃圾回收时会记录一次最大为1111,所以是15次,也可以设置参数,最大值为 15
可以设置大小,最大为15次。
-XX:MaxTenuringThreshold=<N>
3、为什么要分代分区?
可以将不同生命周期的对象存储在不同的区域针对不同的区域执行不同的垃圾回收算法,使得垃圾回收策略更加的优化。
比如将存活时间久的对象存储到老年代区域中,从而减少垃圾时间扫描及GC频率。
4、对象创建及存储过程
新创建的对象都存储在伊甸园区,当垃圾回收时,将还被使用的对象,转移到某一个幸存者区,将伊甸园区垃圾对象移除,当下次垃圾回收时,将伊甸园区存储获得的对象与当前正在使用的幸存者区的存活的对象一起存储到另一个幸存者区,(每一次都会空闲一个幸存者区),当一个对象经历过15次垃圾回收仍然存活,那么就把该对象移动到老年代,老年代比较少的垃圾回收,在老年代空间不足时,会对老年代进行垃圾回收,那么就会触发FULL GC(整堆收集,应尽量避免)当整堆收集后,内存仍然不足时,就会产生OOM异常(Java.lang.OutOfMemoryError:Java heap space),内存溢出错误
5、jvm调优
可以根据程序具体的使用场景,对运行时数据区的各种空间大小进行调整。例如方法区、堆、对垃圾回收器的调整
官网地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial
查看所有参数的默认初始值
-Xms:初始堆空间内存
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC 处理日志
方法区
1、概念:
是一个被线程共享的内存区域。存放加载类中的各种信息,
方法区的大小也是可以设置的 ,
方法区也会进行垃圾回收,
方法区也会可能出现内存溢出的问题
2、方法区的垃圾回收
方法区的垃圾回收,是对类信息进行回收。
类信息如果不再被使用,类信息额可以被卸载
卸载条件:
1、该类所产生的对象不存在了
2、加载该类的加载器也被回收了
3、该类的Class对象也不再被使用
本地方法接口
是虚拟机中专门用来调用本地方法的接口
1、什么是本地方法?
在Java中native关键字修饰的方法,没有方法体。是c++在操作系统底层实现的方法
如:Object类的 hashCode()获取对象的内存地址,涉及到读取内存。
IO中读文件(输入文件,操作硬盘) read0(),
启动线程 start0();启动线程就是把这个线程注册到本地操作系统
关键字 native 可以与其他所有的 java 标识符连用,但是 abstract 除外,native和abstract都没有方法体
2、Jav1a中为什么要调用本地方法?
因为Java属于应用层语言,有时会需要对硬件的系统资源进行调用,此时就不方便,在一个就是系统资源不允许应用层程序直接调用,那么就需要通过本地方法调用操作硬件资源
执行引擎
1、概念
执行引擎是虚拟机核心部件之一,主要作用是将加载到的虚拟机的字节码再一次的转换为机器码(字节码并不是系统能够直接执行的机器码)
执行引擎可以通过解释/编译俩种方式,实现字节码转为机器码
2、Java程序执行过程中涉及俩次编译:
第一次 .java(源代码 通过jdk javac调用编译器)-->.class文件 称为前端编译。
第二次:通过执行引擎 将字节码 编译为 机器码 称为后端编译
3、将字节码转为机器码的 俩种方式:
解释器(解释执行):对字节码逐行解释翻译,重复性代码也是每次都要解释执行,效率低。
编译器(编译执行):对某一段字节码进行整体编译,然后存储起来,以后使用时不在需要编译,效率高
编译器会针对执行过程中的热点代码进行编译,并缓存起来。
4、什么是解释器?什么是 JIT 编译器?
解释器:当 Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT(Just In Time Compiler)编译器:就是虚拟机将源代码一次性直接编译成和本地机器平台相关的机器语言,但并不是马上执行。
5、为什么要是有半编译执行和半解释执行?
程序开始运行时,解释器可以立即发挥作用,投入使用,
而编译器虽然执行效果高,但是前期需要对热点代码进行跟踪和编译,需要消耗时间。
垃圾回收
1、什么是垃圾对象?
就是一个对象没有被任何引用所指向
String s=new String(); s=null;//此时s就是垃圾对象
垃圾对象如果不清理,新的对象没有足够的空间,可能会导致内存溢出问题
2、垃圾回收发展
早期c/c++这类语言,内存管理都是手动,使用时申请,使用完后手动释放。
优点:对内存管理更加准确,效率高
缺点:层架程序负担,控制不好容易出事(忘记释放,误操作内存空间)
后来发展为自动回收:
Java,c#...都采用自动垃圾回收
优点:解放了程序员
缺点:占用一定的内存空间(垃圾不是出现后立即回收的,是周期性的),降低程序员管理内存能力。
3、哪些区域会出现垃圾回收
堆:对象 频繁回收年轻代,较少回收老年代
方法区:类信息卸载 整堆收集时,会进行回收 FULL GC
4、内存溢出与内存泄漏
内存溢出 内存不够用了
内存泄漏:系统中用不到的,但是又不能回收的对象
案例:单例对象、数据库连接对象、IO流、socket这些提供close()的类,用完之后,如果没有关闭,垃圾回收器是不能主动回收这些对象。
内存泄漏,虽然不能直接触发内存溢出,但是长期有对象不能背后收,也是导致内存溢出的原因之一
5、Stop the Word
垃圾回收时,会经历俩个阶段:一是:标记阶段,二是:回收阶段,在标记和回收时,需要我们的用户线程暂停,不暂停标记和回收时可能会出现错标和漏标
6、垃圾回收阶段算法
垃圾标记阶段
将虚拟机中不再被任何引用指向的对象标记出来,在垃圾回收阶段,就会将标记的对象进行回收
垃圾标记阶段相关算法:
引用计数算法
(存在缺陷,没有被虚拟机所使用的)
设计思想:在对象中维护一个整数计数器变量,当有引用指向对象时,计数器加一,相反减一(引用断开)
优点:设计实现简单,容易分辨对象是否是垃圾对象。
缺点:需要维护一个变量存储引用数量,频繁修改引用计数器变量,占用空间,还耗时,最重要的是,无法解决循环引用问题,造成内存泄漏。
可达性分析算法
(根搜索算法)
设计思想:从一些可以被称为GCRoots的对象开始向下查找,只要某一个对象与GCRoots对象有联系,可以判断对象被使用,与根对象引用链没有任何关系的对象,可视为垃圾对象。
哪些对象可以作为GCRoots(根对象)?
1、虚拟机栈中(被调用的方法)所使用的对象。
2、类中的静态属性。
3、虚拟机中使用的系统类对象
4、所有被同步锁 synchronized 持有的对象
基 本 数 据 类 型 对 应 的 Class 对 象 , 一 些 常 驻 的 异 常 对 象 ( 如 :
NullPointerException、OutofMemoryError),系统类加载器。
对象的finalize()机制
Object类中的finalize()方法,这个方法时在对象被回收前,由虚拟机自动调用的,在对象被回收前,需要执行的一些操作,就可以在此方法中编写,
finalize()方法可以在子类中重写,
finalize()方法自会被调用一次(第一次被判定为垃圾,要对其回收,调用finalize,对象可能又被引用了,对象就不能被回收,当下一次被判定为垃圾对象时,就不会在调用finalize())
不主动调用finalize()方法原因?
1、调用finalize()方法可能会导致对象复活。
2、finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发送GC,则finalize()方法不会调用。
3、一个糟糕的finalize()会严重影响GC的性能。如finalize()是一个死循环。
由于finalize()方法存在,被标记的垃圾对象,也不是非死不可的,
jvm中可以将对象分为三种状态:
可触及:被GCRoots引用的,不是垃圾对象
可复活的:被判定为垃圾对象,但是finalize()方法没有被调用过的
不可触及的:被判定为垃圾的,finalize()已经调用过了
7、垃圾回收阶段算法
1、标记—复制算法
将内存可以分为多个较小的块,当发生垃圾回没有将一个区域中存活的对象复制到另一个区域,在另一个区域从头开始排列,清除当前垃圾区域。
优点:清理之后内存没有碎片
缺点:回收时需要移动对象,所以适合小内存块,而且存活对象较少的情况
适合新生代。
2、标记-清除算法
将被标记的垃圾对象地址进行记录,后面如果分配新对象,判断垃圾对象空间是否能够存储新的对象,如果可以存储下,用新的对象直接覆盖垃圾对象即可存活的对象不需要移动。
清除:这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放(也就是覆盖原有的地址)。
优点:对象不需要移动
缺点:内存总会出现不连续的内存碎片
3、标记-压缩(整理)算法
将存活的对象移动到内存区域的一端,按照顺序进行排列(压缩),清理边界以外的空间,在标记清除的基础上进行一次内存整理。
优点:回收后没有内存碎片
清除和压缩对比
清除:不移动存活对象
压缩:会移动存活对象
两者都适合老年代对象回收,
显示有标记—清除,当老年代空间不足是,或者不能存储较大的对象时,在使用标记-压缩算法
垃圾回收时,根据不同的分区采用不同的回收算法
新生代:标记-复制算法
老年代:标记-清除和标记-压缩混合使用
8、垃圾回收器
1、什么是垃圾回收器
垃圾回收器是对垃圾回收过程的实践者。
不同的虚拟机中,垃圾回收器的种类也是有很多的
2、有哪些垃圾回收器以及特点
垃圾回收器分类:
线程数量上分:
单线程:垃圾回收线程只有一个
多线程:有多个垃圾回收线程
工作模式上分:
独占式:垃圾回收线程执行时,用户线程需要暂停(stop the word)
并发式:垃圾回收线程和用户线程可以并发(同时)执行
分区的角度上分
新生代垃圾回收器
老年代垃圾回收器
3、垃圾收集器性能指标
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运
行时间+内存回收的时间)
用户线程暂停时间(重点):执行垃圾收集时,程序的工作线程被暂停的时间。
回收时内存开销:垃圾收集所用时间与总运行时间的比例。
Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old,CMS,G1
CMS:并发标记清除收集器
多线程老年代收集器,开创了垃圾收集线程与用户线程并发执行的先例
过程:
初始标记--独占执行
并发标记--并发执行
重新标记--独占执行
并发清除--并发执行
G1(Garbage First)回收器
G1垃圾回收器,继承了CMS中垃圾收集线程和用户线程并行的特点,减少了用户线程暂停的时间。
同时,将新生代和老年代的各个区域又划分成多个更小的区域,对每一个区域进行跟踪,优先回收垃圾多的区域,例如(可以把eden区划分成多个小的eden区域),提升回收效率,提高了吞吐量。不在区分年轻代和老年代,可以做到对整个堆进行收集
非常适合服务器端程序,大型项目
4、设置垃圾回收器
打印默认垃圾回收器
-XX:+PrintCommandLineFlags -version
JDK 8 默认的垃圾回收器
年轻代使用 Parallel Scavenge GC
老年代使用 Parallel Old GC
打印垃圾回收详细信息
-XX:+PrintGCDetails -version
设置默认垃圾回收器
Serial 回收器
-XX:+UseSerialGC 年轻代使用 Serial GC, 老年代使用 Serial Old GC
ParNew 回收器
-XX:+UseParNewGC 年轻代使用 ParNew GC,不影响老年代。
CMS 回收器
-XX:+UseConcMarkSweepGC 老年代使用 CMS GC。
# G1 回收器
-XX:+UseG1GC 手动指定使用 G1 收集器执行内存回收任务。
-XX:G1HeapRegionSize 设置每个 Region 的大小。