目录
1、jvm运行时
1.1、程序计数器(线程私有)
1.2、本地方法栈
1.3、虚拟机栈(线程栈)(线程私有)
1.4、虚拟机堆(线程共享)
1.4.1、年轻代(GC频繁,复制算法)
1.4.2、老年代Tenured Gen(GC不频繁,标记清除法)
1.4.3、一些参数配置
1.4.4、为什么要分为Eden和Survivor?为什么要设置两个Survivor区?
1.5、方法区(线程共享)
1.5.1、永久代(java7)
1.5.2、元空间(java8)
2、GC算法
2.1、年轻代
2.2、老年代
2.3、System.gc()函数
3、GC回收器
3.1、分类
4、java内存模型(JMM)
4.1、并发内存模型的实质
4.1.1、原子性(Automicity)
4.1.2、可见性
4.1.3、有序性
5、指令重排序
5.1、as-if-serial
5.2、volatile关键字的作用
5.3、指令重排序例子
6、jvm参数
6.1、常用
6.2、内存参数
6.3、辅助信息参数
7、强、软、弱、虚引用
7.1、强引用
7.2、软引用
7.3、弱引用
7.4、虚引用
8、什么情况下会发生栈内存溢出
1、jvm运行时
1.1、程序计数器(线程私有)
- 作用:大致为字节码行号指示器
- 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
- 如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)
1.2、本地方法栈
1.3、虚拟机栈(线程栈)(线程私有)
- 存放基本类型的局部变量
- 局部变量也可能是指向一个对象的一个引用。引用存放在线程栈上,但是对象本身存放在堆上
- 一个对象可能包含方法,这些方法可能包含局部变量。这些局部变量仍然存放在线程栈上,即使这些方法所属的对象存放在堆上
1.4、虚拟机堆(线程共享)
- 存放所有对象,包括包装类
- 一个对象的成员变量随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型
- 静态成员变量跟随着类定义一起也存放在堆上
- java8开始将常量池和类的静态变量放在堆上
1.4.1、年轻代(GC频繁,复制算法)
- 新生代Eden Space(伊甸园)
- 幸存者区s0 Survivor Space
- 幸存者区s1 Survivor Space
1.4.2、老年代Tenured Gen(GC不频繁,标记清除法)
Major GC(或FULL GC) 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上
1.4.3、一些参数配置
- 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。
- 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)
- Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold)
1.4.4、为什么要分为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两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
1.5、方法区(线程共享)
- 方法区是java虚拟机规范去中定义的一种概念上的区域,具有什么功能,但并没有规定这个区域到底应该位于何处
- 存储类的元信息
- 运行时常量池在java7是方法区的一部分
1.5.1、永久代(java7)
只有hotspot虚拟机有永久代这个概念
1.5.2、元空间(java8)
由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出,所以java8开始方法区采用本地内存
2、GC算法
2.1、年轻代
年轻代采用复制算法
- 首先,当Eden区满的时候回触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活着的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄 + 1
- 然后,情况Eden和SurvivoFrom中的对象,也即复制之后有交换,谁空谁是To
- SurvivorTo和SurvivorFrom互换
- 最后,SUrvivoTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入老年代
2.2、老年代
- 标记清除法
- 标记压缩法
2.3、System.gc()函数
gc()函数的作用只是提醒虚拟机:希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策
3、GC回收器
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
3.1、分类
图中展示了7种不同分代的收集器:
- Serial:单线程的收集器,收集垃圾时,必须stop the world,使用复制算法
- ParNew: Serial收集器的多线程版本,也需要stop the world,复制算法
- Parallel Scavenge:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%
- Serial Old:是Serial收集器的老年代版本,单线程收集器,使用标记整理算法
- Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法
- CMS:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片
- G1:标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿
而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:
两个收集器间有连线,表明它们可以搭配使用:
- Serial+Serial Old
- Serial+CMS
- ParNew+Serial Old
- ParNew+CMS
- Parallel Scavenge+Serial Old
- Parallel Scavenge+Parallel Old
- G1
3.2、判断对象算法可回收的判定方法
引用计数法:引用加1,失效-1,为0时可回收,jvm没用这种方式,因为无法判定相互调用(A调用B,B调用A)
引用链法(可达性分析法):从一个被称为GCRoots对象开始向下搜索,如果一个对象到GCRoots对象没用任何相连,则说明此对象不可用
4、java内存模型(JMM)
java内存中线程的工作内存和主内存的交互是由java虚拟机定义了如下的8种操作来完成的,每种操作必须是原子性的(double和long类型在某些平台有例外,因为它们是64位的)
java虚拟机中主内存和工作内存交互,就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存同步回主内存
- lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
- unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
- read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
- load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
- use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
- assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
- store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
4.1、并发内存模型的实质
Java内存模型围绕着并发过程中如何处理原子性、可见性和顺序性这三个特征来设计的
4.1.1、原子性(Automicity)
由Java内存模型来直接保证原子性的变量操作包括read、load、use、assign、store、write这6个动作,虽然存在long和double的特例,但基本可以忽律不计,目前虚拟机基本都对其实现了原子性。如果需要更大范围的控制,lock和unlock也可以满足需求。lock和unlock虽然没有被虚拟机直接开给用户使用,但是提供了字节码层次的指令monitorenter和monitorexit对应这两个操作,对应到java代码就是synchronized关键字,因此在synchronized块之间的代码都具有原子性。
4.1.2、可见性
可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。
除了volatile,synchronized和final也可以实现可见性。synchronized关键字是通过unlock之前必须把变量同步回主内存来实现的,final则是在初始化后就不会更改,所以只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。
4.1.3、有序性
有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是“线程内表现为串行的语义”,后半句值得是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。
保证有序性的关键字有volatile和synchronized,volatile禁止了指令重排序,而synchronized则由“一个变量在同一时刻只能被一个线程对其进行lock操作”来保证。
总体来看,synchronized对三种特性都有支持,虽然简单,但是如果无控制的滥用对性能就会产生较大影响
5、指令重排序
不管是单线程还是多线程,都会发生指令重排序
指令重排序的意义:处理器在不影响最终计算结果的情况下,尽可能提高计算效率。
如何做到不影响计算最终计算结果?计算的时候是有数据依赖关系的
比如:
int a=0; //1
System.out.println(a); //2
如果有依赖关系,则1和2不会发生指令重排序。因此,指令重排序在单线程环境下不会有影响
但在多线程环境下有影响
编译器将不会对存在数据依赖性的程序指令进行重排,这里的依赖性仅仅指单线程情况下的数据依赖性;多线程并发情况下,此规则将失效
5.1、as-if-serial
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内
存可见性问题
5.2、volatile关键字的作用
- volatile关键字可以保证变量的可见性,因为对volatile的操作都在Main Memory中,而Main Memory是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或Cache,因为它们都不是全局的,无法保证可见性
- volatile还有一个作用就是局部阻止重排序的发生,对volatile变量的操作指令都不会被重排序,因为如果重排序,又可能产生可见性问题
但是volatile只能保证可见性,不能保证原子性
5.3、指令重排序例子
为什么指令重排序会对多线程程序有影响?
如下例子,线程内部相当于单线程,指令之间没有数据依赖性,可能会发生指令重排序
然后线程之间有数据依赖性,如果线程内部发生了执行重排序,势必会造成影响
public class CommandReorderTest {
static int a = 0;
static int b = 0;
static int x = 0;
static int y = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a=1;
x=b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b=1;
y=a;
}
});
one.start();
two.start();
//join方法的作用:挂起父线程,等待子线程执行完毕才能继续执行父线程
one.join();
two.join();
System.out.println("("+x+","+y+")");
}
}
如果不发生执行重排序,即x=b一定在a=1之后执行,y=a也一定在b=1之后执行,那么会有如下执行可能:
- a=1, x=b, b=1, y=a (0,1)
- a=1, b=1, y=a, x=b, (1,1)
- a=1, b=1, x=b, y=a (1,1)
- b=1, y=a, a=1, x=b (1,0)
- b=1, a=1, x=b, y=a (1,1)
- b=1, a=1, y=a, x=b (1,1)
而(0,1)和(1,0)这两种结果更多一点,因为one和two这两个线程内部就两个指令,一般不会交替执行
但是实际情况是,会发生执行重排序,那么最终程序输出还有一种结果(0,0)
6、jvm参数
6.1、常用
jdk1.7
-Xms512m -Xmx1024m -XX:PermSize=512M -XX:MaxPermSize=1024M
jdk1.8(由于jdk8开始,没有了永久区的概念,所以在jvm参数配置上不再需要-XX:PermSize和-XX:MaxPermSize)
-Xms512m -Xmx1024m -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024M
6.2、内存参数
- -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区,直接进入年老代
6.3、辅助信息参数
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGC -Xloggc:F:/test/gc.log 将GC日志输出到文件中
7、强、软、弱、虚引用
7.1、强引用
我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象
7.2、软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?
public class SoftReferenceTest {
public static void main(String[] args) {
//浏览器上一页
Browser prevPage = new Browser();
//浏览器当前页
Browser currentPage = null;
//将上一页转化为软引用
SoftReference<Browser> sr = new SoftReference<>(prevPage);
if(null == sr.get()){
//代表内存吃紧,被回收了
System.out.println("被回收");
}else{
//没有被回收
System.out.println(sr.get());
}
}
}
/**
* 浏览器页面对象
*/
class Browser{
}
7.3、弱引用
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
String str=new String("abc");
WeakReference<String> wr = new WeakReference<String>(str);
System.out.println(wr.get());
7.4、虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动
8、什么情况下会发生栈内存溢出
思路: 描述栈定义,再描述为什么会溢出,再说明一下相关配置参数,OK的话可以给面试官手写是一个栈溢出的demo
- a)栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型
- b)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常,方法递归调用产生这种结果
- c)如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多)
- d)参数 -Xss 去调整JVM栈的大小
- e)java.lang.*Error的例子
public class *ErrorTest {
public void exe(boolean b){
if(b){
exe(b);
}
}
public static void main(String[] args) {
*ErrorTest se = new *ErrorTest();
se.exe(true);
}
}
9、JDK工具
9.1、jps(JVM Process Status)
jps | grep 16116
jps -q 只显示pid
jps -m 输出传递给main方法的参数
jps -l 输出应用程序main class的完整package名或者应用程序的jar文件完整路径名
jps -v:输出jvm参数
jsp -V:输出通过flag文件传递到JVM中的参数