栈分配空间太小,或执行的方法递归层数太多创建了太多的栈帧导致溢出
解决方案:配置-Xss参数增加线程栈大小,优化程序也至关重要
内存结构:
Eden区是一块,Survivor是两块,均属于堆中的新生代
Eden和Survivor的比例是8:1:1,可以通过-XX:SurvivorRatio来设定
从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中
Survivor具有预筛选保证,只有对象经历了16次Minor GC才会被送到老年代,Survivor可以减少被送到老年代的对象,进而减少Full GC发生。有两个Survivor,在Minor GC后,可以保证一个为空,另一个非空且无内存碎片
完整GC流程:
-Xss: 栈容量
-Xms:设置最小堆内存
-Xmx:设置最大堆内存
-Xmn10M:设置新生代10M
-XX:SurvivorRatio=8:设置Eden、Survivor比例8:1
-XX:PermSize=32M:永久代最小内存32M
-XX:MaxPermSize=64M:永久代最大扩展内存64M
-XX:+HeapDumpOnOutOfMemoryError:堆内存溢出时Dump出当前的内存堆转储快照以便事后分析
Serial收集器:新生代单线程的收集器,收集垃圾时,必须stop the world,使用复制算法
ParNew收集器:新生代Serial多线程版本收集器,也需要stop the world,使用复制算法
Paraller Scavenge收集器:新生代并发的多线程收集器,目标达到一个可控的吞吐量,使用复制算法
Serial Old收集器: 是Serial收集器的老年代版本,单线程,使用标记整理算法
Parallel Old收集器: Parallel Scavenge收集器的老年代版本,多线程,使用标记整理算法
CMS收集器:是一种以最短回收停顿时间为目标的老年代收集器,使用标记清除算法,运行过程:初始标记-并发标记-重新标记-并发清除,收集后会产生大量空间碎片
G1收集器:可作用于新生代与老年代,是标记整理算法,运行过程:初始标记-并发标记-最终标记-筛选标记,不会产生碎片,可以精确的控制停顿
- | CMS收集器 | G1收集器 |
---|---|---|
算法类型 | 标记-清除 | 标记-整理 |
收集范围 | 老年代 | 新生代、老年代 |
目标 | 最短的停顿时间为目标 | 可预测的垃圾回收时间 |
追问:GC ROOT有哪些?
JVM除了程序计数器其他区域都可能发生内存溢出
堆溢出OutOfMemoryError
用visualVM工具分析堆快照(-XX:+HeapDumpOnOutOfMemoryError)
如果发生内存泄漏
栈溢出
一般由于递归,导致栈空间不足,发生OutOfMemoryError:Java heap space说明运行时常量池移到了堆中
方法区溢出
方法区是存放类的地方。如果多个项目有多个相同jar,且都在WEB-INF/lib下,则每个项目都会加载一遍jar,会导致方法区有大量相同类,又不会被GC,则可建立共享lib库,否则尝试增加-XX:MaxPermSize
Java内存模型:
Java模型规定了所有的变量都存储在主内存中,每个线程有自己的工作内存,线程的工作内存保存了所使用变量主内存的副本拷贝。
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
不同线程之间无法访问对方工作内存的变量,线程间变量传递依赖于主内存。
指令重排:
处理器将指令乱序执行,可以大大提高执行效率,这就是指令重排
内存屏障:
内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序与内存可见性问题
happen-before原则:
happends-before是JMM最核心的概念,理解happends-befores是理解JMM的关键。
程序顺序规则:一个线程的每个操作,happends-before于该线程中任意的后续操作
监视器锁规则:对一个锁的解锁,happends-before于任意后续对这个锁的加锁
volatile变量规则:对一个volatile的写,happends-before于任意后续对该volatile域的读
传递性:如果A happends-before B 且 B happends-before,那么 A happends before C
start()规则: 如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start()操作happends before于线程B的任意操作
join()规则:如果线程A执行操作ThreadB.join()方法并成功返回,则线程B中的任意操作happends before于线程A从ThreadB.join()操作成功返回
什么是类加载器?
类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象
双亲委派模型:
如果一个类加载器收到类加载请求时,首先不会尝试自己加载这个类,而是把这个请求委派给父加载器完成。每个加载器都是如此,只有当父加载器在自己的搜索范围找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载
为什么需要双亲委派模型?
如果没有双亲委派,那么用户自定义一个java.lang.Object, java.lang.String类,并把它放在classpath中,那么类之间的比较结果及类的唯一性将无法保证。双亲委派模型,防止内存中出现多份同样的字节码
怎么打破双亲委派模型?
继承ClassLoader类,重写loadClass和findClass方法
思路: 可以说一下堆栈配置相关的,垃圾收集器相关的,还有一下辅助信息相关的。
堆栈配置相关
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurviorRatio=4 -XX:MaxTenuringThreshold=0
-Xmx3550m: 最大堆大小为3550m
-Xms3550m: 最小堆大小为3550m
-Xmn2g: 设置新生代大小为2g
-Xss128k: 设置每个线程的栈大小为128K
-XX:MaxPermSize: 设置永久代的大小为16m
-XX:NewRatio: 设置新生代(包括Eden和两个Survivor区)与老年代的比值(除去永久代)
-XX:SurvivorRatio: 设置新生代Eden与Survivor区的大小比值,设置为4,则两个Survivor区与Eden区比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold: 设置垃圾最大年龄。如果设置为0的话,则新生代不经过Survivor区,直接进入老年代
垃圾收集器相关
-XX:+UseParallelGC # 选择垃圾收集器为并行收集器
-XX:ParallelGCThreads=20 # 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC # 设置老年代为并发收集
-XX:CMSFullGCsBeforeCompaction=5 # 由于CMS收集器不对内存空间进行压缩整理,所以会产生内存碎片。此值设置运行5次Full GC后对内存空间压缩整理
-XX:+UseCMSCompactAtFullCollection # 打开对老年代的压缩,可能会影响性能,但是可以消除碎片
辅助信息相关
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintGC 输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
思路: 可以说一下jps,top,jstack这几个命令,再配合一次排查线上问题进行解答。
jps
,获取进程号top -Hp pid
,获取本进程所有线程的CPU耗时性能jstack pid
,查看当前java进程的堆栈信息jstack -l /tmp/output.txt
把堆栈信息打到一个txt文件欢迎关注公众号算法小生,本文持续更新中