目录
jvm概述
为什么学习jvm
高级程序员了解需要。
jvm作用
jvm负责把编译后的字节码转换为机器码。
jvm整体组成部分
类加载部分:把硬盘上的字节码加载到内存中(运行时数据区)。
运行时数据区:存储运行时产生的各种数据,类信息,对象信息...
执行引擎:负责将字节码转为机器码。
本地发放接口:调用本地方法 。(启动线程 start0())
垃圾回收器:
jvm类加载系统
负责将硬盘上的字节码文件加载到jvm中,生成类的class对象,存储在方法区;
类就是一个模板;
类加载过程
1、加载:
以二进制文件流进行读取,在内存中生成类的class对象
2、链接:
验证:验证字节码的结构是否正确
准备:为类的静态属性进行初始化的赋值
解析:把字节码的符号引用 替换成 内存中的直接引用地址
字节码查看:javap -verbose Demo.class
3、初始化
类初始化阶段主要是为类中的静态成员进行赋值;
因为类加载执行完初始化阶段才说明类加载完成了。
类在哪些情况下会被加载
- 调用类中静态成员(变量,方法)
- new类的对象
- 在类中执行main()
- 反射加载类 class.dorName("地址")
- 子类被加载会带动父类被加载
类不会被加载
1、类作为数组类型
2、只是访问类中静态的常量
类加载器分类
类加载器是负责读取类的功能
站在jvm角度上
引导类加载器(不是用java写的,是用c/c++),负责读取java中底层数据库
java写的类加载器(用来读取我们写的应用程序)
再细分类加载器
1、启动类加载器
c/c++语言实现,负责加载Java核心类库(系统库 Java.lang)
2、扩展类加载器
用Java语言来实现的,继承自classloader,加载jre下面扩展类的jre/lib/ext目录
3、应用程序类加载器
用Java语言实现的,继承classloader类,用来加载我们自己开发的应用程序类
双亲委派机制
当加载一个类的时候,它总是先向上加载它的父级类加载器,确保把系统中的类优先的加载;
直到父级的类找不到时,再逐级向下,让子级类加载器加载;
如果子级也找不到,最终抛类找不到异常。
防止我们自己写的类替换了系统中类。
如何打破双亲委派机制
自定义加载器
jvm运行时数据区
概述
存储运行时产生的各种数据
程序计数器
程序计数器用来记录每一个线程执行的指令位置;
速度是最快的,而且是线程私有的(每一个线程都会有一个计数器);
此区域不会出现内存溢出(不够用),也不会出现垃圾回收。
java虚拟机栈
栈是运行的,解决程序方法执行,在虚拟机栈中是用来运行Java自己写的方法的;
调用方法,方法入栈,运行结束出栈(先进后出,栈顶的方法,成为当前栈帧);
虚拟机栈是线程私有的,线程之间互相隔离;
一个方法就是一个栈帧,在栈帧中存储局部变量,运行结果......
栈区域不存在垃圾回收,但是会存在内存溢出问题。
栈帧中存储什么内容
1、局部变量表
int a = 10;
int b = 10;
2、操作数栈
int c = a + b;
3、方法返回地址
本地方法栈
本地方法栈是用来执行调用的本地方法的。
是线程私有的,不会存在垃圾回收;
会出现内存溢出的问题。
堆
堆的作用是用来存储Java语言产生的对象的;
是运行时数据区中最大的一块内存空间,空间大小可以设置;
堆空间是所有线程共享的;
堆空间是垃圾回收的重点区域,堆中没有被使用到的垃圾堆下,会被垃圾回收器回收掉。
堆空间区域划分
新生区(新生代 年轻代):
伊甸园区:
幸存者0区:
幸存者1区:
老年区(老年代):
为什么分区(代)
可以将不同生命周期的对象,存储再不同的区域,针对不同的区域采取不同的垃圾回收算法,使得垃圾回收策略更加优化。
对象创建内存分配过程


