JVM内存结构
程序计数器:存储字节码文件执行到哪一行,用于线程上下文切换
本地方法栈:存储由C++实现的方法
堆:存储对象和数组
栈:存储多个栈帧,栈帧存储方法的信息(局部变量,参数列表,返回值)
方法区:存储类的相关信息(成员变量,构造函数)和运行时常量池
Java对象一定会分配在堆上吗
不一定,JIT优化机制会对对象做逃逸分析,当对象没有逃离到线程和方法外,会在栈上分配
直接内存
直接内存不是JVM内存结构,是操作系统的内存
常用于NIO操作,用于数据缓冲
分配回收成本较高,但是读写性能高
方法区的位置
方法区只是JVM规范中定义的一个概念,具体实现方案有 永久代(1.8以前)和元空间(1.8以后)
1.7版本之前方法区存储在堆中,1.8之后存储在直接内存中为了防止OOM
常量池和运行时常量池
常量池:可以看作一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型等信息
运行时常量池:当类被加载,他的常量池信息就会放入运行时常量池并把符号地址翻译成真实地址
类加载
什么是类加载器,类加载器分类
JVM只会运行二进制文件,类加载器的作用是将字节码文件加载到JVM中转化为二进制文件
启动类加载器:加载Java的核心库,加载 jre/lib目录下的类
扩展类加载器:加载Java的扩展库,加载 jre/ext目录下的类
应用类加载器:加载开发者自己编写的Java类
自定义类加载器:实现自定义加载规则
双亲委派机制
所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器能找到这个类,由上级加载,加载后该类也对下级加载器可见,找不到这个类,则下级类加载器才有资格执行加载
通过双亲委派可以避免一些类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
为了安全保证类库api不被修改
类加载的过程
-
通过类的全名,获取类的二进制数据流
-
解析类的二进制数据流存入方法区
-
在堆中创建类的class,作为方法区的访问入口
验证:检验类是否符合jvm规范,安全性检查
准备:为类变量分配内存并且设置类变量的初始值
解析:把类的符号引用变为直接引用
3.初始化
对类的静态变量,静态代码块执行初始化操作
垃圾回收机制
对象是如何创建的
1)类加载
当 JVM 遇到一条 new 指令时,是否能在常量池中定位到该类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析以及初始化。如果没有,则先执行类加载过程。
2)内存分配
类加载检查通过后,对象所需的内存空间大小在类加载完成之后便可确定,JVM 会根据垃圾回收器选取内存分配算法
-
指针碰撞:内存是连续的,内存指针移动基于对象大小移动。
-
空闲列表:通过维护一个空间内存列表,存放对象、分配内存还需考虑并发申请内存问题
3)初始化
第一步:设置初始值,比如 int 类型被初始化为0,对象被初始化为null
第二步:设置对象头,比如对象头中的hashcode、轻量级锁、GC分代年龄
第三步:调用构造方法
创建对象并发问题的解决方案
对分配内存空间的动作进行同步,即用CAS失败重试的方式;
把内存分配的动作按照线程划分在不同的空间中进行,每个线程在 Java 堆中预先分配一小块内存,即本地线程分配缓冲 TLAB
如何判断对象是否需要被回收
引用计数法
只要一个对象被另一个变量所引用,计数+1,当计数为0的时候就会被回收
存在循环引用问题会造成内存泄漏
可达性分析算法
对堆中的所有对象进行扫描,看所扫描的对象是否被根对象(GC Root)直接或者间接的引用,如果是则对象不能被回收反之则可以当作垃圾被回收。
那么GCroot对象有哪些呢?
1)虚拟机栈中引用的对象(在方法中new的对象)
2)方法区中(1.8称为元空间)的类静态属性引用的对象;(被static修饰的成员变量)
3)方法区中的常量引用的对象;(类静态常量引用的对象)
4)本地方法栈中的JNI(native方法)引用的对象。(c++源码中引用的对象)
5)持有synchronized锁的对象
6)活着的线程
四种引用
强引用:永不回收,Java 中默认引用
软引用:当垃圾回收之后内存依然不足就会被回收
弱引用:不管垃圾回收后内存是不是不足都会被回收
虚引用:如果一个对象只有虚引用就相当于没有引用,虚引用主要用于跟踪对象被垃圾回收的活动
垃圾回收算法
标记清除算法
标记:将可回收的对象进行标记
清除:把对象所占用内存的其实结束地址记录下来,下次再创建对象分配内存空间的时候将其覆盖掉即可
优点:清除速度很快
缺点:会产生内存碎片(这里的内存碎片跟OS里的一样)
标记整理算法
标记:将可回收的对象进行标记
整理:先使用紧凑技术将不被回收的对象占用内存变得紧凑并将要回收的内存释放掉
优点:没有内存碎片
缺点:效率较低,由于紧凑技术设计了对象内存的移动
复制算法
首先标记出不需要被回收的内存
然后将不需要被回收的内存从FROM区复制到TO区
最后释放FROM区所剩下的内存,并将FROM区和TO区调换
优点:不会产生碎片
缺点:会占用双倍内存空间
分代回收
新建的对象会在伊甸园中产生
当伊甸园内存不够时会触发MinorGC(标记不需要被回收的对象 并将其复制到到幸存区TO,并将幸存区的对象的寿命+1,最后回收掉伊甸园的所有内存并将From和To调换)
MinorGC会触发stop the world(暂停其他用户的线程,让垃圾回收优先进行,垃圾回收结束之后用户现线程再恢复运行)
to区存放当次GC幸存下来的对象,from区存放以前GC幸存下来的对象
当伊甸园内存再次不够时会触发第二次MinorGC(这次也会回收幸存区From需要被回收的对象)
当幸存区的对象寿命到达一个阈值(说明该对象价值很高),则会晋升到老年代
寿命阈值的最大值为是15(4bit)
当新生代晋升到老年代的对象较多使得老年代也内存不足,会先尝试MinorGC,如果MinorGC之后空间仍不足,会触发FullGC(把新生代老年代的对象都清除掉,会触发stop the world消耗更多时间)
垃圾回收器
串行垃圾回收器
单线程
安全点:在垃圾回收的过程中可能对象的地址会改变,为了保证对象地址安全,需要所有用户线程到达安全点再进行垃圾回收
吞吐量优先
-
eden内存不足发生Minor GC,标记复制STW
-
old内存不足会发生Full GC,标记整理STW
-
注重吞吐量(总的stw时间少)
触发GC时会有多个线程同时进行垃圾回收,并且垃圾回收时CPU会飙高
响应时间优先(CMS)
-
old并发标记,重新标记需要STW,并发清除
-
Faillback Full GC
-
注重响应时间(单次stw时间少)
初始标记:老年代空间不足时会发生初始标记(只标记GCRoot)会发生STW但是时间很短
并发标记:并发标记阶段用户线程和标记线程并发工作(标记线程继续标记要回收的对象)
重新标记:并发标记阶段可能会改变对象的引用所以需要重新标记,会发生STW
清理:用户线程和清理线程并发进行
三色标记和并发漏标问题
白色-未标记 黑色-已标记 灰色-正在标记
并发漏标问题解决方案
增量更新:只要赋值发生,该赋值对象就会被记录
原始快照:会记录新增引用和被删除的引用,结束并发标记阶段之后会重新检查一遍看看有没有遗漏
G1 GC
-
兼顾响应时间和吞吐量
-
划分多个区域,每个区域充当Eden,Survivor,Old,Humongous(存储大对象)
-
新生代回收:eden内存不足,标记复制STW
-
并发标记:old并发标记,重新标记时需要STW
-
混合收集:并发标记完成,开始混合收集,参与复制的
-
有eden、survivor、old,其中old会根据暂停时间目标,选择部分回收价值高的区域,复制时STW Failback Full GC
第一阶段:新生代垃圾回收
eden内存不足会使用复制算法把幸存对象复制到幸存区中,然后把eden区的的内存释放掉
eden内存再次不足会把eden和幸存区的对象都进行复制算法存储到一个新的幸存区,如果有对象达到了晋升老年代的年龄阈值,会复制到老年代,然后把原来的幸存区和eden内存释放掉
第二阶段:并发标记阶段
当老年代的内存占比达到一定阈值(默认45%)就会触发并发标记阶段
该阶段会在老年代标记出存活对象,不需要stw,但是重新标记(防止漏标)会发生stw
第三阶段:混合收集阶段
先挑出存活对象少的老年代进行标记用复制算法放到新的老年代中,同时eden和幸存区也会垃圾回收
可能会进行多次混合收集,多次混合收集后再次回到第一阶段进行循环
当垃圾回收的速度跟不上新对象产生的速度导致内存越来越满会进行fullGC
JVM实践
JVM调优参数
堆空间大小
如果堆太小垃圾回收会很频繁,如果堆太大单次垃圾回收时间会很长。
虚拟机栈的大小
如果设置太大会使得线程数减少,如果设置太小会出现频繁的栈内存溢出。
新生代在红eden和两个survivor的比例
默认值是8:1:1
新生代晋升到老年代的阈值
默认值是15,取值范围0-15
设置垃圾回收器
CMS、G1等
JVM调优工具
命令工具
-
jps:进程状态信息
-
jstack:查看java进程内线程的堆栈信息
-
jmap:查看堆转信息
-
jstat:JVM统计监测工具
可视化工具
-
jconsole:用于对jvm内存,线程,类的监控
-
visualVM 能够监控线程,内存情况
cpu飙高怎么处理
用top定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
jstack 进程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
OOM
内存溢出原因:
-
内存中加载数据过多,一次从数据库中取出太多数据集合类中有对象引用,使用后为清空
-
代码里有死循环或者循环产生过多重复对象
-
启动参数设置太小
解决方法
-
修改JVM启动参数(-Xms),直接增加内存
-
检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误
-
对代码进行Debug和分析,找出可能发生内存溢出的位置
-
使用内存查看工具动态查看内存使用情况