JVM详解
JVM
- 1.JVM的好处
- 2.JVM的内存结构
-
- (1)程序计数器
- (2)虚拟机栈
-
- 定义
- 问题辨析
- 栈内存溢出
- 线程运行诊断
- (3)本地方法栈
- (4)堆
-
- 堆内存溢出
- 堆内存诊断
- 垃圾回收之后,内存占用仍然很高
- (5)方法区
-
- 定义
- 内存溢出
- 运行时常量池
- StringTable(字符串常量池)
- (6)直接内存
-
- 直接内存释放原理
- 禁用显示回收对直接内存的影响
- 3.垃圾回收
-
- (1)如何判断对象是否可以被回收
-
- 引用计数法
- 可达性分析法
- 4.四种引用
-
- (1)强引用
- (2)软引用
- (3)弱引用
- (4)虚引用
- 5.垃圾回收算法
-
- (1)标记清除
- (2)标记整理
- (3)复制
- (4)分代回收
- (5)GC
-
- GC相关参数
- (6)垃圾回收器
-
- 串行垃圾回收器
- 吞吐量优先垃圾回收器
- 响应时间优先垃圾回收器
- (7)G1
-
- 新生代回收
- 新生代回收+并发标记
- 混合回收
- Full GC
- 新生代回收的跨代引用(老年代引用新生代)问题
- Remark
- 字符串去重
- 并发标记时的类卸载
- 回收巨型对象
- 并发标记起始时间的调整
- (8)GC调优
- 6.类加载
-
- (1)类文件结构
-
- 魔数
- 版本
- 常量池
- (2)字节码指令
-
- 将class文件中常量池信息载入运行时常量池
- 方法字节码载入方法区
- main线程开始运行 分配栈帧内存
- 执行引擎开始执行字节码
- (3)javap
- (4)类加载
-
- 加载
- 链接
- 初始化
- (5)类加载器
-
- 启动类加载器
- 扩展类加载器
- 应用程序类加载器
- 自定义类加载器
- 双亲委派
1.JVM的好处
(1)一次编译,到处运行(2)自动内存管理,垃圾回收功能(3)数组下标越界检查(4)多态
2.JVM的内存结构
(1)程序计数器
(1)记住下一条JVM指令的执行地址(2)使用寄存器做程序计数器(3)线程私有的(4)不会存在内存溢出
(2)虚拟机栈
定义
(1)每个线程运行时所需要的内存(2)每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存(3)每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
(1)垃圾回收不涉及栈内存,因为每一个方法对应一个栈帧,每个方法运行结束后,栈帧就会自动出栈(2)栈内存不是分配越大越好,因为栈内存分配越大,线程数量就会越少(3)方法内的局部变量是否线程安全?如果方法内局部变量没有逃离方法的作用范围,它是线程安全的如果局部变量引用了对象,并逃离了方法的作用范围,它就存在线程安全的风险
栈内存溢出
(1)栈帧过多导致栈内存溢出(方法递归调用时)(2)栈帧过大
线程运行诊断
(1)某个程序CPU占用过多第一步:用 top 定位哪个进程对CPU的占用过高第二步:用 ps H -eo pid,tid,%cpu | grep 进程id 来定位这个进程中哪个线程对CPU占用过高第三步:用 jstack 进程id ,可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号(2)程序运行很长时间没有结果第一步:jstack 进程id :划到最后可以看出可能是线程死锁
(3)本地方法栈
给本地方法运行提供内存空间
(4)堆
(1)通过new关键字创建的对象都会使用堆内存(2)是线程共享的,堆中对象都需要考虑线程安全的问题(3)有垃圾回收机制
堆内存溢出
public class TestOOM {public static void main(String[] args) {int i=0;try {List<String> list=new ArrayList<>();String a="hi";while (true){list.add(a);a=a+a;i++;}}catch (Throwable e){e.printStackTrace();System.out.println(i);}}}
堆内存诊断
(1)jps工具查看当前系统中有哪些java进程(2)jmap工具jmap -heap 进程id :查看堆内存占用情况(3)jconsole工具图形界面的,多功能的检测工具,可以连续监测
垃圾回收之后,内存占用仍然很高
(1)jps:查看进程id(2)jmap -heap 进程id :查看堆内存占用
(5)方法区
定义
(1)线程共享的(2)存储了和类的结构相关的数据,类的成员变量、方法数据、成员方法以及构造器方法的代码(3)在JVM启动时被创建(4)JDK1.7时方法区(永久代)在堆内存,JDK1.8(元空间)在直接内存
内存溢出
(1)JDK1.8之后,方法区在直接内存,没有设置上限,不容易出现方法区内存溢出
运行时常量池
(1)常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等(2)运行时常量池:常量池是class文件中的,当该类被加载时,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
StringTable(字符串常量池)
(1)常量池中的字符串仅是符号,第一次用到时才变为对象(2)利用字符串常量池的机制,来避免重复创建字符串对象(3)字符串变量拼接的原理是StringBuilder(4)字符串常量拼接的原理是编译器优化(5)可以使用 intern 方法,主动将字符串常量池中还没有的字符串对象放入串池将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
public class TestStringTable {public static void main(String[] args) {//StringTable ["a" "b" "ab"]/* 常量池中的信息都会被加载到运行时常量池中* 这时 a b ab 都是常量池中的符号,还没有变成字符串对象* ldc #2 会把 a 符号变为 "a" 字符串对象* ldc #3 会把 b 符号变为 "b" 字符串对象* ldc #4 会把 ab 符号变为 "ab" 字符串对象*/String s1="a";String s2="b";String s3="ab";String s4=s1+s2;//new StringBuilder().append("a").append("b").toString() 堆中:new String("ab")String s5="a"+"b";//javac在编译期间做了优化 结果已经在编译器确定为abSystem.out.println(s3==s4);//falseSystem.out.println(s3==s5);//true}}
public class TestStringTable {public static void main(String[] args) {String s=new String("a")+new String("b");/* 此时:* 字符串常量池中:["a","b"]* 堆中:new String("a") new String("b") new String("ab")*///将s这个字符串对象尝试放入串池,如果串池中有则不会放入,如果没有则放入,会把串池中的对象返回String s2=s.intern();//此时字符串常量池:["a","b","ab"]System.out.println(s2==s);//trueSystem.out.println(s2=="ab");//trueSystem.out.println(s=="ab");//true}}public class TestStringTable {public static void main(String[] args) {String x="ab";String s=new String("a")+new String("b");/* 此时:* 字符串常量池中:["a","b","ab"]* 堆中:new String("a") new String("b") new String("ab")*///将s这个字符串对象尝试放入串池,如果串池中有则不会放入,如果没有则放入,会把串池中的对象返回String s2=s.intern();System.out.println(s2==x);//trueSystem.out.println(s==x);//falseSystem.out.println(s=="ab");//false}}
垃圾回收
(1)当空间不足时,将没有用的字符串常量垃圾回收
性能调优
(1)调整StringTable的大小(2)将StringTable的size调大,可以减少哈希冲突,速度会变快(3)可以使用 inter() 来避免重复的字符串对象入池,从而减少需要的内存
(6)直接内存
(1)常见于NIO操作时,用于数据缓冲区(2)分配回收成本较高,但读写性能高(3)不受JVM内存回收管理
直接内存释放原理
(1)使用 Unsafe 类分配和释放内存,并且回收时需要主动调用 freeMemory()(2)ByteBuffer的实现类内部使用了 Cleaner(虚引用) 来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的clean方法调用freeMemory方法来释放内存
禁用显示回收对直接内存的影响
(1)为了不影响程序执行效率,可能会禁用 System.gc(),这样的话就不会自动进行直接内存回收(2)需要我们手动调用 unsafe.freeMemory() 来释放直接内存
3.垃圾回收
(1)如何判断对象是否可以被回收
引用计数法
(1)某个对象被其它对象引用,则计数+1;如果该引用释放,则计数-1,当计数为0时,就可被回收(2)但存在循环引用,也就是两个对象互相引用的情况
可达性分析法
(1)确定根对象,扫描堆内存中所有的对象,如果某个对象被根对象直接或间接引用,则标记它,表示该对象不能被回收;没有被标记的对象则可以被垃圾回收(2)哪些对象可以作为根对象:java虚拟机栈中的引用的对象,方法区中的类静态属性引用对象方法区中的常量引用对象,本地方法栈中的引用对象
4.四种引用
(1)强引用
(1)平常通过 = 创建的对象(2)只有当没有引用指向该对象时,该对象才可以被垃圾回收
(2)软引用
(1)当发生垃圾回收并且内存不足时,一次垃圾回收后内存依然不足,只有软引用引用的对象会被回收(2)一些不重要的数据可以用软引用,在内存不足时可以进行回收(3)如果使用了引用队列,当软引用引用对象被回收时,该软引用会放入引用队列,是为了回收软引用
public class Test {private static final int _4MB=4*1024*1024;public static void main(String[] args) {soft();}//软引用private static void soft(){//list --强引用--> SoftReference --弱引用--> byte[]List<SoftReference<byte[]>> list=new ArrayList<>();for (int i=0;i<5;i++){SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB]);list.add(ref);System.out.println(list.size());}System.out.println(list.size());//1}//带队列的软引用private static void soft1(){//list --强引用--> SoftReference --弱引用--> byte[]List<SoftReference<byte[]>> list=new ArrayList<>();//引用队列ReferenceQueue<byte[]> queue=new ReferenceQueue<>();for (int i=0;i<5;i++){//关联了引用队列,当软引用所指向的byte[]被回收时,软引用自己会加入到queue中SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB],queue);list.add(ref);System.out.println(list.size());}System.out.println(list.size());//1//从队列中获取无用的软引用对象,并移除Reference<?extends byte[]> poll= queue.poll();while (poll!=null){list.remove(poll);poll= queue.poll();}}}
(3)弱引用
(1)只要发生垃圾回收,不管内存充不充足,只有弱引用引用的对象都会被回收(2)如果使用了引用队列,当弱引用引用对象被回收时,该弱引用会放入引用队列,是为了回收弱引用
(4)虚引用
(1)虚引用必须配合引用队列使用
5.垃圾回收算法
(1)标记清除
(1)将没有被根对象引用的对象标记(2)将被标记的对象释放(将垃圾对象的起始和结束地址进行记录)(3)比较快,但容易产生内存碎片
(2)标记整理
(1)将没有被根对象引用的对象标记(2)在清除垃圾的过程中,将可用的对象移向一端(3)不会造成内存碎片问题,效率低
(3)复制
(1)将内存区域划分为大小相等的区域(2)将没有被根对象引用的对象标记(3)将from区域存活的对象复制到to区域(4)清除from中的垃圾,交换from和to的位置(5)不会存在内存碎片,需要双倍内存空间
(4)分代回收
(1)对象首先分配在伊甸园区域(2)新生代空间不足时,触发 Minor GC,伊甸园和from存活的对象使用复制算法复制到to中,存活的对象年龄+1,释放垃圾,交换from和toMinor GC 会引发 stop the world,暂停其它用户线程,等垃圾回收结束,用户线程恢复运行(3)当对象寿命超过阈值(最大15次)时,会晋升到老年代(4)当老年代空间不足时,会先进行 Minor GC,当 Minor GC 后空间依然不足,会触发 Full GCFull GC 也会引发 stop the world,比 Minor GC 引发的时间更长(5)此时空间仍然不足时,会触发OOM
(5)GC
GC相关参数
(6)垃圾回收器
串行垃圾回收器
(1)单线程(2)适合堆内存较小,适合个人电脑(3)-XX:+UseSerialGC = Serial + SerialOld 打开串行垃圾回收器
吞吐量优先垃圾回收器
(1)多线程(2)适合堆内存较大,必须多核CPU(3)垃圾回收时,让单位时间内 stop the world 的时间最短
响应时间优先垃圾回收器
(1)多线程(2)适合堆内存较大,必须多核CPU(3)垃圾回收时,尽可能的让 stop the world 的单次时间最短(4)内存碎片可能过多,导致并发失败,
(7)G1
(1)注重吞吐量和低延迟(2)适合超大堆内存,会将堆划分为多个大小相等的Region(3)整体上是标记整理算法,两个区域之间用的是复制算法
新生代回收
回收伊甸园区的垃圾对象
新生代回收+并发标记
(1)在 Young GC 时会进行 GC Root 的初始标记(2)老年代占用堆内存空间比例达到阈值时,进行并发标记
混合回收
(1)最终标记,将可能没有被标记的垃圾对象标记(2)对伊甸园区、幸存区、old区进行回收(3)伊甸园区的存活的对象会被复制到幸存区,幸存区达到条件的存活对象会晋升到老年代,其余也复制到另一个幸存区。垃圾对象将被回收(4)老年代的垃圾对象会根据设置的暂停时间被有选择的回收,如果某个区域垃圾对象被回收的时间会超过暂停时间,那么就不会回收;如果不会超过暂停时间的,则会回收
Full GC
(1)当垃圾回收的速度跟不上垃圾产生的速度时,并发收集失败,开始 Full GC
新生代回收的跨代引用(老年代引用新生代)问题
将引用新生代对象的老年代对象标记(将卡表中的卡标记为脏卡)
Remark
(1)根据对象的黑白状态,来确定是否被回收(2)可能存在被强引用的对象被标记为白色,而被垃圾回收(3)采用写屏障技术,在某个对象的引用改变前,会将该对象加入队列,在remark阶段会重新扫描队列
字符串去重
字符串在底层是由char[]数组表示(1)将所有新分配的字符串放入一个队列(2)在新生代回收时,G1并发检查是否有字符串重复(3)如果它们值一样,让他们引用同一个 char[](4)优点:节省大量内存 缺点:略微多占用了CPU时间,新生代回收时间略微增加
并发标记时的类卸载
(1)所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它们所加载的所有类
回收巨型对象
(1)一个对象大于region的一半时,称之为巨型对象(2)G1不会对巨型对象进行拷贝(3)回收时被优先考虑(4)G1会追踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时被处理掉
并发标记起始时间的调整
并发标记必须在堆空间占满前完成,否则退化为 Full GC
(8)GC调优
6.类加载
(1)类文件结构
魔数
0~3字节,表示它是否是class类型的文件
版本
4~7字节,表示类的版本
常量池
记录java类中的各种信息
8~9字节,表示常量池长度,00 23 23的十进制就是35 表示常量池有#1~#34项,#0不计入,也没有值
第#1项0a表示一个method信息,00 06 和 00 15 表示它引用了常量池中#6和#21项来获得这个方法的
所属类和方法名
(2)字节码指令
将class文件中常量池信息载入运行时常量池
方法字节码载入方法区
main线程开始运行 分配栈帧内存
执行引擎开始执行字节码
(3)javap
字节分析类文件结构太麻烦了,可以使用javap工具来反编译class文件javap -v xxx.class-v:显示详细信息
(4)类加载
加载
链接
验证
验证类是否符合JVM规范,安全性检查
准备
(1)为静态变量分配空间,设置默认值,需要在初始化阶段完成赋值(2)静态变量存储在堆中(3)如果静态变量是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成(4)如果静态变量是final的,但数于引用类型,那么赋值也会在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用
初始化
(1)即调用 <cinit>()会导致初始化的情况:
(2)main方法所在的类,总会被首先初始化(3)首次访问这个类的静态变量或静态方法时会导致初始化(4)子类初始化时,如果父类还没初始化,那么会引发父类初始化(5)子类访问父类的静态变量,只会触发父类的初始化(6)调用Class.forName会触发初始化(7)new 会导致初始化不会导致类初始化的情况:
(1)访问类的 static final 静态常量(基本类型和字符串) 不会触发初始化(2)类对象.class不会触发初始化(3)创建该类的数组不会触发初始化(4)类加载器的 loadClass 方法,不会导致初始化(5)Class.forName 的参数2为false时不会导致初始化
(5)类加载器
启动类加载器
(1)加载 JAVA_HOME/jre/lib
扩展类加载器
(1)加载 JAVA_HOME/jre/lib/ext
应用程序类加载器
(1)加载 classpath
自定义类加载器
(1)加载 自定义什么时候需要自定义类加载器:
(2)想加载非 classpath 随意路径中的类文件
(3)都是通过接口来使用实现,希望解耦时,常用在框架设计
(4)这些类希望予以隔离,不同应用的同类名都可以加载,不冲突,常见于tomcat步骤:
(1)继承 ClassLoader 父类
(2)要遵从双亲委派机制,重写 findClass 方法
(3)读取类文件的字节码
(4)调用父类的 defineClass 方法来加载类
(5)使用者调用该类加载器的 loadClass 方法
双亲委派
(1)沙箱安全机制,防止核心API被篡改(2)代码复用,如果一个类已经被加载,那么不会被重复加载(3)稳定性,可以确保java类库的稳定性