新创建的对象都存储在伊甸园区,当垃圾回收时,将还被使用的对象转移至某一个幸存者区,将伊甸园区垃圾对象进行清除,当下一次垃圾回收时,将伊甸园区存活的对象与当前正在使用的幸存者区存活的对象,转移到另一个幸存者区(每一次会空闲幸存者区)。
当一个对象经历过15次垃圾回收后,仍然存活,那么就把该对象转移到老年代。;
老年代比较少进行垃圾回收,在老年代空间不足时,会进行垃圾回收;
当回收后内存仍然不足时,会触发FULLGC(整堆收集 应尽量避免);
当整堆收集后,它仍然不够使用,那么就会出现内存溢出错误 ---OOM(OutOFMemoryError)
jvm调优
可以根据程序具体的使用场景,对运行时数据区的各种空间大小进行调整 例如堆、方法区。
对垃圾回收器进行选择
方法区
方法区主要用来存储加载的类信息
方法区的大小是可以设置的
方法区也会进行垃圾回收,方法区也可能出现内存溢出问题
方法区的垃圾回收
方法区的垃圾回收,是对类信息进行回收的
类信息不再被使用,类信息也可以被卸载
卸载条件
该类产生的对象不存在了
该类的class对象,也不再被使用了
加载给类的类加载器也被回收了
本地方法接口
是虚拟机中专门用来调用本地方法的接口。
什么是本地方法
在Java中被 native关键字 修饰的方法,没有方法体,不是用Java语言实现的方法,用c/c++在操作系统底层实现的方法。
没有方法体:void a(); abstract方法也没方法体。
eg:
Object hashCode() 获取对象内存地址 涉及到读取内存;
IO中读文件(输入文件 操作硬盘) read0();
启动线程 native void start0(); 启动线程,就是把这个线程注册到操作系统。
Java中为什么要调用本地方法
因为Java属于应用层(上升)语言,有时候,需要对硬件系统资源进行调用;
此时就不方便,在一个系统资源不允许应用程序直接调用;
直接调用可能会造成改写底层,影响较大。
那就需要通过本地方法 调用操作硬件资源。
执行引擎
执行引擎是虚拟机核心部件之一;
主要作用是将加载到虚拟机中的字节码 再次 转换为机器码(字节码并不是系统能够直接执行的机器码)。
执行引擎可以通过解释/编译两种方式 实现字节码转为机器码。
Java程序执行过程中涉及两次编译
第一次 hello.java(源代码 通过jdk javac调用编译器)--->hello.class文件 称为前端编译;
第二次 通过执行引擎 将字节码 编译为 机器码 称为后端编译。

