这是一篇JVM小白的入门笔记,适合刚接触JVM的伙伴们入门参考。
笔记是完全按照我看的视频来的,说实话内容不深很好理解,主要的作用就是把所有的知识点串起来让你不会迷茫,就像画画一样先描好轮廓,后面才能画成,画好;入门之后就可以结合博客和书籍进行深入理解补充知识了(书籍是标准答案),看视频的你只需要对着视频对我的笔记加以修改和补充,效率会事半功倍而且体验感很好(这事我自己没少干哈哈) 视频地址:黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓 我也正在全面深入的补充JVM知识,会很快更新为入好门的伙伴们参考。。。 word文档版 提取码:hlgc –来自百度网盘超级会员V4的分享
JVM概述栈:先进后出
多个线程多个虚拟机栈
一个栈由多个栈帧组成
2.栈内存分配越大,物理内存反而越来越少,而线程在物理内存中运行,线程会越来越少;栈内存大了通常只是能进行更多次的方法递归调用,而不会提高运行效率,反而会影响线程数目变少
3.方法内的局部变量如果是每个线程私有的,就不会有线程安全问题;方法内的局部变量如果是每个线程共享的(static),就会出现线程安全问题
外防输入,内防输出,保证线程安全哈哈
栈内存溢出栈帧过多导致内存溢出(如:方法递归调用时如果处理不当,方法一直调用便会一直产生栈帧,导致栈内存溢出)
栈帧过大导致内存溢出(这种情况一般出现概率很小)
java.lang.StackOverflowError 栈内存溢出异常
可通过-Xss设置虚拟机栈内存大小
有时候在日常写代码时 第三方的库也会导致栈内存溢出
(如:两个类之间的循环引用导致json引用时出现StackOverflowError 栈内存溢出异常,解决:在json转换时打破这种循环引用,比如让一方中断,加上@JsonIgnore
第一个线程一上来就锁住了对象a,休眠2s后会尝试锁住对象b
在第一个线程开始休眠1s后第二个新的线程来了,
第二个线程一上来就锁住了对象b,接着想尝试锁住对象a,这时时间已经过去了1s,但是a已经被线程一锁住了,线程二要等待线程一释放对象a,继续等待1s,线程一会尝试锁住对象b,但是b已经被线程二锁住了,线程一只能等待线程二释放对象b,拿不到对象b导致无法释放对象a;这样两个线程一直在等待对方释放对象,造成死锁
本地方法栈为本地方法的运行提供内存空间
堆上面的程序计数器,虚拟机栈,本地方法栈都是线程私有的
堆以及下面的方法区都是线程共享的
jvm堆分为:新生代(一般是一个Eden区,两个Survivor区),老年代(old区)
常量池属于 PermGen(方法区)
堆有垃圾回收机制为什么还会产生内存溢出呢,
当有对象不断地在产生并且这些对象在不断地被使用,那么这些对象就不能当做垃圾被回收,就有可能造成内存溢出
java.lang.OutOfMemoryError:Java heap space 堆内存溢出异常
可通过-Xmx设置堆内存大小
堆内存诊断堆内存诊断工具
jmap工具:jmap -heap 进程id
jconsole工具
排查出肯定是student对象和ArrayList占用内存比较高
并且对象是长时间使用的导致垃圾回收无法回收它们的内存
方法区:所有java虚拟机线程共享的区域,它存储了跟类的结构有关的信息,如运行时常量池,类的成员变量,方法数据,成员方法和构造器方法的代码部分;方法区在虚拟机启动时被创建,逻辑上是堆的组成部分,永久代和元空间都是方法区的实现。方法区如果申请内存时发现不足,会让虚拟机抛出OutMemoryError异常,也就是方法区也会导致内存溢出错误
java.lang.OutfMemoryError:Metaspace 元空间内存溢出异常
可通过-XX:MaxMetaspaceSize设置元空间内存大小
java.lang.OutfMemoryError:PermGen space 永久代内存溢出异常
元空间使用的是系统内存(本地内存)便相对充裕很多,不会像永久代一样垃圾回收效率非常的低,导致内存溢出
实际场景中使用动态加载类,动态生成类,使用不当都会导致方法区的内存溢出
运行时常量池常量池中的信息,都会被加载到运行常量池中
StringTable(还需要看一下)StringTable常量池与串池的关系(详情见:https://www.bilibili.com/video/BV1yE411Z7AP?p=28)
StringTable字符串变量拼接
假如不断地往StringTable内存空间中放入字符串对象,并且用一个长时间存活的对象来应用这些字符串对象,势必会造成内存空间不足,环境下运行如果在1.6环境下运行,便会触发永久代内存运行不足,如果在1.8环境下运行,它触发的内存不足是堆空间不足,
1.8串池用的是堆空间
1.6串池用的是永久代
运行时常量池存放什么? 存放编译期生成的各种字面量和符号引用;(字面量和符号引用不懂的同学请自行查阅) 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。 此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池与字符串常量池?(可能有同学把他俩搞混) 字符串常量池:在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池.
StringTable垃圾回收 StringTable调优案例:比如有大量的用户地址信息存储占用大量的内存空间,那么采用字符串intern()方法,可以去除重复的地址,相同的地址在串池中只会存储一份,这样就能减少字符串对于内存空间的占用
直接内存使用direct memory进行大文件的读写效率会非常的高
基本使用文件读写流程
由于java代码访问不到存在系统缓存区中的数据,所以还要转存到java缓存区中才能读到,这样就造成了不必要的数据复制,使读取效率降低
使用direct memory进行文件读写流程
使用direct memory在操作系统内直接划出一块缓冲区,这样使得系统和java代码都能访问,大大提高了数据读写效率
内存溢出java.lang.OutOfMemoryError:Direct buffer memory 直接内存溢出
直接内存 释放通过Unsafe对象管理的(手动调用释放)
java内存 释放通过垃圾回收释放(自动调用释放)
分配和回收原理使用-XX:+DisableExplicitGC禁用内存直接回收
垃圾回收 如何判断对象可以回收 引用计数法容易造成内存泄漏
可达性分析算法通过Memory Analyer工具分析堆
先运行该程序 到1,然后jps命令
jmap抓取堆的运行状态转储为一个文件
选择 Java Basics–》GC Roots 查看根对象
严格来说一般有下面五种引用
强引用
软引用
弱引用
虚引用
终结器引用
强引用
软引用
弱引用
Mark Sweep 先标记后清除
优点:速度快 缺点:容易产生内存碎片(空间不连续)
Mark Compact 先标记后整理
优点:没有什么内存碎片 缺点:速度慢
Copy 先标记后复制
优点:没有什么内存碎片 缺点:会占用双倍的内存空间
把堆内存区域划分成了两大区域:新生代和老年代
新生代中存放用完就会丢弃了的对象,老年代中存放长期使用的对象
所以新生代中的垃圾回收发生的比较频繁,老年代中垃圾回收长时间才发生一次
新生代垃圾回收:Minor GC
老年代垃圾回收:Full GC
读垃圾回收日志
下面是没有运行任何代码时堆内存的使用情况
加个7MB的数组
如:新生代GC后回收前,新生代的内存占用1984k,回收后的内存占用667k,新生代的总内存大小是9216k,垃圾回收耗时0.0028851 secs
堆的回收前内存占用1984k,回收后的内存占用667k,堆的总内存大小是19456k,垃圾回收耗时0.0029666 secs
GC:新生代发生的垃圾回收
FULL GC:老年代发生的垃圾回收
如果放入的对象太大,新生代和伊甸园都容纳不下时,就不会触发新生代垃圾回收了,对象会直接晋升到老年代
如果放入的对象太大,新生代和伊甸园甚至老年代都容纳不下时,会抛出堆内存溢出异常
Serial工作在新生代 采用的是复制算法
SerialOld工作在老年代 采用的是标记整理算法
-XX:+UseParallelGC 工作在新生代的垃圾回收器
-XX:+UseParallelOldGC 工作在老年代的垃圾回收器
可开启多个线程同时进行垃圾回收
-XX:GCTimeRatio=ratio 调整垃圾回收时间和总时间的占比(吞吐量)
-XX:+UseAdaptiveSizePolicy 调整新生代大小堆大小等等
-XX:MaxGCPauseMills=ms 最大暂停毫秒数(垃圾回收时的暂停时间)它和上面的参数时对立的,比如上面堆大小调大,吞吐量会提升,那么每次垃圾回收暂停时间会增长;上面堆大小调小,每次垃圾回收暂停时间会减小,那么吞吐量会降低
-XX:ParallelGCThreads=n 调整垃圾回收的线程数
concurrent并发:比如用户线程和垃圾回收线程是可以同时运行的
并行:比如多个垃圾回收线程运行期间用户线程不能同时运行
-XX:+UseConcMarkSweepGC 工作在老年代的垃圾回收器,在某些时候可以和用户线程并发执行
-XX:+UseParNewGC =n工作在新生代的垃圾回收器
-XX:ParallelGCThreads=threads 并行线程参数 -XX:ConcGCThreads 并发线程参数
-XX:CMSInitiatingOccupancyFraction=percent 控制何时进行垃圾回收
-XX:+CMSScavengBeforeRemark 在重新标记之前进行新生代的垃圾回收,减轻重新标记的压力
该垃圾回收器的bug是:如果内存碎片过多会造成并发失败,退回到垃圾串行回收,导致时间响应变长造成不好的用户体验
G1垃圾回收阶段
G1把堆内存划分成了一个个大小相等的区域,每个区域都可以独立作为 伊甸园,幸存区和老年代
白色部分是空闲区,一开始分配的对象都会放在伊甸园绿色部分
当伊甸园区域被占满,便会触发新生代的一次垃圾回收
幸存的对象会复制到幸存区蓝色部分
再工作一段时间,当幸存区的对象也比较多了或者幸存区的对象超过了一定时间,便会再触发新生代的垃圾回收,幸存区的一部分对象会晋升到老年代橙色部分,没有超过时间的对象被复制到新的幸存区中,当然也有一些新生代的对象复制到新的幸存区中
不会STW:不会停止用户线程
当老年代占到整个堆空间的45%时,就会进行并发标记
E绿色:伊甸园区 S蓝色:幸存区 O橙色:老年代区
-XX:MaxGCPauseMillis 垃圾回收时的最大暂停时间
垃圾回收后E中的幸存对象会复制到新S区,别的在S中不够年龄的对象也会被复制到新S区,符合晋升条件的对象也会被复制到O中
在有限的暂停时间内,也会从老年代区中选取回收价值最高的区(O红色部分)进行垃圾回收,复制到新的老年代区中
当垃圾回收速度大于新产生的垃圾的速度时,进行并发收集,响应时间短
当垃圾回收速度小于新产生的垃圾的速度时,并发收集失败,退化成串行收集 full gc,导致响应时间变长
Young Collection 跨带引用新生代垃圾回收过程:首先找到根对象,根对象可达性分析找到存活对象,存活对象复制到幸存区
那么有一个问题:有一些根对象是在老年代中找的,如果遍历整个老年代去寻找根对象效率是非常低的;因此解决这个问题采用卡表技术,把老年代再进行细分,成为多个card。每个card大约是512k;如果老年代中有一个card引用了新生代的对象,那么就把这个card标记为脏card,找根对象时在这些标记的脏card中找就行了,提高了扫描根对象的效率
并发标记时对象的处理状态
黑色:已经处理完成 灰色:正在处理中 白色:还未处理
有强引用在引用(黑色箭头)变成黑色后最终会存活,而没有被引用的最终会被垃圾回收掉
JDK 8u20 字符串去重原理:让两个字符串对象引用同一个char[]数组
-XX:+UseStirngDeduplication 开启字符串去重功能
JDK 8u40 并发标记类卸载巨型对象的垃圾回收有所不同:G1不会对巨型对象进行拷贝,回收时会优先回收巨型对象
当老年代中有card引用了巨型对象,那么这个card标记为脏card,当老年代中没有card引用巨型对象,那么这个巨型对象会在新生代垃圾回收中被回收掉
JDK 9 并发标记起始时间-XX:InitiatingHeapOccupancyPercent 老年代和堆内存的占比阈值 超过阈值时并发垃圾回收开始,阈值默认是45%,实际过程中阈值大了有可能产生full gc,小了会频繁的做并发标记和混合收集,JDK 9以后可以动态的调整这个阈值,尽可能的避免并发垃圾回收退化成串行垃圾回收full gc
JDK 9 更高效的回收高吞吐量垃圾回收器:ParallelGC
响应时间快垃圾回收器:CMS,G1,ZGC
最快的GC是不发生GC我们在做内存调优时一般都从新生代优化,因为新生代的调优空间更大一些
可通过调大新生代内存大小来达到内存优化
-Xmn 设置新生代内存大小
如果新生代内存划得太大,那么垃圾回收时便容易触发老年代垃圾回收,老年代垃圾回收会消耗更长的时间
建议新生代内存占整个堆内存的比例为 大于1/4,小于1/2
新生代最大容量(理想状态):一次请求和响应中产生的对象*并发量
并发量:同一时刻有多少用户访问
老年代调优CMS:低响应时间,并发的垃圾回收器
在垃圾回收时,用户线程运行产生的新的垃圾叫做浮动垃圾,如果浮动垃圾过多导致内存不足便会引发并发失败,退化到串行老年代垃圾回收器,那么效率就变得非常低了,响应时间变得非常长
所以给老年代划分内存空间时会划得大一点,避免浮动垃圾引发并发失败
案例magic 魔数:
所有的文件都有自己的特定类型,不同的文件都有自己的魔数信息,如.png和.jpg的魔数信息就不一样
所以魔数用来标识不同文件的类型
版本 version:
常量池 constant_pool:(查表没怎么听懂)
访问标识与继承信息:
成员变量 fields:
方法 methods:
附加属性:
准备:
把class文件中的常量池信息载入到运行时常量池
a赋值:
a被赋值10
剩余:
istore_1:把操作数栈栈顶数据弹出来存入到局部变量表中1号槽位
istore_2:把操作数栈栈顶数据弹出来存入到局部变量表中2号槽位
iload_1:把局部变量1槽位的数据读到操作数栈上
iload_2:把局部变量2槽位的数据读到操作数栈上
iadd +操作
将操作数栈中的两个变量弹出,将结果存入到操作数栈
istore_3:把操作数栈栈顶数据弹出来存入到局部变量表中3号槽位
a+b赋值给c
getstatic #4:到常量池中找到成员变量的引用,进而找到其在堆中的对象,最后将堆中的对象引用的地址放入到操作数栈中
iload_3:把局部变量3槽位的数据读到操作数栈上
iload 运算 iinc 自增
bipush:把10放入到操作数栈中
istore_1:把操作数栈栈顶数据弹出来存入到局部变量表中1号槽位
相当于把10赋值给a
iload_1:把局部变量1槽位的数据读到操作数栈上
iinc 1,1:对1槽位自增1
相当于a++ 先运算再自增
这个动作在局部变量表中发生,并没有影响到操作数栈
iinc 1,1:对1槽位自增1
iload_1:把局部变量1槽位的数据读到操作数栈上
相当于++a 先自增再运算
iadd +操作
将操作数栈中的两个变量弹出,将结果存入到操作数栈
iload_1:把局部变量1槽位的数据读到操作数栈上
iinc 1,-1:对1槽位自减1
相当于a-- 先运算再自减
相当于完成了a++ + ++a + a–操作
条件判断指令iconst_0:得到0这个常量(表示数-1~5之间的数)
istore_1:存入到a变量
iload_1:从局部变量表中把a加载进来放入操作数栈进行比较
ifne 12 :如果操作数栈中的数不等于0,跳转到12行
bipush 20:给操作数栈上放入一个20
istore_1:(将20)存入到a变量
ifne 12 :如果操作数栈中的数等于0,继续往下走
bipush 10:给操作数栈上放入一个10
istore_1:(将10)存入到a变量
goto 15:跳转到末行号15 return
循环控制指令iconst_0:得到0这个常量(表示数-1~5之间的数)
istore_1:存入到a变量
iload_1:从局部变量表中把a加载进来放入操作数栈进行比较
bipush 20:给操作数栈上放入一个10
if_icmpge 14:比较两个整数,0是否大于等于10,如果是则跳转到14行,否则继续往下走
iinc 1,1:将变量a自增
goto 2:跳转到行号2
这样不断的循环
先自增,a加载到操作数栈,然后将10加载到操作数栈,
如果0
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?