1.JVM内存模型
蓝色为进程共享,黄色为线程共享
-Xms 为jvm启动时分配的内存,比如-Xms200m,表示分配200M
-Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存
-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M
程序计数器
较小的内存空间,当前线程的所执行的字节码的行号指示器,通过此来选取下一条所需要的执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要依赖计数器。线程切换时需要保存PC计数器的状态。
虚拟机栈
虚拟机栈描述了java方法执行的内存模型,指的是java内存中的栈内存,内部含有局部变量表。
每个方法执行的时候都会创建栈帧,用于存储局部变量表(存放局部变量、对象引用)、操作数据栈(运算数据)、动态链接、方法出口等信息
异常:
StackOverflow Error,虚拟机栈内存不允许动态扩展大小,当线程请求栈内存达到虚拟机栈的最大深度(递归层次太深),则跑出该异常
OutOfMemory Error,线程请求栈内存用完了,无法动态扩展了,则抛出该异常
本地方法栈
为native方法创建的栈区,调用非java方法(例如C/C++)时使用
方法区
存储已被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码数据
运行时常量池
是方法区的一部分,用于存储常量等信息,主要分为
字面量:文本字符串,final常量,基本数据类型的值(1,true等)
符号引用:类和结构的完全限定名(类名、枚举名),字段名和描述符,方法名和描述符
2.堆-内存模型
参数
-Xms | 初始堆大小。如:-Xms256m |
-Xmx | 最大堆大小。如:-Xmx512m |
-Xmn | 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% |
-Xss | JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。 |
-XX:NewRatio | 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio | 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
-XX:PermSize | 永久代(方法区)的初始大小 |
-XX:MaxPermSize | 永久代(方法区)的最大值 |
-XX:+PrintGCDetails | 打印 GC 信息 |
-XX:+HeapDumpOnOutOfMemoryError | 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用 |
java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域存放对象实例与数组等( new出来的)
堆被分为新生代(Young)、老年代(Old)、永久代(Perm,jdk8之后被移除,由元空间metaspace替代,元空间使用的是物理内存)
young:old默认为1:2。可通过-XX:NewRatio指定young的大小。-XX:PretenureSizeThreshild参数指定老年代大小,默认为0代表不管多大先在eden中分配内存。
Eden:from:to = 8:1:1
几乎所有的对象都是在eden中产生的,Eden区域为连续内存空间分配极快,对象的生命周期很短,当eden区满的时候,会将存活对象会在from和to中来回复制,过程为eden+from->to,下一次则为eden+to->from,会保证有一个Survivor为空,复制一次年龄加1,当年龄到达阈值后转移到Old中
老年代存储的是一些大对象和需要年需存储内存空间的对象(静态变量、缓存、线程池等)
回收过程
- 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
- 最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
- 下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;
- 将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;
- 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
3.垃圾回收机制
minor gc(Young gc)
在年轻代中存在的回收算法,当JVM无法为新对象分配内存(Eden区满)时会触发minor gc,此回收会造成所有应用线程停顿,但相对于Old区GC时间忽略不计
使用的是复制算法,其中一块内存用完就复制到另外一块上面,将已使用过的内存清理
Full GC
清理整个堆的GC事件,包括新生代、老年代、元空间,CMS并发收集就是这个模式
采用的是标记-清除算法,位置不连续,容易产生碎片
收集器
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
- 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收集器: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。
CMS收集器和G1收集器的区别:
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
- G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
- CMS收集器以最小的停顿时间为目标的收集器;
- G1收集器可预测垃圾回收的停顿时间
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
- G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
- jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
GC日志
public static void main(String[] args) {
Object obj = new Object();
System.gc();
System.out.println();
obj = new Object();
obj = new Object();
System.gc();
System.out.println();
}
4.java对象创建
类加载检查
执行new后,会检查该参数是否在常量池中有该类的符号引用(一般是有对应的构造函数),并检查该类是否已经被加载、解析和初始化过,如果没有就先执行类加载过程
分配内存
类加载完成后就可以确定所需内存大小,从Java堆中分配一块确定大小的内存。
分配方式
指针碰撞:内存规整的情况下,用一个指针指向已分配和未分配的分界点,通过移动指针来分配内存
空闲列表:内存不完整情况,分配内存互相交错,通过一张表记录空闲内存块信息
分配并发问题
创建对象时需要考虑线程安全,主要有两种方式
CAS+失败重试
CAS是乐观锁的一种实现方式,每次不加锁假设没有冲突去完成操作,失败就重试直到成功。虚拟机采用CAS保障更新操作的原子性
TLAB
每个线程预先在Eden分配一片区域(TLAB),线程分配时先在TLAB中分配,TLAB用完之后再使用CAS策略
初始化零值
初始化所有字段对应的零值
设置对象头
对象的hashCode,对象的GC分代年龄信息,锁标志状态等。另一部分是类型指针,指向类元数据的指针,确定对象是哪个类的数据