JVM学习笔记
C语言中的栈和堆
栈是存放局部变量的地方,数组等非new的方法都是属于局部变量,局部变量不能开太大,不然会爆栈
全局变量或new出来的就存储在堆中,堆比较大,可能有内存那么大,会默认初始化,堆操作速度比栈慢
如何记忆堆和栈
堆是乱的内存,堆成一堆,所以比较多,因为比较乱,所以访问比较栈慢
栈是一层层叠好的,所以不能叠太高(栈内存较小),因为整齐,所以访问速度快
PS:这些只是形象理解,不是原理,与实际存在偏差。
Java中的堆和栈
堆:存放引用指向的对象
虚拟机栈(VM Stack):存放基本类型和引用
Java虚拟机与操作系统的相似之处
PC register:程序计数器
应该是指向当前线程正在执行的指令(位置)
这一部分空间非常小,所以OOM没有为其定义OOM异常
if the thread is native, the value of the Java Virtual Machine’s pc register is undefined.
方法区
存储运行时常量池和类元数据信息和静态变量
元空间和永久代是对方法区的实现(类比接口和2个实现类)
永久代(premGen):Java8以前的实现,是堆空间的一部分
元空间(metaspace):Java8的实现,是与堆不相连的本地区域,可以手动限制大小,也会OOM
本地方法栈:给native方法使用
参考:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
虚拟机栈
私有的
最小单位是栈帧(方法的状态保存在栈帧中,先进后出符合程序的调用逻辑)
栈帧中包含局部变量表,操作栈,动态连接,返回地址
局部变量表:存储参数、局部变量(引用类型和基本类型)
操作数栈:存放计算的中间结果,就是保存上下文,为下一次进入时继续执行做好准备
动态连接:执行运行时常量池中给栈帧所属方法的引用,通过动态连接可以找到当前需要调用的方法地址
方法出口(返回地址):下一个栈帧的执行位置
扩展:acwing,递归改迭代,通过栈实现
表达式求值
JVM处于JDK中最底层的位置
class文件 --> JVM虚拟机 --> 操作系统
JVM的组成
类加载子系统
运行时数据区(内存模型)
执行引擎
类的加载
class文件通过类加载子系统加载进内存模型的方法区并在堆中创建一个class对象
流程:
加载(IO读取class文件) --> 连接(验证、准备、解析) --> 初始化(静态变量和静态代码块的执行) --> 使用 --> 卸载
种类:
启动类加载器:jre的lib目录下的核心类库,如rt.jar、charsets.jar等
扩展类加载器:jre的lib目录下的ext扩展目录中的jar类包
应用程序类加载器:加载classpath路径下的类包,主要是加载自己写的类和第三方jar
自定义加载器:加载自定义路径下的类包
程序私有区域
程序计数器(不会OOM)
Java虚拟机栈
本地方法栈
程序公有区域
堆内存:分配所有对象实例,垃圾回收的主要区域
元数据区(方法区在Java8的实现):虚拟机加载的类信息,常量和静态变量
直接内存:非JVM内存的堆外内存,NIO会直接操作给内存,读写效率高
JVM内存分配与回收策略
-
对象优先在Eden区中分配
堆的内存划分:
年轻代(Eden和Survivor区(from和to区))
老年代
大多数时候对象在Eden区分配,如果Eden区内存部分,
虚拟机会发起一个次Minor GC进行垃圾回收(频繁,速度快)
Minor GC/Young GC: 指发生新生代的垃圾收集动作
Major GC/Full GC:一般会回收老年代,年轻代和方法区的垃圾,速度比Minor GC慢10倍以上
新生代、年轻代、老年代的含义
堆大小=年轻代+老年代
年轻代 = eden space(新生代) + from survivor +to survivor
MinorGC和YoungGC不同吗png
2.大对象直接进入老年代
大对象:字符串、数组
可以通过JVM参数调整大对象的大小,只在Serial和ParNew阶段有效
-XX:PretenureSizeThreshold
补充:新生代收集器有 Serial收集器、ParNew收集器、Parallel Scavenge收集器
参考:https://blog.youkuaiyun.com/zhao_miao/article/details/88748037
这么做就可以避免大对象在新生代,屡次躲过GC,还得把他们来复制来复制去的,最后才进入老年代,
这么大的对象来回复制,是很耗费时间的。
参考:https://blog.youkuaiyun.com/weixin_38106322/article/details/109035529
3.长期存活的对象将进入老年代
3.1 Minor GC存活后survivor区足够放入该对象
对象在Eden经第一次Minor GC存活后,会进入survivor区,
经过多次minor GC后,当到达一定的年龄(默认15)就会进入老年代(每次GC年龄+1)
可以通过MaxTenuringThreshold跳转默认年龄
3.2 Minor GC存活后survivor区不够放入该对象
如果survivor区放不下,就部分到老年代,部分survivor区
Eden和Survivor区(from和to区))默认8:1:1
Eden区满了会触发Minor GC
因为Eden区中的对象绝大部分是“朝生夕死”的,所以这个分配很合理,Eden取要尽量大
对象年龄动态判断
参考:https://www.jianshu.com/p/989d3b06a49d
年龄从小到大进行累加,当加入某个年龄段后,
累加和超过survivor区域*TargetSurvivorRatio的时候,就从这个年龄段网上的年龄的对象进行晋升
年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升。
用处:喜欢可能长期存活的对象尽早进入老年代。
对象动态年龄判断机制一般是在minor gc后触发的
老年代空间分配担保机制
参考:https://www.cnblogs.com/july-sunny/p/12618054.html
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败(java8默认开启)。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC
Java虚拟机如果判断对象是否存活(JVM垃圾判定算法)
1. 引用计数法(Java中未采用)
当对象引用计算器为0时,即无其他对象引用,判定为死亡,可被回收,
但是存在循环引用问题,如A引用B,B又引用A,会导致引用计算器永远不为0
参考:https://zhuanlan.zhihu.com/p/81673709
2. 可达性分析法
从GCRoot开始向下搜索,直接或间接可达的抵消,即存活对象
GCRoot可以是虚拟机栈引用对象、本地方法栈引用对象、静态属性引用对象、常量引用对象
扩展:
引用的分类
强引用:程序中普遍存在的对象引用
软引用:softReference实现,内存溢出前回收
弱引用:WeakReference实现,下一次GC时回收
虚引用:PhantomReference实现,形同虚设
对象怎么起死回生
不可达的对象会检测finalize方法,如果在该方法中让对象重新与引用链相连就能起死回生
但是不建议这么做,无法保证对象的调用顺序
四种JVM垃圾回收算法
1.标记清除法
统一标记可回收的对象(不可达对象),然后GC统一回收,
会产生大量内存碎片,当申请大段连续空间时可能再次GC
2.标记整理算法
在标记回收对象后,会集中整理内存区域,有效减少内存碎片,但效率较低
3.复制算法
内存分为2个区域,把不回收对象复制到空闲区域,再清空旧区域
这样内存碎片少,效率高,但空间利用率低,不适合老年代区域,适合“朝生夕死”的年轻代
4.分代回收算法(JVM采用)
年轻代采用复制算法,老年代采用标记清除会标记整理/清除算法
为什么?
年轻代通常存活时间很短,决大多数都会被第一次GC回收,所以复制的内存很少
老年代的对象存活时间很长,对象很大,频繁复制会降低效率
JVM收集器
参考:https://www.cnblogs.com/javastack/p/13152564.html
https://www.nowcoder.com/questionTerminal/46b6030921164ab0a3cb3dbd6d64f01a
Serial收集器
单CPU、单线程,stop the word,响应停顿,适合内存占用在一两百兆的桌面应用
优点:简单、效率高(单CPU\stop the word)
ParNew收集器
多线程的Serial版本,运行在Server模式下的首要选择
除了Serial收集器外,只有它能和CMS收集器配置工作
面经:
参考:https://www.cnblogs.com/lwh1019/p/13814486.html
Java线程栈和方法栈的区别
没有方法栈的概念,方法调用时的方法栈就是保存在线程栈(JM Stack)中的
方法对应栈帧,一个方法对应一个栈帧
其他:标量替换,对象不一定都在堆中
对java字节码有什么了解
cafe babe
如何反编译字节码文件到一个文件中
java -c Math.class > math.txt
为什么需要程序计数器
为什么程序计数器是私有的
为什么不会OOM
为什么Native方法执行时是Undefined
随去修改程序计数器的值
字节码执行引擎
https://blog.youkuaiyun.com/weixin_43218500/article/details/106043711
java运行时一共有几个线程
GC、Main
在一个web应用程序中,哪些对象可能会在老年代
单例、静态变量、数据库连接池、spring容器的Service、conroller、。。。
为什么GC需要stop the world
不停止其他程序会导致GC找到的非垃圾对象在GC过程中变成垃圾对象
使用jvisualvm
/*================================================================*/
jvm的全称是Java虚拟机,它处于jdk的最底层,jvm由类加载子系统、运行时数据区、执行引擎等部分组成
类加载子系统负责把class文件加载进入运行时数据区
运行时数据区,可以按照线程私有和线程共享分为2大组成
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:堆、方法区(方法区是一个概念,Java8之前的实现是永久代、Java8是元空间)
下面简述每一个部分的作用:
程序计数器:记录当前线程执行的位置,如果执行的是本地方法就是undefined
程序计数器有执行引擎负责改变它的值
程序计数器是唯一不会OOM的区域,因为存储的是当前执行的指令地址,空间固定,所以不用定义OOM
程序计数器用于程序的正常执行顺序和切换线程时保存上下文,所以它是线程私有的
虚拟机栈:主要存放基本数据类型和对象的引用
虚拟机栈由一个个栈帧组成
栈帧代表一个方法,由局部变量表、操作数栈、动态链接(其他方法所在方法区的地址)、运行出口(下一个要执行的方法位置)
本地方法栈:给native方法使用
堆:垃圾回收和jvm调优的主要区域
分为老年代和年轻代
年轻代又分为Eden和Survivor区,Survivor又分为form和to,一般三者的大小是8:1:1
年轻代中的对象大都“朝生夕死”,存活对象不多,所以采用复制算法