一、概念
1、什么是JVM?
Java虚拟机,是Java程序的运行环境(Java二进制字节码的运行环境)
2、JVM的好处
(1)一次编写,到处运行,屏蔽了字节码文件和底层操作系统的差异,对外提供一个一致的运行环境。
(2)自动内存管理,提供了垃圾回收功能,减少了程序员手动释放内存的步骤
(3)提供了数组下标越界检查,因为下标越界可能会覆盖其他的内存空间
3、比较JDK、JRE、JVM
JVM是一种规范,我们使用的JVM是HotSpot
二、结构分析图
三、JVM内存结构
1、程序计数器(寄存器)(线程私有)
作用:程序计数器是用于存放下一条指令所在单元的地址的地方
特点:①线程私有;②不会存在内存溢出问题;
2、虚拟机栈(线程私有)
(1)作用
线程运行需要的内存空间
(2)组成
多个虚拟栈帧组成(一个栈帧对应一次方法的调用,每个方法运行需要的内存)
每个线程只能由一个活动线程,对应着当前正在执行的方法
(3)存放的东西
方法参数,方法局部变量,返回地址
(4)问题分析
1)垃圾回收是否涉及栈内存?
不涉及,因为方法调用完,内存空间就自动释放掉了,不需要进行垃圾回收
2)栈内存(默认是1024KB)分配越多越好吗?
不是,栈内存越多,可以进行更多次的方法递归调用;占用内存越多,线程数目就会越少,降低了吞吐量;
3)方法内的局部变量是否线程安全?
首先要确定局部变量是线程共享的还是线程私有的:
①如果是线程私有的,就不会存在线程安全问题;
②如果是线程共享的,就会存在线程安全问题;
③在局部变量前加上关键字"static",就被定义成为一个静态局部变量。静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。
如果局部变量逃离了方法,它就不是线程安全的,比如,参数作为返回值返回会被其他线程操作;还有,参数作为实参传入进来,此参数被其他方法调用,也是线程不安全的;
(5)栈溢出问题
①递归调用,没有结束条件;②循环引用,对象1的属性包含对象2,对象2的属性包含对象1;
(6)CPU占用过多问题
定位问题:
①用top命令定位到占用内存较多的进程,确定进程编号
②ps H -eo pid(进程),tid(线程),%cpu | grep(筛选) 进程编号 ---定位到线程
③jstack 线程id(16进制) ---查看运行的类,运行的方法,问题代码行数
(7)程序运行很长时间没有结果问题
定位问题:
① jstack 线程id(16进制)
② 查看"Found one Java-level deadlock"
③ 查看死锁的两个线程,以及死锁的代码行数
3、本地方法栈(线程私有)
非Java方法,一般是C或是C++写的方法
4、堆(线程共享) Heap
(1)定义
通过new 关键字,创建的对象都会使用堆内存
(2)特点
线程共享,对中对象都要考虑线程安全的问题
有垃圾回收机制
(3)堆内存(默认是1GB)溢出
堆内存诊断
1)jps工具
查看当前系统中有哪些进程
2)jmap工具
查看堆内存占用情况
3)jconsole工具
4)Java VisualVM
5、方法区
(1)定义
存储已被Java虚拟机加载的类信息(构造器,成员方法)、类加载器,运行时常量池(StringTable)、常量、静态变量、即时编译器编译后的代码
类加载器可以用来加载类的二进制字节码
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息;
运行时常量池:常量池时*.class文件中,当该类被加载,它的常量池信息就会被放入运行时常量池,并把卢木安的符号地址变为真实地址
面试题:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
System.out.println(s3 == s4); // false String s4 = new String("ab");
System.out.println(s3 == s5); // true
四、垃圾回收
1、如何判断对象可以被回收
(1)引用计数法
原理
·引用计数法(Reference Counting)比较简单,对每一个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
·对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象的引用计数器的值为0,即表示对象A不能在被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:1)他需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2)每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3)引用计数器还有一个严重的问题,即无法处理循环引用的问题,这是一条致命的缺陷,导致在Java回收的垃圾回收器中没有使用这类算法。
(2)可达性分析算法(根搜索算法、追踪性垃圾收集)
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
实现原理
1)可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
2)使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。
3)如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
4)在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
(3)GC Roots 可以是哪些
1)虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。
2)本地方法栈内 JNI(通常说的本地方法)引用的对象
3)方法区中类静态属性引用的对象
比如:Java类的引用类型静态变量
4)方法区中常量引用的对象
比如:字符串常量池(string Table)里的引用
5)所有被同步锁 synchronized 持有的对象
6)Java虚拟机内部的引用。
基本数据类型对应的 Class 对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
7)反映 java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
(4)四种引用
1)强引用
只有所有GC Roots对象都不通过【强引用】引用改对象,该对象才能被垃圾回收
Person person = new Person();
2)软引用
没有强引用,当垃圾回收,内存不够时,软引用所引用的对象会被释放掉(回收)
3)弱引用
没有强引用,当垃圾回收,弱引用所引用的对象会被释放掉(回收)
4)虚引用
必须配合引用队列使用
5)终结器引用
第一次垃圾回收的时,会创建终结器引用入引用队列,第二次垃圾回收的时候才会调用finalize()方法,回收对象
2、垃圾回收算法
1)标记清除
标记:将没有引用的对象进行标记,其可以作为垃圾回收
清除:将标记的对象占用的空间进行释放(将对象占用的起始和结束内存地址记录到空闲的地址列表就行,分配新对象就会去这个地址列表去找一块空间存放对象)
优点:速度快
缺点:造成空间不连续,会产生内存碎片
2)标记整理
标记:将没有引用的对象进行标记,其可以作为垃圾回收
整理:将标记的对象占用的空间进行释放,并将存活对象的内存地址进行挪移,对象整理成存储在一段紧凑的内存地址上
优点:不会存在内存碎片
缺点:速度慢
3)复制
①将没有引用的对象进行标记,其可以作为垃圾回收
②将标记的对象占用的空间进行释放,并将存活对象的复制到另外一块内存地址上,后面将原来地址上的对象都删除,最后将上面两块内存地址进行交换,对象还是在之前的内存地址上
优点:不会产生内存碎片
缺点:会占用双倍的内存空间
3、分代垃圾回收
新生代(一个E区,两个S区,S1,S2) 老年代(存放大对象和S2中存活次数超过6的对象)
1)对象首先分配在伊甸园(E区)区域
2)新生代空间不足时,触发 Minor GC,伊甸园和S1(from)存活的对象使用copy复制到S2(to)中,让存活的对象年龄+1,并且交换S1和S2
3)Minor GC会引发stop the world,时间较短(用户线程阻塞,垃圾回收线程完成,用户线程才能继续运行)
4)当对象寿命超过阈值时,会晋升到老年代,最大寿命是15(4bit)
5)当老年代空间不足,会尝试触发Minor GC,如果内存还是不足,会触发Full GC,也会引发stop the world,时间较长
前期内存空间较多,垃圾回收都是Minor GC(只清理新生代)
后期新生代和老年代的内存空间都不足的时候,垃圾回收是Full GC(新生代和老年代都会清理)
4、垃圾回收器
5、垃圾回收调优