将字节码转换为机器码有两种方式:
解释器(解释执行):对字节码逐行进行解释翻译,重复性代码,也是每次都要解释执行,效率低。
编译器(编译执行):对某段字节码进行整体编译,然后存储起来,以后使用时不需要编译了,效率高。
编译器会针对执行过程中的热点代码进行编译,并缓存起来。
为什么Java是半编译半解释执行
程序开始运行时,解释器可以立即发挥作用,投入使用;
而编译器虽然执行效率高,但是前期需要对热点代码进行跟踪和编译,需要消耗时间。
垃圾回收
什么是垃圾对象
指在运行程序中没有任何指向的引用对象(没有任何引用指向的对象)。
垃圾对象如果不清理,新的对象可能没有足够的空间,可能会导致内存溢出问题
垃圾回收发展
早期c/c++这类语言,内存管理是手动的,使用时申请,使用完后手动释放。
优点:对内存管理更加精确,效率高;
缺点:增加程序员负担,控制不好,容易出事(忘了释放,误操作内存空间)。
后来就发展为自动回收:
java,c#...都采用自动垃圾回收
优点:解放程序员;
缺点:会占用一些内存空间(垃圾不是出现后立即回收的),降低了程序员管理内存的能力。
哪些区域会出现垃圾回收
堆 产生的对象 频繁回收年轻代,较少回收老年代
方法区 类信息卸载 整堆收集时,会进行回收 FULL GC
内存溢出与内存泄漏
内存溢出:
内存不够用了
内存泄漏:
系统中哪些用不到的,又不能回收的对象
eg:
单例对象;
数据库连接对象,IO流,socket这些提高close()的类 用完之后,没有关闭,垃圾回收器,是 不能主动回收这些对象的。
内存泄漏,虽然不会直接除法内存溢出,但是长期有对象不能被回收,也是导致内存溢出的原因之一。
Stop the World
垃圾回收时会经历两个阶段:一是标记阶段,二是回收阶段,在标记和回收时,需要我们的用户线程暂停,不暂停标记和回收时肯会造成错标和漏标。
垃圾回收阶段算法
1、垃圾标记阶段
将虚拟机中不再被任何引用指向的对象标记出来,在垃圾回收阶段,就会将标记对象进行回收
垃圾标记阶段相关算法:
引用计数算法
存在缺陷,没有被虚拟机所使用。
设计思想:在我们的对象中维护一个整数计数器变量,当有引用指向对象时,我们的计数器加一,相反就减一(引用断开)。
优点:设计实现简单,容易分辨我们的对象是否是垃圾对象。
缺点:需要维护一个变量存储引用数量,需要频繁的修改引用计数器变量,占空间,还耗时;
最重要的是无法解决循环引用问题。
可达性分析算法(根搜索算法)
设计思想:从一些可以被称为GCRoots的对象开始向下查找,只要某一个对象与GCRoots对象有联系的,可以判断对象时被调用的,与GCRoots对象引用链没有任何关系的对象,可以视为垃圾对象。
哪些对象可以视为GCRoots(根对象)
1、虚拟机栈中(被调用的方法)所使用的对象(活跃对象);
2、类中的静态属性;
3、虚拟机中使用的系统类对象。
对象的finalization机制
Object类中有一个finalize()方法,这个方法是在对象被回收之前,由虚拟机自动调用,在对象被回收前需要执行的一些操作就可以在此方法中编写。
finalize()可以在子类中重写。
finalize()方法只会被调用一次(第一次被判定为垃圾,要对其回收,调用finalize(),对象有可能又被引用了,对象就不能被回收,当下一次被判定为垃圾对象时,就不会再调用finalize())。
复活后然后呢?
由于finalize()方法的存在,被标记为垃圾的对象也不是非死不可的。
可以将对象分为三种状态:
可触及:被GCRoots引用的,不是垃圾对象;
可复活的:被判定为垃圾的,但是finalize()方法还没有被调用的;
不可触及的:被判定为垃圾的,finalize()已经被调用过了。
2、垃圾回收阶段
1、标记-复制算法(Copying)
将内存可以分为较小的块,当发生垃圾回收时,将一个区域中存活的对象复制到另一个区域,在另一个区域从头开始排列,清除当前垃圾回收的区域。
优点:清理之后,内存没有碎片;
不足:回收时,需要移动对象,所以适合小内存块,而且存活对象少的情况。
适合新生代。
2、标记-清除算法(Mark-Sweep)
将被标记为垃圾的对象的地址进行记录,后面如果分配新对象,判断垃圾对象的空间是否能够存储下新的对象,如果可以存储下,用新对象直接覆盖垃圾对象即可;
存活对象是不发生移动的。
优点:对象不移动;
不足:回收后内存中会出现碎片。
3、标记-压缩算法(Mark-Compact )
将存活的对象会移动到内存区域的一端,按顺序排列(压缩),清除边界以外的空间;
在标记清除的基础上进行一次内存够管理。
优点:回收后没有内存碎片。
标记-清除和标记-压缩对比:
标记-清除:不移动存活对象;
标记-压缩:会移动存活对象;
两者都适合老年代对象回收。
先使用标记-清除,当老年代空间不足,或者不能再存储一个较大的对象,再使用标记-压缩算法。
总结
垃圾回收时,根据不同的分区采用不同的回收算法。
新生代:标记-复制;
老年代:标记-清除、标记-压缩。
垃圾回收器
什么是垃圾回收器
垃圾回收器是堆垃圾回收过程的一个实践者(落地)
不同的虚拟机中垃圾回收器的种类也是很多的
有哪些垃圾回收器
从线程数量上分类
单线程:垃圾回收器只要一个;
多线程:垃圾回收器有多个。
从工作模式上进行划分
独占式:垃圾回收线程执行时,其他用户线程需要暂停(stop the world);
并发式:垃圾回收线程和用户线程可以做到并发执行。
从分区角度上分:
新生代:
老年代:
垃圾收集器性能指标
吞吐量:
用户线程暂停时间(重点):
回收时内存开销:
常见垃圾收集器
Serial:单线程,新生代
Serial Old:单线程,老年代
ParNew:多线程新生代收集器
Parallel Scavenge:多线程新生代收集器
Parallel Old:多线程老年代收集器
CMS:
多线程,老年代收集器,开创了垃圾收集线程与用户线程并发执行的先例
并发标记清除收集器
初始标记--独占执行
并发标记--并发执行
重新标记--独占执行
并发清除-并发执行
G1:
G1垃圾回收器,继承了CMS中,垃圾收集线程和用户线程并行执行的特点,减少了用户线程暂停时间。
同时将新生代和老年代的各个区域又划分成多个更小的区域,对每一个区域进行跟踪,优先回收价值高的区域(垃圾多的区域,例如可以把伊甸园区分成好几个小的区域)。
提升回收效率,提高了吞吐量,不再区分年轻代和老年代,可以做到对整个堆进行回收。
非常适合服务器端程序,大型项目。
设置垃圾回收器
打印默认垃圾回收器
-XX:+PrintCommandLineFlags -version
打印垃圾回收详细信息
-XX:+PrintGCDetails -version
设置默认垃圾回收器
-XX:+UseG1GC