1.
什么情况下会发生栈内存溢出。
栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储
局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用
类型
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError
异常,方法递
归调用产生这种结果。
如果
Java
虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成
扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么
Java
虚拟机将抛出一
个
OutOfMemory
异常。
(
线程启动过多
)
参数
-Xss
去调整
JVM
栈的大小
2.
详解
JVM
内存模型
JVM
内存结构
程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程
私有。
Java
虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。
Native
方法栈:和虚拟栈相似,只不过它服务于
Native
方法,线程私有。
Java
堆:
java
内存最大的一块,所有对象实例、数组都存放在
java
堆,
GC
回收的地方,线程共享。
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),
回收目标主要是常量池的回收和类型的卸载,各线程共享
3.JVM
内存为什么要分成新生代,老年代,持久代。新生代中为什么
要分为
Eden
和
Survivor
。
1
)共享内存区划分
共享内存区
=
持久带
+
堆
持久带
=
方法区
+
其他
Java
堆
=
老年代
+
新生代
新生代
= Eden + S0 + S1
2
)一些参数的配置
默认的,新生代
( Young )
与老年代
( Old )
的比例的值为
1:2
,可以通过参数
–XX:NewRatio
配
置。
默认的,
Edem : from : to = 8 : 1 : 1 (
可以通过参数
–XX:SurvivorRatio
来设定
)
Survivor
区中的对象被复制次数为
15(
对应虚拟机参数
-XX:+MaxTenuringThreshold)
3)
为什么要分为
Eden
和
Survivor?
为什么要设置两个
Survivor
区?
如果没有
Survivor
,
Eden
区每进行一次
Minor GC
,存活的对象就会被送到老年代。老年代很快被
填满,触发
Major GC.
老年代的内存空间远大于新生代,进行一次
Full GC
消耗的时间比
Minor GC
长得多
,
所以需要分为
Eden
和
Survivor
。
Survivor
的存在意义,就是减少被送到老年代的对象,进而减少
Full GC
的发生,
Survivor
的预筛选
保证,只有经历
16
次
Minor GC
还能在新生代中存活的对象,才会被送到老年代。
设置两个
Survivor
区最大的好处就是解决了碎片化,刚刚新建的对象在
Eden
中,经历一次
Minor
GC
,
Eden
中的存活对象就会被移动到第一块
survivor space S0
,
Eden
被清空;等
Eden
区再满
了,就再触发一次
Minor GC
,
Eden
和
S0
中的存活对象又会被复制送入第二块
survivor space
S1
(这个过程非常重要,因为这种复制算法保证了
S1
中来自
S0
和
Eden
两部分的存活对象占用连续
的内存空间,避免了碎片化的发生)
4. JVM
中一次完整的
GC
流程是怎样的,对象如何晋升到老年代
Java
堆
=
老年代
+
新生代
新生代
= Eden + S0 + S1
当
Eden
区的空间满了,
Java
虚拟机会触发一次
Minor GC
,以收集新生代的垃圾,存活下来的对
象,则会转移到
Survivor
区。
大对象
(需要大量连续内存空间的
Java
对象,如那种很长的字符串)
直接进入老年态
;
如果对象在
Eden
出生,并经过第一次
Minor GC
后仍然存活,并且被
Survivor
容纳的话,年龄设为
1
,每熬过一次
Minor GC
,年龄
+1
,
若年龄超过一定限制(
15
),则被晋升到老年态
。即
长期存
活的对象进入老年态
。
老年代满了而
无法容纳更多的对象
,
Minor GC
之后通常就会进行
Full GC
,
Full GC
清理整个内存
堆
–
包括年轻代和年老代
。
Major GC
发生在老年代的
GC
,
清理老年区
,经常会伴随至少一次
Minor GC
,
比
Minor GC
慢
10
倍以上
。
5.
你知道哪几种垃圾收集器,各自的优缺点,重点讲下
cms
和
G1
,包
1
)几种垃圾收集器:
Serial
收集器:
单线程的收集器,收集垃圾时,必须
stop the world
,使用复制算法。
ParNew
收集器:
Serial
收集器的多线程版本,也需要
stop the world
,复制算法。
Parallel Scavenge
收集器:
新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达
到一个可控的吞吐量。如果虚拟机总共运行
100
分钟,其中垃圾花掉
1
分钟,吞吐量就是
99%
。
Serial Old
收集器:
是
Serial
收集器的老年代版本,单线程收集器,使用标记整理算法。
Parallel Old
收集器:
是
Parallel Scavenge
收集器的老年代版本,使用多线程,标记
-
整理算法。
CMS(Concurrent Mark Sweep)
收集器:
是一种以获得最短回收停顿时间为目标的收集器,
标
记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除
,收集结束会产生大量空间碎
片。
G1
收集器:
标记整理算法实现,
运作流程主要包括以下:初始标记,并发标记,最终标记,筛选
标记
。不会产生空间碎片,可以精确地控制停顿。
2
)
CMS
收集器和
G1
收集器的区别:
CMS
收集器是老年代的收集器,可以配合新生代的
Serial
和
ParNew
收集器一起使用;
G1
收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
CMS
收集器以最小的停顿时间为目标的收集器;
G1
收集器可预测垃圾回收的停顿时间
CMS
收集器是使用
“
标记
-
清除
”
算法进行的垃圾回收,容易产生内存碎片
G1
收集器使用的是
“
标记
-
整理
”
算法,进行了空间整合,降低了内存空间碎片。
6.JVM
内存模型的相关知识了解多少,比如重排序,内存屏障,
happen-before
,主内存,工作内存。
1
)
Java
内存模型图:
Java
内存模型规定了所有的
变量都存储在主内存
中,每条
线程还有自己的工作内存
,线程的工作内存中
保存了该线程中是用到的变量的主内存副本拷贝,
线程对变量的所有操作都必须在工作内存中
进行,
而
不能直接读写主内存
。不同的线程之间也
无法直接访问对方工作内存中的变量
,线程间变量的传递均需
要自己的工作内存和主存之间进行数据同步进行。
2
)指令重排序。
在这里,先看一段代码
运行结果可能为
(1,0)
、
(0,1)
或
(1,1)
,也可能是
(0,0)
。因为,在实际运行时,代码指令可能并不是严格按
照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(
out-of-order execution
,简
称
OoOE
或
OOE
)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取
下一条指令所需数据时造成的等待
3
。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是
指令重排
。
3
)内存屏障
内存屏障
,也叫内存栅栏,是一种
CPU
指令,用于控制特定条件下的重排序和内存可见性问题。
LoadLoad
屏障
:对于这样的语句
Load1; LoadLoad; Load2
,在
Load2
及后续读取操作要读取的
数据被访问前,保证
Load1
要读取的数据被读取完毕。
StoreStore
屏障
:对于这样的语句
Store1; StoreStore; Store2
,在
Store2
及后续写入操作执行
前,保证
Store1
的写入操作对其它处理器可见。
LoadStore
屏障
:对于这样的语句
Load1; LoadStore; Store2
,在
Store2
及后续写入操作被刷出
前,保证
Load1
要读取的数据被读取完毕。
StoreLoad
屏障
:对于这样的语句
Store1; StoreLoad; Load2
,在
Load2
及后续所有读取操作执行
前,保证
Store1
的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实
现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
4
)
happen-before
原则
单线程
happen-before
原则
:在同一个线程中,书写在前面的操作
happen-before
后面的操作。
锁的
happen-before
原则:同一个锁的
unlock
操作
happen-before
此锁的
lock
操作。
volatile
的
happen-before
原则
:对一个
volatile
变量的写操作
happen-before
对此变量的任意操
作
(
当然也包括写操作了
)
。
happen-before
的传递性原则
:如果
A
操作
happen-before B
操作,
B
操作
happen-before C
操
作,那么
A
操作
happen-before C
操作。
线程启动的
happen-before
原则
:同一个线程的
start
方法
happen-before
此线程的其它方法。
线程中断的
happen-before
原则
:对线程
interrupt
方法的调用
happen-before
被中断线程的检测
到中断发送的代码。
线程终结的
happen-before
原则:
线程中的所有操作都
happen-before
线程的终止检测。
对象创建的
happen-before
原则:
一个对象的初始化完成先于他的
finalize
方法调用。
7.
简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。
1)
什么是类加载器?
类加载器
就是根据指定全限定名称将
class
文件加载到
JVM
内存,转为
Class
对象。
启动类加载器(
Bootstrap ClassLoader
):由
C++
语言实现(针对
HotSpot
)
,
负责将存放在
<JAVA_HOME>\lib
目录或
-Xbootclasspath
参数指定的路径中的类库加载到内存中。
其他类加载器:由
Java
语言实现,继承自抽象类
ClassLoader
。如:
扩展类加载器(
Extension ClassLoader
):负责加载
<JAVA_HOME>\lib\ext
目录或
java.ext.dirs
系统变量指定的路径中的所有类库。
应用程序类加载器(
Application ClassLoader
)。负责加载用户类路径(
classpath
)上
的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载
器默认就是用这个加载器。
2
)双亲委派模型
双亲委派模型工作过程是:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派
给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的
类时(即
ClassNotFoundException
),子加载器才会尝试自己去加载。
双亲委派模型图:
3
)为什么需要双亲委派模型?
在这里,先想一下,如果没有双亲委派,那么用户是不是可以
自己定义一个
java.lang.Object
的同名
类
,
java.lang.String
的同名类
,并把它放到
ClassPath
中
,
那么
类之间的比较结果及类的唯一性将无法
保证
,因此,为什么需要双亲委派模型?
防止内存中出现多份同样的字节码
4
)怎么打破双亲委派模型?
打破双亲委派机制则不仅
要继承
ClassLoader
类,还要
重写
loadClass
和
findClass
方法。
8.
说说你知道的几种主要的
JVM
参数
1
)堆栈配置相关
-Xmx3550m
:
最大堆大小为
3550m
。
-Xms3550m
:
设置初始堆大小为
3550m
。
-Xmn2g
:
设置年轻代大小为
2g
。
-Xss128k
:
每个线程的堆栈大小为
128k
。
-XX:MaxPermSize
:
设置持久代大小为
16m
-XX:NewRatio=4:
设置年轻代(包括
Eden
和两个
Survivor
区)与年老代的比值(除去持久代)。
-XX:SurvivorRatio=4
:
设置年轻代中
Eden
区与
Survivor
区的大小比值。设置为
4
,则两个
Survivor
区
与一个
Eden
区的比值为
2:4
,一个
Survivor
区占整个年轻代的
1/6
-XX:MaxTenuringThreshold=0
:
设置垃圾最大年龄。如果设置为
0
的话,则年轻代对象不经过
Survivor
区,直接进入年老代。
2
)垃圾收集器相关
-XX:+UseParallelGC
:
选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20
:
配置并行收集器的线程数
-XX:+UseConcMarkSweepGC
:
设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction
:由于并发收集器不对内存空间进行压缩、整理,所以运行一段
时间以后会产生
“
碎片
”
,使得运行效率降低。此值设置运行多少次
GC
以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection
:
打开对年老代的压缩。可能会影响性能,但是可以消除碎
片
3
)辅助信息相关
-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
9.
怎么打出线程栈信息。
可以说一下
jps
,
top
,
jstack
这几个命令,再配合一次排查线上问题进行解答。
输入
jps
,获得进程号。
top -Hp pid
获取本进程中所有线程的
CPU
耗时性能
jstack pid
命令查看当前
java
进程的堆栈状态
或者
jstack -l > /tmp/output.txt
把堆栈信息打到一个
txt
文件。
可以使用
工具
堆栈定位
10.
强引用、软引用、弱引用、虚引用的区别?
1
)强引用
我们平时
new
了一个对象就是强引用,例如
Object obj = new Object();
即使在内存不足的情况下,
JVM
宁愿抛出
OutOfMemory
错误也不会回收这种对象。
2
)软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会
回收这些对象的内存。
3
)弱引用
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦
发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
4
)虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚
引用主要用来跟踪对象被垃圾回收器回收的活动。