文章目录
JVM是什么
java虚拟机,是一个可以执行java字节码的虚拟机进程。
java之所以可以跨平台允许,便是由于java虚拟机;但是不同的平台需要安装不同版本的JVM,因为各平台之间还是有一定的差异性
JVM的组成
- 类加载器(class loader)
- 运行时数据区
1. 方法区
2. 堆
3. 栈
4. 本地方法栈
5. 程序计数器
方法区、栈所有线程间共享
栈、本地方法栈、程序计数器线程私有
- 执行引擎
- 本地接口->本地库
32位个64位最大堆内存分别是多少
32为理论上堆内存可以到达2^32,4GB
64位2^64(一个非常大的数)
JAVA内存堆和栈区别
- 栈内存用来存储基本类型的变量和对象引用变量;堆内存用来存储java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
- 栈内存归属于单个线程,每个现场都会有一个栈内存,其存储的变量只能在其所属线程可见,即栈内存可以理解成线程的私有内存;堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问
- 如果栈内存没有可用的空间存储方法调用和局部变量,jvm会抛出stackoverflowerror错误;如果堆内存美而有可用的空间存储生成的对象,jvm会抛出oom错误
- 栈内存要远远小于堆内存,如果使用递归,栈内存很容易就满。-Xss选项设置栈内存大小,-Xms选项可以设置堆内存开始的大小
jvm中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常比堆小,也不会再多个线程之间共享,而堆整个线程共享
JAVA对象的创建过程
装载:验证->准备->解析
初始化:对象实例化->垃圾收集->对象终结->卸载类型
java中对象的唇膏就就是在堆上分配内存空间的过程
-
检测类是否被加载
当虚拟机遇到new关键字时u,首先去检测这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载】解析和初始化,如果没有,就执行类加载过程 -
对象分配内存
类加载完成后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经确定了,只需要在堆上分配所需要的内存即可
1. 对于内存绝对规整的情况,虚拟机只需要在被占用的内存和可用空间之间移动指针即可,这种方式被称为指针碰撞
2. 对于内存不规则的情况,虚拟机需要维护一个列表,来记录哪些内存是可用的,分配内存的时候需要找到一个可用的内存空间,然后在列表上记录下已被分配,这种方式被称为空闲列表
多线程并发时会出现正在给对象A分配内存,还没来得及修改指针,对象B又用这个指针分配内存,出现这种问题的解决方案:
1. 采用同步的方法,使用CAS来保证操作的原子性
2. 每个现场分配内存都在自己的孔家进行,即时每个现场都在堆中预先分配一小块内存,称为本地线程分配缓冲,分配内存的时候再缓冲上分配,互不干扰
- 为分配的内存空间初始化零值
- 对对象进行其他设置
设置对象头所属的类、类的元数据信息、对象的hashcode、GC分带年龄等 - 执行init方法
此时对象才是真正的创建,之前的步骤只是初始化零值,并没有根据程序中的代码分配初始值
过程如下:
- new
- 根据new的参数在常量池定位一个类的符号引用
- 如果没有,说明类未被加载,则执行类加载过程和初始化
- 虚拟机在堆中给对象分配内存
- 将分配的内存初始化为零值
- 调用对象的init方法完成创建
对象的内存布局
- 对象头:
1. 自身运行时数据:hashcode、gc分带年龄、锁状态标示、线程持有的锁
2. 类型指针,对象指向元数据的指针 - 实例数据
- 对其填充:避免空间浪费,因为分配内存是分配一段内存,读取也是读取一段
有哪些OOM异常
- java堆溢出
- 虚拟机栈和本地方法栈溢出
- 方法区和运行时常量池溢出
- 本机直接内存溢出
如何排查OOM异常
- 控制台看错误日志
- JDK自带的jvisualvm工具查看系统的堆栈日志
- 定位出内存溢出的空间:堆、栈还是永久代
- 如果是堆内存溢出,看是否创建了超大的对象
- 如果是栈内存溢出,看是否创建了超大的对象,或者产生了死循环
JAVA存在内存泄漏么?
理论上由于垃圾回收机制是不存在的,实际上还是存在的:
- hibernate的session中的对象属于持久态
- netty的堆外bytebuf对象,使用完未归还
什么是垃圾回收机制
java中对象采用new或反射方法创建,这些对象的创建都是在堆中分配的,所有对象的回收都是由java虚拟机通过垃圾回收机制完成的,GC为了能够正确释放对象,会监控每个对象的运行状态,对他们的申请、引用、被引用、赋值等状态进行监控
为什么不建议在程序中显示使用System.gc()
显示声明就是做堆内存扫描,即full gc,需要停止所有活动的,对应用有很大可能存在影响。而且调用system.gc方法后,不会立即执行full gc,而是虚拟机自己决定
如果一个对象的引用被设置为null,gc会立即释放对象的内存么
不会,在下一次gc循环中被回收
如果判断一个对象死亡
引用计数:
每个对象有一个引用计数属性,新增一个引用计数+1,释放-1,为0则可被回收
对象的引用类型
- 强引用:
new - 软引用:
如果内存空间足够,垃圾回收期不会回收;如果不足则会 - 弱引用:
垃圾回收期线程扫描它所管辖的内存区域,只要发现弱引用的对象,不论内存足够不足够,都会回收它的内存 - 虚引用:
任何时候都可能被垃圾回收,但是必须与引用队列referenceQueue联合使用
JVM垃圾回收算法
-
标记-清除算法
在标记阶段,首先通过根节点,标记所有根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有未被标记的对象
缺点:- 效率不高
- 空间问题,标记清楚之后会产生大量不连续的内存碎片,导致在以后程序需要分配交大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
-
标记-整理算法
类似于标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界意外的内存
优点:- 相对于标记-清除算法,解决了内存碎片的问题
- 没有内存碎片,对象的创建基于本地现场分配缓冲分配内存也更快了
缺点:
- 效率不高
-
复制算法
将内存按容量划分为大小相同的两块,每次只使用其中的一块,当这一块内存快用完了,就将还存活的对象复制到另一块上面,然后再将已经使用过的内存空间一次清理掉,这样就使得每次都是对整个半区进行内存回收,内存分配也不会有内存碎片,只要一动堆顶指针,按熟悉怒分配内存即可
优点:- 效率高,没有内存碎片
缺点:
1. 浪费一半的内存空间
2. 复制收集算法在对象存活率较高时要进行较多的复制操作,效率将会变低
- 分代收集算法
1. 新生代中,每次垃圾收集都发现有大批大对象死去,只有少数存活,选用复制算法
2. 老年代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记清理或标记整理算法来进行回收
对象分配策略:
- 对象优先在Eden区域分配,如果对象过大直接分配到old区域
- 长时间存活的对象进入到old区域。年龄大于15,survivor来回复制一次年龄增长加一
JVM垃圾收集器有哪些
收集器 | 串/并行 | 新/老生代 | 算法 | 目标 | 适应场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU |
Serial Old | 串行 | 老年代 | 标记-整理算法 | 响应速度优先 | 单CPU |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU |
Pa rallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 后台运算无需交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 后台运算无需交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | BSjava应用 |
G1 | 并发 | both | 标记-整理+复制 | 响应速度优先 | BSjava应用 |
对象的分配规则
- 对象先分配在Eden区
- 如果Eden区无法分配,那么会尝试把活着的对象放在Survivoro中(Minor GC)
- 如果Survivoro可以放入,那么放入之后清除Eden区
- 如果不可以放入,那么尝试把Eden和Survivoro的存活对象放入Survivor1中
- 如果Survivor1可以放入,那么放入Survivor1之后清除Eden和Survivoro,之后再把Survivor1中的对象复制到Survivoro中,始终保持一方为空
- 如果Survivor1不可以放入,那么直接把它们放入老年代中,并清除Eden和Survivoro,这个过程叫分配担保
总共的分配顺序:新生代(Eden=>Survivoro=>Survivoro1)=> 老年代
-
大对象直接进入老年代
大对象是指需要连续内存空间的对象 -
长期存活的对象进入老年代
-
动态判断对象的年龄
-
空间分配担保
什么是新生代老年代GC
新生代:
- 一个Eden区
- 两个Survivor区
老年代
默认新生代和老年代的比例值是1:2
默认Eden:from:to=8:1:1
-
新生代GC:指发生在新生代的垃圾收集动作,因为Java对象大多生命周期不会太长,所以MinorGC非常频繁,一般回收速度也比较快
-
老年代GC:指发生在老年代的GC,出现了MajorGC
-
young GC:对象优先在新生代Eden区分配,如果Eden区没有足够的空间时,就会触发一次YoungGC
-
Full GC:
1. 在执行young GC之前,JVM会进行空间分配担保,如果老年代的连续空间小于新生代对象的总大小,会触发一次full gc
2. 大对象直接进入老年代,从年轻代晋升上来的老对象,尝试在老年代分配内存时,但是老年代内存空间不够
3. system.gc()显示调用
调优
GC优化配置
链接: JVM参数调优
排查Full GC频繁问题
链接: 线上 Full GC 频繁的排查
JVM类加载机制
类加载器发生的时机
- new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类还没进行初始化,则需要先触发其初始化
- 使用java.lang.reflect包的方法进行反射调用的时候
- 当初始化一个类的时候,如果父类未初始化,先触发父类初始化
- 当虚拟机启动时,需要一个指定的主函数执行,则会先初始化该主类
类如何加载class文件
第一阶段:加载,把.class文件加载到内存中
第二阶段:连接(字节码验证、class类数据结构分析及相应内存分配、符号表解析)
第三阶段:类中静态属性和初始化赋值,静态块的执行
-
加载:
1. 通过一个类的全限定名获取其定义的二进制字节流
2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
3. 在java堆中生成一个代表这个类的对象,作为对方法区中这些数据的访问入口 -
连接(验证):保证被加载类的正确性
1. 文件格式验证:判断字节流是否符合class文件格式的规范
2. 元数据验证:对字节码描述的信息进行语义分析
3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
4. 符号引用验证:确保解析动作能正确执行 -
连接(准备):为静态变量分配内存,并初始化
1. 这时候进行内存分配的仅是static变量,并不包括实例变量,实例变量会在对象实例化时跟随对象一块分配在java堆中
2. 初始值默认为各个类型的零值,而不是被java代码中显示的赋予的真实值
3. 如果类字段属性同时被final和static修饰,那么准备阶段变量就会被初始化为类指定所属值 -
连接(解析):把类中的符号引用转换为直接引用
1. 符号引用:一组符号来描述目标
2. 直接饮用:直接指向目标的指针、偏移量 -
初始化
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
双亲委派模型
- Bootstrap ClassLoader:根类加载器,负责加载java的核心类(JVM自身实现)
- Exension ClassLoader:扩展类加载器,记载JDK目录下jre/lib/ext
- System ClassLoader:应用类加载器,根据指定的classpath环境变量加载当前路径下的类
虚拟机是如何判断两个类时相同的
类的全名一致,加载此类的类加载器一样
双亲委派模型的工作过程
-
当前ClassLoader首先从自己已经加载的类中,查询是否此类已经加载,如果已经加载则直接返回已经加载的类
-
当前ClassLoader的缓存中没有找到被加载的类的时候
1. 委托父类加载器去加载,父类加载器也会采用相同的策略,先查看自己的缓存,在委派父类的父类,直到Bootstrap ClassLoader
2. 当所有的父类加载器都没有加载时,再由当前的类加载器加载,放入缓存
为什么优先使用父加载器
- 共享功能:避免重复加载
- 隔离功能:为了安全性,避免自己写的类动态替换java核心类
破坏双亲委派模型
只需要在loadClass方法中,不调用父类的类加载方法区加载类,那么就成功了,那么只需要覆盖lloadClass方法,不去使用父ClassLoader方法区加载类即可