本篇JVM笔记总结自 【狂神说Java】JVM快速入门篇
JVM
什么是JVM
JVM是java虚拟机的缩写,是一个抽象出来的、虚拟的计算机。与操作系统进行交互,有自己完善的架构,如处理器、堆栈、寄存器,用来解析和运行java程序
Java为什么能实现跨平台
关键在于使用java虚拟机,java代码不直接在cpu上运行,而是在java虚拟机上运行
java先把java文件编译成二进制的.class字节码文件,然后再加载到java虚拟机中,jvm解析成各个操作系统的机器指令,从而实现跨平台
jvm运行时的数据区域
类加载过程
1、加载:把.class的二进制字节码文件加载近jvm中
2、连接
1)验证:确保被加载类的正确性
2)准备:为类变量(静态变量)分配内存,设置默认值(初始化之前不赋予初始值)
3)解析:把类中的符号引用转换成直接引用
3、初始化:为类变量赋予初始值
4、实例化:为新对象分配内存空间,为实例变量赋值
双亲委派机制
一个类加载器接受到类加载的请求时,会将请求委托给父类加载器,一直委托到最上层的父类加载器,父加载器加载失败时,会将类交给子加载器加载,所有加载器都加载不到时,抛出ClassNotFoundException异常。
双亲委派机制作用为确保java核心类库中的类不会被用户自定义的类所替代
模型工作工程:用户自定义的类加载器 --> 应用类加载器 Application ClassLoader --> 拓展类加载器Extension ClassLoader --> 根加载器 Bootstrap ClassLoader(顶端)
JVM五大数据区域
本地方法栈(Native Method Stack)
服务于本地方法接口(JNI:Java Native Interface),JDK中带有native关键字的方法为本地方法,本地方法接口是为了融合不同的编程语言为java所用,在执行引擎执行的时候加载本地方法库,目前使用极少
程序计数器(Program Counter Register)
线程私有的区域,比较小的一块内存空间,用于记录当前线程要执行的字节指令所处的地址,通过程序计数器来选取下一条需要执行的字节码指令
方法区(Method Area)
也叫非堆,逻辑上存在物理上不存在,和堆一样是线程共享的区域
用于存放被虚拟机加载的类信息、常量池、方法数据、方法代码、符号引用等
JDK1.7以前是在永久代中,JDK1.8是在元空间中(元空间不在虚拟机中,使用本地内存)
栈(Stack)
线程私有的区域,存储基本类型的值,对象的引用、方法等,
线程中执行一个方法时往栈中插入一个栈帧,最先入栈的最后出栈,与队列相反遵循先进后出的原则,方法执行完成后出栈,栈的生命周期与线程一致,
堆(Heap)
线程共有,jvm调优的区域,存放类的相关信息以及类new的实例对象
堆内存分为三部分:新生代、老年代、永久代(JDK1.8之后为元空间),新生代与老年代内存比例为1:2
新生代分为:伊甸区、幸存者0区,幸存者一区,比例为8:1:1,为GC的主要场所
对象在堆内存中的历程:
1)所有对象都在伊甸区创建,并且绝大部分在伊甸区完成整个生命周期
2)对象持续创建的情况下,当伊甸区空间用完时,JVM会对伊甸区进行GC(轻GC:只针对新生代),将存活下来的对象移动到幸存者0区
3)若0区也满了,再次进行GC,在将存活对象移动到幸存者1区,此时0区和1区是互相交替的一个过程(from->to的关系,空的一方为to),若新生代的三区内存都满了,就将对象移动到老年代
4)若老年代对象也满了,JVM进行重GC(fullGC:针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC),若老年代执行了重GC内存还是满的没办法创建新对象,就会产生OOM异常(OutOfMemoryError)
JVM GC在不同区域实行分代算法;新生代使用复制算法,老年代使用标记清除与标记整理
OOM与Dump内存快照
当程序发生OOM时,判断OOM类型,定位有可能出现问题的代码
最常见的OOM有以下两种:
1、java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,这种情况最常见,可调整堆内存大小
2、java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,一般由于方法去存在过多的Class信息导致,可调整方法区大小
3、java.lang.StackOverflowError------>栈溢出,一般是死循环或者递归调用产生
以上操作没解决问题的话使用性能分析工具JProfile分析Dump内存快照,下载安装并引入到IDEA的tool中便可使用,JProfile中可以查看具体是哪个线程、哪行代码可能存在OOM,帮助我们快速定位问题
GC四大算法
1、引用计数法:每个对象都有一个计数器,对象被引用一次加1,引用失效减1,计数器变为0被GC
计数器本身有一定消耗,并且较难处理循环引用,JVM一般不采用这种算法
2、复制算法:将内存分为两块,只是用其中一块,当内存满了进行GC,将存活的对象复制到另一块内存上
优点是效率高,没有内存碎片,缺点是需要双倍内存空间,有点浪费;适合对象存活率低的情况使用,新生代采用这种算法
3、标记清除算法:第一阶段扫描整个内存区域,将存活的对象进行标记,第二阶段再次扫描,清除没有被标记的对象
优点是不需要额外内存空间,缺点是存在内存碎片以及两次扫描耗时严重效率低并且应用程序会暂时停止
4、标记整理算法:第一阶段与标记清除一致,第二阶段将存活对象都向一端移动,然后再清理掉边界以外的内存
优点是节约内存空间还没有内存碎片,缺点是效率最低
这几种算法各有优劣,所以没有最好的算法,只有最合适的算法(分代收集算法)
如何判断对象是否为内存垃圾
简单来说没有被使用的对象即为内存垃圾
方式一:引用计数法(同GC四种算法)
方式二:可达性分析(主流)
将一组活跃的引用对象当作根集合GC Roots,从这组对象向下遍历搜索,能遍历到的(可到达的)对象判定为存活,不能遍历到的判定为死亡需要被GC
Java中可以作为GC Roots的四种对象
1、虚拟机栈中引用的对象
2、方法区中常量引用的对象
3、方法区中静态属性引用的对象
4、本地方法栈中JNI (Native方法)引用的对象
JVM 调优和参数配置
XX参数之Boolean类型 公式: -XX: + 或者 - 某个属性值 + 表示开启,- 表示关闭
1、-XX:+PrintGCDetails 输出详细GC收集日志信息
XX参数之KV设值类型 公式: -XX: 属性key=属性值value
1、-Xms 初始内存大小,默认为物理内存的 1/64,等价于 -XX:InitialHeapSize
2、-Xmx 最大内存大小,默认为物理内存的1/4,等价于 -XX:MaxHeapSize
3、-Xss 设置单个线程栈的大小,一般默认为 512k ~ 1024k,等价于 -XX:TheadStackSize
4、-Xmn 设置年轻代大小,一般不用动
5、-XX:MetaspaceSize 设置元空间大小:
强引用、软引用、弱引用、虚引用
强引用:把对象赋值给一个引用变量就是强引用,即使出现OOM都不会被GC,一直是可达状态,用不到也不会被JVM回收
软引用:出现在内存敏感的程序中,回收机制运行的时候如果内存够用的时候就保留,不足的时候就回收
弱引用:只要回收机制一运行,不管内存够不够用都会回收
虚引用:不决定对象生命周期,随时可被回收,主要是跟踪对象被垃圾回收的状态进行进一步的操作
垃圾收集器
串行垃圾回收器(Serial)
它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环 境。
并行垃圾回收器(Parallel)
多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算、大数据处理首台处理等弱交互场景。
并发垃圾回收器(CMS)
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用对响应时间有要求的场景。
G1垃圾回收器
G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
户线程是暂停的,适用于科学计算、大数据处理首台处理等弱交互场景。
并发垃圾回收器(CMS)
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用对响应时间有要求的场景。
G1垃圾回收器
G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收