运行时数据区
栈管运行,堆管内存
-
一个进程 :一个jvm实例
-
一个jvm : 一个运行时数据区(Runtime)
-
一个Runtime :一个方法区一个堆
-
一个线程:一套程序计数器、本地方法栈、虚拟机栈。
-
一个进程:多个线程
堆
jvm管理最大一块内存
内部结构
jdk7:新生区、养老区、永久区
jdk8:新生区、养老区、元空间
约定:
新生区新生代年轻代
养老区老年区老年代
永久区==永久代
设置堆空间大小
如:-Xms10m -Xmx10m
虚拟机参数:
“-xms"用于表示堆区的起始内存,等价于-XX:InitialHeapsize
“-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapsize
-X是jvm的运行参数
ms是memory start
默认单位:byte 可写:k、m、g
通常会将-ms和一Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
- 默认情况下,初始内存大小:物理电脑内存大小/ 64
- 最大内存大小:物理电脑内存大小/ 4
// 查看堆内存
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
}
}
-
NewRatio:设置新生代与老年代的比例。默认值是1:2
-
在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1
虚拟机参数:
-XX:NewRatio :设置新生代与老年代的比例。默认值是2.
-XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例。默认值是8
-XX:-UseAdaptivesizePolicy :关闭自适应的内存分配策略―(暂时用不到)
-Xmn:设置新生代的空间的大小。(一般不设置)
对象分配内存过程
四个区:伊甸园、幸存0、幸存2、老年代
from。空=to
-
先放伊甸园
-
伊甸园满。发生YGC/Minor GC回收(红色部分),未回收的标识年龄放到幸存0。
-
幸存0满。伊甸园未回收放入幸存1。幸存0的未回收的标识年龄放入幸存1.
-
伊甸园满。未回收放入幸存0,幸存1未回收放入幸存0。不断往复。
-
幸存1某某达阈值,晋升到老年代
舍时候能去养老区呢?可以设置次数。默认是15次。
可以设置参数: -XX:MaxTenuringThreshold=进行设置。
伊甸园满发生YGC/Minor GC会把幸存0和幸存1一起回收。
80%左右的对象在新生代被销毁
YGC的时候会把伊甸园区清空,未回收的丢入幸存区。
总结
- 针对幸存者se,s1区的总结:复制之后有交换,谁空谁是to.
- 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。l
特殊对象分配
一、YGC清空伊甸园后,对象仍放不下:对象较大,直接放入老年代。
- 老年代也放不下:进行GC(FGC)
- GC后空间够,放入
- GC后仍不够,OOM(内存溢出)
除FullGC,还有majorGC
二、伊甸园YGC后,对象放入幸存区但放不下:直接放入老年代
伊甸园满以后进行一次GC
Minor GC、Major GC与Full GC
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
针对HotSpot VM的实现,它里面的Gc按照回收区域又分为两大种类型:
- 一种是部分收集(Partial Gc)
- 一种是整堆收集(Full GC)
部分收集
不是完整收集整个Java堆的垃圾收集。
- 新生代收集(Minor Gc / Young GC):只是新生代的垃圾收集
- 老年代收集(Major Gc / old GC):只是老年代的垃圾收集。
- 目前,只有CMS GC会有单独收集老年代的行为。
- 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC会有这种行为
整堆收集(Full GC)
收集整个java堆和方法区的垃圾收集。
年轻代(Minor GC)触发
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。每次Minor GC会清理年轻代的内存。
- 因为Java 对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快
- Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代GC (Major GC/Ful1 GC)触发
说明
- 指发生在老年代的Gc,对象从老年代消失时,说“Major GC”或“Full Gc发生了。
- 出现了Major Gc,经常会伴随至少一次的Ninor Gc(但非绝对的,在Parallel scavenge收集器的收集策略里就有直接进行Major Gc的策略选择过程)。
- 也就是在老年代空间不足时,会先尝试触发Ninor GC。如果之后空间还不足,则触发Major Gc。
- Major GC的速度一般会比Minor Gc慢10倍以上,STW的时间更长。
- major Gc后,内存还不足,就报OOM了
full GC触发
- (1)调用system.gc()时,系统建议执行Full Gc,但是不必然执行
- (2)老年代空间不足
- (3)方法区空间不足
- (4)通过Minor Gc后进入老年代的平均大小大于老年代的可用内存
- (5)由Eden区、survivor spaceo (From Space)区向survivor space1 (ToSpace)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。
虚拟机参数:
-XX:+PrintGCDetails 可查看gc一些细节。
内存分配策略
对象提升(Promotion)规则
针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果survivor区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- 空间分配担保:-xx:HandlePromotionFailure
对象分配过程:TLAB
堆空间为每个线程分配TLAB
为什么有TLAB ( Thread Local Allocation Buffer ) ?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
什么是TLAB ?
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
TLAB说明
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
- 在程序中,开发人员可以通过选项“-XX :UseTLAB”设置是否开启TLAB空间(+=开启,-=关闭)
- 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-xX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
堆空间一定是共享的吗?
no,在Eden中被每个线程所私有的空间TLAB
堆空间参数
官方:https: / /docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial 查看所有的参数的默认初始值
-XX:+PrintFlagsFinal 查看所有的参数的最终值(可能会存在修改,不再是初始值)
-xms 初始堆空间内存(默认为物理内存的1/64)一X:+PrintFlagsFinal -XX:SurvivorRatio=5`
-Xmx 最大堆空间内存(默认为物理内存的1/4)
-xmn 设置新生代的大小。(初始值及最大值)
-XX:NewRatio 配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio 设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄(0-15)
-XX:+PrintGcDetails 输出详细的GC处理日志
-XX:+PrintGc -verbose:gc 打印gc简要信息:
-XX:HandlLePromotionFailure 是否设置空间分配担保
在发生Minor Gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
- 大于,则此次Minor Gc是安全的
- 小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
- 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
- 大于,则尝试进行一次Minor Gc,但这次Minor cc依然是有风险的;
- 小于,则改为进行一次Full GC。
- 如果HandlePromotionFailure=false,则改为进行一次Full Gc
- 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。
JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor cc,否则将进行Full GC。
堆空间对象分配策略
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
- 例如作为调用参数传递到其他地方中。
// 没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。
public void my_method() {
v v = new V();
// use v
//l ......
v = null;
}
// stringBuffer对象逃逸
public static stringBuffer createStringBuffer (String s1,string s2){
stringBuffer sb =new stringBuffer() ;
sb.append(s1);
sb.append(s2);
return sb ;
}
// 上述代码如果想要stringBuffersb不逃出方法,可以这样写:
public static string createstringBuffer (String s1,string s2)
{
stringBuffer sb = new stringBuffer();
sb.append(s1) ;
sb.append(s2) ;
return sb.tostring();
}
是否逃逸,注意是
new的实体
是否在方法外被调用。例:
public class EscapeAnalysis{
public EscapeAnalysis obj;
/*
方法返回EscapeAnalysis对象,发生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null? new EscapeAnalysis() : obj;
}
/*
对象的作用域仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
/*
引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
}
}
参数:
在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。
如果使用的是较早的版本,开发人员则可以通过:
“-XX:+DoEscapeAnalysis"显式开启逃逸分析
“-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果。
开发中能使用局部变量的,就不要使用在方法外定义。
优化:栈上分配
栈上分配:将对分配转化为栈分配。
没有逃逸的对象会进行栈上分配。
// 实现栈上分配:打开逃逸分析 -XX:+DoEscapeAnalysispackage
com.atguigu.java2;
/**
* 栈上分配测试: -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails // 未开启逃逸分析
* @author shkstart shkstart@126.com
* @create 2020 10:31
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
// 查看执行时间
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(1000000);
} catch(InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();//未发生逃逸
}
static class User{}
}
优化:同步省略
同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 同步后果:降低并发性和性能
- 动态编译同步块时,JIT编译器可借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。取消同步的过程就叫同步省略,也叫锁消除。
public void f(o){
object hollis = new object () ;
synchronized (hollis){
system.out.println(hollis);
}
}
/*
代码中对hollis这个对象进行加锁,
但是hollis对象的生命周期只在f()方法中并不会被其他线程所访问到,
所以在JIT编译阶段就会被优化掉。优化成:
*/
public void f(o){
Object hollis = new Object();
system.out.println(hollis);
}
优化:分离对象/标量替换
分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在cPU寄存器中。
- 标量(scalar):指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
- 聚合量(Aggregate):还可以分解的数据。Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
- 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
*拆分的成员变量可以存储栈上,相当于变形的栈上分配。
// 开启标量替换: -XX:+EliminateAllocations
package com.atguigu.java2;
/**
* 标量替换测试
* -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
* @author shkstart shkstart@126.com
* @create 2020 12:01
*/
public class ScalarReplace {
public static class User {
public int id;
public String name;
}
public static void alloc() {
User u = new User();//未发生逃逸
u.id = 5;
u.name = "www.atguigu.com";
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
}
}
无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
逃逸分析虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
-
MinorGC:回收年轻代对象行为
-
MajorGG/FullGC:回收老年代对象时。
FullGC其实要回收整个堆空间,包括方法区。