1.JAVA内存结构
Java虚拟机管理的内存包括几个运行时数据内存:方法区、虚拟机栈、堆、本地方法栈、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区。
1.1 程序计数器
每个线程拥有一个PC寄存器
在线程创建时创建
指向下一条指令的地址
执行本地方法时,PC的值为undefined
1.2 方法区
保存装载的类信息
类型的常量池
字段,方法信息
方法字节码
通常和永久区(Perm)关联在一起
1.3 堆内存
和程序开发密切相关
应用系统对象都保存在Java堆中
所有线程共享Java堆
对分代GC来说,堆也是分代的
GC管理的主要区域
1.4 Java虚拟机栈
核心:一个线程一个栈,一个方法一个栈帧
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈内存就是虚拟机栈,或者说是虚拟机栈中局部变量表的部分。局部变量表存放了编辑期可知的各种基本数据类型(boolean、byte、char、short、int、long、double、float)、对象引用(reference)类型和returnAddress类型(指向了一条字节码指令的地址)
其中64位长度的long和double类型的数据会占用两个局部变量空间,其余的数据类型只占用1个。
Java虚拟机规范对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
1.5 本地方法栈
本地方法栈和虚拟机栈发挥的作用是非常类似的,他们的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
1.6 运行时常量池
它是方法区的一部分。class文件中除了有关的版本、字段、方法、接口等描述信息外、还有一项信息是常量池,用于存放编辑期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
Java语言并不要求常量一定只有编辑期才能产生,也就是可能将新的常量放入池中,这种特性被开发人员利用得比较多是便是String类的intern()方法。
当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
2.堆内存的构成
现在的GC基本都采用分代收集算法,如果是分代的,那么堆也是分代的。如果堆是分代的,那堆空间应该是下面这个样子:
新生代:当一个对象被创建的时候,特别大的对象放在老年代,普通对象分配在年轻代,大部分对象创建以后都不再使用,对象很快变得不可达,就是对象无用,由于垃圾是被年轻代清理掉的,所以被叫做Minor GC或者Young GC。
过程:不大的新生对象new出来,放在Eden区,第一次GC后所有幸存对象放在survivor 区1,然后又有对象在Eden区new出来,第二次GC后,Eden和survivor区1中的幸存对象全都复制到survivor区2中,也就是survivor from和survivor to。经过多次GC仍然没有被回收的幸存对象转入老年代。
老年代:对象如果在创建时就非常大,或者在新生代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。
3.垃圾收集
3.1 什么是垃圾?
建议画图理解,没有任何引用指向的对象叫垃圾(不完全对,比如互相引用造成的垃圾、环形引用的垃圾)
String ss = new String("laji");
ss = null;
这样ss就是垃圾了。
3.2 什么是引用
强引用就是在程序代码中普遍存在的,类似Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用是用来描述一些还有用但并非必须的元素。对于它在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二回收,如果这次回收还没有足够的内存才会抛出内存溢出异常。
弱引用是用来描述非必须对象的,但是它的强度比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象
虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
3.3 如何确定垃圾
3.3.1 引用计数器法
给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。 不过会有循环引用的问题,两个垃圾惺惺相惜。。。
3.3.2 可达性分析算法
顺藤摸瓜,能摸到的瓜都是好瓜。通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(用图论的话来说就是从GC Roots到这个对象不可达),则证明此对象是不可用的。
Java语言中GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈JNI(Native方法)引用的对象
3.4 垃圾收集算法
3.4.1 标记-清除算法
算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:一个是效率问题,标记和清除两个过程的效率都不高,另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 清除不是擦除,内容不会被马上清空,直到有新的的内容写入才会覆盖。
3.4.2 复制算法
它将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。
不足:内存浪费。
实际中我们并不需要按照1:1比例来划分内存空间,如堆内存的新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当另一个Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
3.4.3 标记压缩算法
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 主要用在老年代中
3.4.4 分代收集算法
只是根据对象存活周期的不同将内存划分为几块。一般把Java堆分成新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记压缩算法来进行回收。
3.5 垃圾收集器
a)Serial收集器: 【序列化的】
【数据量小于100M,单处理器】这个收集器是一个单线程的收集器,但它的单线程的意义不仅仅说明它会只使用一个CPU或一条收集线程去完成收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
b)ParNew收集器: 【并行的】
Serial收集器的多线程版本,除了使用了多线程进行收集以外,其余行为和Serial收集器一样
并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上。
c)Parallel Scanvenge 【并行的】
【峰值表现最重要,对于停顿可接受】该收集器是一个新生代收集器,它是使用复制算法的收集器,又是并行的多线程收集器。 并发量大,不过每次GC时,JVM需要停顿。
吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
d)Serial old收集器: 是Serial收集器的老年代版本,是一个单线程收集器,使用标记整理算法。
e)Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。
f)CMS收集器: 【并发的】
【对响应时间要求高】CMS收集器是基于标记清除算法实现的,整个过程分为4个步骤:1.初始标记 2.并发标记 3.重新标记 4.并发清除
优点:并发收集、停顿时间短
缺点:
- CMS收集器对CPU资源非常敏感,CMS默认启动的回收线程是(CPU数量+3)/4;
- CMS收集器无法处理浮动垃圾,可能出现Failure失败而导致一次Full G场地产生。
- CMS是基于标记清除算法实现的。
g)G1收集器: 【并发的】
它是一款面向服务器应用的垃圾收集器 ,不仅停顿短,同时并发量大。
- 并行与并发:利用CPU缩短STOP-The-World停顿的时间
- 分代收集
- 空间整合:不会产生内存碎片
- 可预测的停顿
运行方式:初始标记,并发标记,最终标记,筛选回收
3.6 JVM参数
——————————————————————————————————————————————————
———————————————————————————————————————————
4. JAVA对象的分配与VM调优
4.1 分配原则
对象分配先在栈上分配,如果分配不下再在TLAB分配,如果过大判断是否需要分配在老年代,最终分配在Eden区
4.1.1 栈上分配
- 线程私有小对象
- 无逃逸
- 支持标量替换
- 无需调整(使用虚拟机默认设置)
小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上,可以自动回收,减轻GC压力;大对象或者逃逸对象无法栈上分配。
4.1.2 线程本地分配TLAB (thread local allocation buffer)
- 占用Eden,默认1%,线程专属。这样就不需要加锁
- 多线程的时候不用竞争Eden就可以申请空间,提高效率
- 小对象
- 无需调整(使用虚拟机默认设置)
4.1.3 老年代
大对象(可以参数设置)
4.1.4 Eden空间
一般对象就在该区域创建
4.2 代码测试
代码:循环new 10000000个对象
4.2.1 测试1
配置:Run Configuration——>Arguments——>VM arguments输入
-XX: -DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB -XX:+PrintGC
解读:
-XX: -DoEscapeAnalysis (不)做逃逸分析(对象也就不能分配到栈上了);
-XX:-EliminateAllocations (不)做标量分析,-号表示取反,否定的意思;
-XX:-UseTLAB (不)使用线程本地缓存
-XX:+PrintGC 打印GC信息
-XX:+PrintGCDetails 打印GC详细信息(未使用)
也就是往Eden区分析
结果:
发生GC 775次,第一次GC,Eden区从6M下降到600KB
4.2.2 测试2
配置:使用线程本地缓存
-XX: -DoEscapeAnalysis -XX:-EliminateAllocations -XX:+UseTLAB -XX:+PrintGC
结果:
发生GC 下降到501次
4.2.3 测试3
配置:在栈上分配
-XX: +DoEscapeAnalysis -XX:+EliminateAllocations -XX:+UseTLAB -XX:+PrintGC
结果:
发生GC 下降到428次
4.2.4 测试4
使用JAVA方法
Runtime.getRuntime().totalMemory()
Runtime.getRuntime().freeMemory()
两者相减就是当前占用的空间大小
4.2.5 测试5
代码:
-
// -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=c:\tmp\jvm.dump -XX:+PrintGCDetails -Xms10M -Xmx10M
-
//-Xms10M程序起始分配内存 -Xmx10M程序最大分配内存
-
public static void main(String[] args) {
-
List<Object> lists = new ArrayList<>();
-
for(int i=0; i<100000000; i++) lists.add(new byte[1024*1024]);
-
}
说明:
将堆栈溢出信息打印到c:\tmp\jvm.dump文件夹中,使用visualVM软件可以查看
4.2.6 测试6
代码:
-
static int count = 0;
-
static void r() {
-
count++;
-
r();
-
}
-
public static void main(String[] args) {
-
try{
-
r();
-
}catch(Throwable t){
-
System.out.println(count);
-
t.printStackTrace();
-
}
-
}
说明:测试线程栈的大小
结果1:
程序异常退出,输出1102
优化:虚拟机VM arguments,配置堆stack的起始值(默认128k)
-Xss512k
结果2:
程序异常退出,输出9203
说明:
线程栈大小 -Xss 设置的小,线程的并发数量可以更多;设置的Xss大,线程递归调用更深,所以一个是“胖”,一个是“高”