JVM运行时数据区域
- 线程栈,每个线程对应一个线程栈 存储方法运行时产生的栈帧,方法的执行和退出就对应着栈帧的入栈和出栈;
1.1栈帧,每个方法调用都对应一个栈帧,栈帧有局部变量表,操作数栈,动态链接,方法返回地址等;
1.1.1局部变量表,方法运行时的局部变量。
1.1.2操作数栈,方法运行时数值的操作在这里进行
1.1.3动态链接,在类加载时将静态方法由符号引用转为直接引用叫静态链接,这里是当方法调用时再将符号引用转为直接引用称为动态链接;
1.1.4方法返回地址,如A 方法调用B 方法,B方法的返回地址就记录执行结束后应该返回A方法的哪一行继续执行;
2.程序计数器,每个线程都包含一个程序计数器 表示当前正在执行的字节码指令的行号;
3.本地方法栈,本地方法调用时需要用到的内存区域
4.堆内存,java中基本所有创建的实例对象都存储在堆内存中
5.方法区(元空间):存储常量,静态变量,类的元信息等。
拿一个简单的示例分析一下各内存区域
public class Math {
public int computer(){
int a=10;
int b=5;
int c=a+b;
return c;
}
public static void main(String[] args) {
Math math=new Math();
int computer = math.computer();
System.err.println(computer);
}
}
通过javap执行得到Math.class 的反汇编代码,只拿computer方法来分析;对应的字节码指令含义可以查阅字节码指令手册
0: bipush 10
2: istore_1
3: iconst_5
4: istore_2
5: iload_1
6: iload_2
7: iadd
8: istore_3
9: iload_3
10: ireturn
bipush 将一个8位带符号整数压入栈,此时操作数栈压入10
istore_1 将int类型值存入局部变量1 ,设置a=10
iconst_5 将int类型常量5压入栈
istore_2 将int类型值存入局部变量2,设置b=5
iload_1 从局部变量1中装载int类型值
iload_2 从局部变量2中装载int类型值
iadd 执行int类型的加法 10 + 5,此时操作数栈中的数值出栈,进行加法运算后得到的值入栈
istore_3 将int类型值存入局部变量3,设置c=15
iload_3 从局部变量3中装载int类型值
ireturn 从方法中返回int类型的数据,返回main方法栈帧对应的位置
各内存区域调优参数
1.栈内存 : -Xss 设置单个线程栈最大内存
2.堆内存:-Xms 这只堆内存最小内存 -Xmx 设置堆内存最大内存
3.新生代:-Xmn 调整新生代的内存,一般新生代与老年代是整个堆内存的 1:2 ,新生代中 Eden :S1 :S2 为新生代的空间 8:1:1
4.方法区:-XX:MetaSpaceSize 指定元空间触发Full GC的阈值,默认是21M 当达到阈值时将触发Full GC进行类的卸载,同时垃圾收集器会对该值进行修改, 如果full gc回收了大量空间那么将调低触发阈值,如果回收只有少量空间将调大触发阈值;由于full gc比较重,在启动时发生大量full gc会导致启动过慢,可以将-XX:metaSpaceSize -XX:MaxMetaSpaceSize(最大元空间内存) 调整成一样的值并依据自己的服务器情况调整合适的值;
对象的创建流程
1.分配内存空间
1.指针碰撞
如果一块内存是规整的,jvm可以维护一个指针用过的内存放在一边,没用过的内存放在一边。分配新的内存时只需要将指针移动这个对象所占空间的大小就可以了;
2.空闲列表
jvm内维护一个空闲列表,分配新对象时从这个列表中查看哪块内存能使用。
内存分配存在的并发问题
在内存分配时很可能出现多个线程同时创建对象争夺一块内存空间,jvm使用以下方式解决;
1.CAS
2.线程分配缓冲(TLAB)
线程启动时给每个线程先在堆内存中分配部分空间,线程内创建对象时优先使用线程自己的这部分空间。空间使用完毕后再使用CAS去分配
赋初始值
把对象的成员变量赋初始值。int=0,Object=null
构造对象头
对象头中主要由3部分组成,头信息,实例数据信息,对齐填充。
1.头信息中包含3部分信息:MarkWord(描述对象的HashCode,锁标志位,分代年龄等),KlassPointer(所属类的元数据指针,存放在元空间的!),数组长度(只有对象是数组时才有)
markWord :32位机下是4字节,64位机下是8字节
无锁状态:25bit 存对象的hashCode,4bit 存分代年龄,1bit 偏向锁标记, 2bit 锁标志位 01
偏向锁:23bit 存偏向线程ID,2bit 存Epoch ,4bit 存分代年龄, 1bit 偏向锁标记, 2bit 锁标志位 01
轻量级锁:30bit 存线程栈中MarkWord副本的指针,2bit 锁标志位 00
重量级锁:30bit 存Mutex(重量级锁)的指针,2bit 锁标志位 10
KlassPointer:当前对象方法区 类的元信息,32位机是4字节, 64位机开启指针压缩是4字节,关闭指针压缩是8字节
数组长度:当对象是数组类型时 4字节表示数组长度
2.实例数据,根据基本数据类型所占字节数, 对象类型存的是引用指针 开压缩 4字节,不开8字节
3.对齐填充,要求所有的对象大小都要是8的倍数,所以在不满足时需要使用空白填充
执行init方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋 零值不同,这是由程序员赋的值),和执行构造方法。
对象内存分配
针对对象内存分配,jvm做了许多优化。如:栈上分配,大对象直接进入old区,长期存活的对象进入old区,动态年龄判断,old区分配担保机制;
栈上分配
在我们的印象中好像所有的对象实例创建都分配在堆内存,但是在jvm的优化手段中有一种基于逃逸分析的内存分配手段 栈上分配;
栈上分配要求 1.开启逃逸分析参数 2.开启标量替换参数 ,在jdk1.8都已经默认开启;
1.什么是逃逸分析?
以下面代码为例 test1方法有返回值不能确定user的使用范围, test2方法只是方法内部调用可以确定使用范围,那么这个对象不会发生逃逸;
public class EscapeAnalysis {
public Order test1(){
Order order=new Order();
order.setId(1);
order.setName("ossssssssssrder");
return order;
}
public void test2(){
Order order=new Order();
order.setId(1);
order.setName("ossssssssssrder");
}
}
2.怎么说明栈上分配的存在?jdk1.8 默认是开启逃逸分析与栈上分配的,我们已开启和关闭状态各调用test2方法一亿次,观察gc
首先是开启状态,一共调用了两次GC
-XX:+PrintGC -Xms10m -Xmx10m
再来关闭逃逸分析和栈上分配,可以发现gc很频繁
-XX:-DoEscapeAnalysis -XX:-EliminateAllocations
对象优先在Eden区分配
-XX:+PrintGCDetails 打印gc详细日志,首先观察一下我本机的 Eden为65M,S1,S2为10M;Old为175M
Heap
PSYoungGen total 75776K, used 5202K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
eden space 65024K, 8% used [0x000000076b700000,0x000000076bc14940,0x000000076f680000)
from space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
to space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
ParOldGen total 173568K, used 0K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
object space 173568K, 0% used [0x00000006c2400000,0x00000006c2400000,0x00000006ccd80000)
Metaspace used 3113K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 339K, capacity 388K, committed 512K, reserved 1048576K
下面我填充一个55M的大对象,再查看内存占用情况; 可以看到Eden区使用了94%
//-XX:PrinterGCDetails
public class GCTest {
public static void main(String[] args) {
byte[] arg1=new byte[1024*55000];
}
}
Heap
PSYoungGen total 75776K, used 61502K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
eden space 65024K, 94% used [0x000000076b700000,0x000000076f30fb50,0x000000076f680000)
from space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
to space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
ParOldGen total 173568K, used 0K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
object space 173568K, 0% used [0x00000006c2400000,0x00000006c2400000,0x00000006ccd80000)
Metaspace used 3297K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
,那么继续添加对象会是什么情况呢? 再创建一个8M的对象,观察内存情况;可以看到之前Eden区的55M对象已经到了Old区,是因为S1,S2 都存不下; 刚创建的8M对象存在Eden区
public class GCTest {
public static void main(String[] args) {
byte[] arg1=new byte[1024*55000];
byte[] arg2=new byte[1024*8000];
}
}
[GC (Allocation Failure) [PSYoungGen: 60201K->776K(75776K)] 60201K->55784K(249344K), 0.0253236 secs] [Times: user=0.16 sys=0.03, real=0.03 secs]
Heap
PSYoungGen total 75776K, used 9426K [0x000000076b700000, 0x0000000774b00000, 0x00000007c0000000)
eden space 65024K, 13% used [0x000000076b700000,0x000000076bf72a78,0x000000076f680000)
from space 10752K, 7% used [0x000000076f680000,0x000000076f742020,0x0000000770100000)
to space 10752K, 0% used [0x0000000774080000,0x0000000774080000,0x0000000774b00000)
ParOldGen total 173568K, used 55008K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
object space 173568K, 31% used [0x00000006c2400000,0x00000006c59b8010,0x00000006ccd80000)
Metaspace used 3298K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
大对象直接进入Old区
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大 对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。以下是示例:
Jdk1.8 默认垃圾收集器为Parallel收集器,大对象设置不会生效; -XX:PretenureSizeThreshold=100000
//默认垃圾收集器的情况下,虽然设置了-XX:PretenureSizeThreshold=100000 但是大对象并没有直接进入老年代;
public class GCTest {
public static void main(String[] args) {
// byte[] arg1=new byte[1024*55000];
byte[] arg2=new byte[1024*8000];
}
}
Heap
PSYoungGen total 75776K, used 14502K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
eden space 65024K, 22% used [0x000000076b700000,0x000000076c529b50,0x000000076f680000)
from space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
to space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
ParOldGen total 173568K, used 0K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
object space 173568K, 0% used [0x00000006c2400000,0x00000006c2400000,0x00000006ccd80000)
Metaspace used 3284K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
//添加 -XX:+UserSerialGC 参数后,可以发现大对象直接进入了老年代
Heap
def new generation total 78016K, used 5550K [0x00000006c2400000, 0x00000006c78a0000, 0x0000000716d50000)
eden space 69376K, 8% used [0x00000006c2400000, 0x00000006c296b9f8, 0x00000006c67c0000)
from space 8640K, 0% used [0x00000006c67c0000, 0x00000006c67c0000, 0x00000006c7030000)
to space 8640K, 0% used [0x00000006c7030000, 0x00000006c7030000, 0x00000006c78a0000)
tenured generation total 173440K, used 8000K [0x0000000716d50000, 0x00000007216b0000, 0x00000007c0000000)
the space 173440K, 4% used [0x0000000716d50000, 0x0000000717520010, 0x0000000717520200, 0x00000007216b0000)
Metaspace used 3212K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
长期存活的对象进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在 老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代 的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
对象动态年龄判断
新生代采用复制算法,发生youngGC时 会将存活的对象往空闲的S1 或 S2 区转移,转移时会判断S区内的各个分代年龄的对象是否大于S区的一半,如果大于就把大于等于某个分代年龄的对象转移到老年代; 比如S区有分代年龄为 age1+ age2 +age3+age4 的对象,其中age1+age2 的对象就已经超过了S区的一半,那么 大于等于分代年龄2的对象都被转移到老年代
老年代空间分配担保机制
年轻代每次youngGC之前JVM都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会再去判断“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了, 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每次youngGc后进入老年代对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾, 如果回收完还是没有足够空间存放新的对象就会发生"OOM" .
如果youngGC之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放youngGC之后的存活对象,则也会发生“OOM”.
垃圾对象判断算法
1.引用计数法,每当一个对象被引用计数器+1, 为0时可以回收。 无法解决循环引用的问题;
2.可达性分析算法,使用一系列GCRoots作为起点,沿着GcRoots往下查找,查找经过的链路叫做引用链,不能与GcRoots连接的对象是垃圾对象;
可作为GCroots节点的有:局部变量表中的引用类型变量,静态变量,常量等
类的卸载
类的元信息主要存储在方法区,方法区也会发生GC,主要是针对类元信息的回收,条件比较苛刻要满足以下3点:
1.该类所有的实例对象都是垃圾对象
2.加载该类的ClassLoader已是垃圾对象
3.该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射调用获取实例;
正常项目中不会发生类的回收,因为正常项目中只有3个ClassLoader 分别是BootStrap,ExtClassloader,AppClassLoader 这3个对象不可能被回收,所以类也不会被卸载。 只有在自定义ClassLoader的情况下才可能发生类型的卸载