jvm将所管理的内存划分为多个区域,每个区域都有各自的用途。
保存当前线程上下文信息,这是一段独立的空间,方便线程的切换。
* 如果当前执行的是java方法,此空间保存的是虚拟机的字节码指令地址 * 如果执行的native方法,此空间值为空(Undefined)
是线程私有的,它的生命周期与线程相同,每个方法的执行都会创建一个栈用与存储局部变量,如java提供的各种基本类型指值类型(boolean,byte,int...)
1. 栈深度大于虚拟机的允许深度,将抛出栈溢出(StackOverflowError),如递归调用
2. 栈的空间不够用时在动态扩展时无法申请更多内存时,抛出OutOfMemoryError异常。
与java虚拟机栈类似,不同的是它专门为使用native方法时服务,比如调用c++方法等这种脱离java虚拟机管理的方法,在sun hotshot中本地方法栈与虚拟机栈合并为一个。
同样本地方法栈也会抛出StatckOverflowError或OutOfMemoryError异常信息。
这是jvm管理的所有下线程共享的一块内存空间。所有引用类型的对象都要在给区域分配空间,或者说放在该空间内的对象都是引用类型的。
这也是java垃圾回收的主要区域,称为GC堆管理。
按照分代管理的额思想,分为 新生代:Eden空间,From Survivor空间,To Survivor空间
老年代:
5. 方法区
该区也是多线程共享的区域,类似与java堆上分割出来的一块永不会被回收的内存空间。
我们在写代码的时候通常方法相关的信息都是固定不变的,而变化的是方法内的局部变量和方法内的实例对象,所以这写不变的信息也要被加载进内存才能被执行,只直到jar包被卸载才会导致方法区需要回收,例如:程序的热更新。
6.运行时常量池
作为方法区的一部分。不论是编译时常量还是运行时常量,都可以写入该区域。
在jdk1.7中常量池已经从该区域移出。
这块区域不属于运行时区域的而一部分,为了避免一些场景中避免jvm到native堆数据的来回复制,而提高性能。
配置jvm内存的时候要考虑:各个区域内存+直接内存 <= 物理内存限制。
1. jvm遇见new指令时,首先检查常量池中否是存在这个符号引用,并检查这个符号引用代表的类被加载、解析和初始化过,没有则先执行响应的加载过程。
2. jvm根据类型的大小分配相应的空间,并初始化为0
3. 接着jvm设置改空间的基本信息,如:属于哪个类的实例,如何找到该类的元数据信息,对象的哈希,对象的GC分代,以及锁状态等
4. 此时从jvm角度看已经完成了初始化,可以理解为非托管的代码已经走完了,接着由托管代码进行初始化,通常理解为调用构造函数等。
当一个线程中正在创建了一个共享对象时,另一个线程就要访问改对象,由于对象还没有被初始化完成,是不能被访问的或者没有达到预期的效果,这时需要使用同步技术来等待改实例被创建完成。
另一种解决办法时,jvm提供线程缓存空间来创建这个实例,由于线程内的局部变量是不会被共享的,所以可以保证安全,等对象被创建成功后,再使用同步技术,将对象复制到指定的位置。
由3部分组成:对象头,实例数据,对齐填充。
对象头:用来存储程对象在jvm中信息,如:哈希码,GC分代信息,锁状态,以及数据的长度等。
实例数据:我们程序创建的实例的信息
对齐填充:如补齐4的倍数长度,可以加快数据的访问
前面介绍了运行时区域可能存在的两种异常信息,下面来证明这些异常的发生。
import java.util.ArrayList;
import java.util.List;
class TestMain {
static class Test {
}
/**
* JAVA_OPTS="-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError"
*/
public static void main(String[] args) {
List<Test> list=new ArrayList<Test>();
long size=Runtime.getRuntime().maxMemory();
System.out.println(size);
while (true) {
Test test=new Test();
list.add(test);
}
}
}
class TestMain {
static int index=0;
static void test() {
index++;
test();
}
/**
* JAVA_OPTS="-Xss128k"
*/
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
System.out.println("递归深度:"+index);
}
}
}
class TestMain {
void test() {
while(true){}
}
public void stackLeakByThread(){
while (true) {
Thread thread=new Thread(()->{
test();
});
thread.start();
}
}
/**
* JAVA_OPTS="-Xss2m"
*/
public static void main(String[] args) {
new TestMain().stackLeakByThread();
}
}
import java.util.ArrayList;
import java.util.List;
class TestMain {
/**
* JAVA_OPTS="-XX:PermSize=10M -XX:MaxPermSize=10m" //jdk1.7
* JAVA_OPTS="-XX:MetaspaceSize=2M -XX:MaxMetaspaceSize=2M" //jdk 1.8
*/
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
int i=0;
while(true){
list.add(String.valueOf(i++).toString());
}
}
}
class TestMain {
/**
* JAVA_OPTS="-XX:PermSize=10M -XX:MaxPermSize=10m" //jdk1.7
* JAVA_OPTS="-XX:MetaspaceSize=2M -XX:MaxMetaspaceSize=2M" //jdk 1.8
*/
public static void main(String[] args) {
while (true) {
Enhancer enhancer=new Enhancer();
enchancer.setSuperclass=(TestObj.class);
enchancer.setUseCache(false);
enchancer.setCallback((obj,methoc,args,proxy)->{
return proxy.invokeSuper(obj,args);
});
enchancer.create();
}
}
static class TestObj{
}
}
import java.lang.reflect.Field;
import sun.misc.Unsafe;
class TestMain {
/**
* JAVA_OPTS="-Xmx20m -XX:MaxDirectMemorySize=10m"
*/
public static void main(String[] args) throws Exception {
final int _1mb = 1024 * 1024;
Field unsafeFiled = Unsafe.class.getDeclaredFields()[0];
unsafeFiled.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeFiled.get(null);
while (true) {
unsafe.allocateMemory(_1mb);
}
}
}
当以对象到GC Roots没有任何引用连接时,证明对象时不可用的。
object obj=new object()
这类引用,只要强引用存在,就不会被垃圾回收回收掉。无用类的判定条件:
给对象添加一个引用计数器,每当被引用时加1,释放时减1,当引用数为0时说明对象可以被清理,但是对于相互引用的情况就导致引用计数一直>=0,导致不能为gc清理,所以java没有采用该方式。
分为两个阶段:先标记出需要清理的对象,然后进行统一清理。
这种方式导致内存空间不连续,在遇见大对象分配时因空间不足而触发垃圾回收。
将内存分为两部分,每次只使用其中一部分,当内存被用完时就将存活的对象复制到另一部分,然后将已使用过的那一部分清理掉。缺点给也很明显内存只有1/2可用。
分为三个阶段:先标记需要被回收的对象,然后将存货的对象向一段移动,也就是将活动的对象地址空间整理成连续的,最后直接清理掉活动内存边界之外的内存。
根据对象存活周期的不同将内存划分为:新生代和老年代,在根据它们的特点采用合适的清理算法。
在新生代中,存在大量一次性对象,所以采用复制算法。
在老年代中,因为对象存活率高、所以采用标记-清理、标记-整理算法。
GC Roots节点的枚举过程会导致Stop The World,可以通过OopsMap类型的数据结构加快这个过程。
OopMap记录了对象内什么偏移量上是什么类型的数据也会在特定位置记录下栈和寄存器中哪些位置是引用,这样GC在扫描时就可以直接得到这些信息。
由于导致OopMap内容变化的指令很多,如果每个指令都记录对应的OopMap会需要大量的存储空间,从而导致gc成功变高,所以HotSopt只在特定的位置生成了这些OopMap数据,这里称为安全点(SafePoint)。
太多、太少的关键点都不利于gc的运行,通常方法的调用、循环的跳转、异常跳转等这些具有明显特征的位置才会生成安全点。
对于安全点,gc发生时会让除了执行JNI线程外的所有线程停顿下来,这里有两种停顿方案:
安全点已经几乎完美的解决了如何进入gc的问题,但实际情况会遇到有些被sleep状态或blocked状态的线程并没有机会执行到安全点来判断中断标记,这种情况就需要安全区域来解决。
安全区域指一段代码中,引用关系不会发生变化,在这个区域中的任意地方开始gc都是安全的。
在线程执行到安全区域时,首先标记自己已进入了安全区域,当gc开始时不用去关心状态在安全区域的线程,当线程要离开安全区域时需要判断gc是否完成了gc roots的枚举,直到gc roots的枚举完成后,线程才可以离开安全区域接着运行。
使用复制算法的新生代单线程收集器。
这是一个单线程的收集器,在jdk1.3.1之前是新生代收集的唯一选择,它的运行会停止其它所有线程的运行。
对于桌面程序而言由于需要的内存压力没有web服务端那么大,所以在Serial单线程的执行模式下可以获得最高的单线程收集效率。
使用复制算法的新生代多线程收集器。
ParNew是Serial的多线程版本,在web服务模式下作为首选的新生代收集器。例外一个与性能无关的原因是,除了Serial,只有ParNew能与cms收集器配合工作。
使用复制算法的新生代多线程收集器。
与ParNew不同的是它关注的是一个可控制的吞吐量。
吞吐量指cpu用于运行用户代码的时间与cpu总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
因为垃圾收集会导致Stop the world,所以减少来及运行时时间可以更多的运行用户代码,从而提高用户体验。
使用标记-整理算法的老年代单线程收集器。
用于收集老年代,同样是一个单线程收集器,不同的是使用标记-整理算法。
这个收集器主要用于桌面模式下,如果在web服务模式下主要两大用途: 一种在jdk1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种是作为cms收集器的后备方案。
使用标记-整理算法的老年代多线程收集器。
这个收集器在jdk1.6中才开始是使用,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态,原因是Parallel Scavenge收集器不能与除了Serial old之外的老年代收集器配合使用。直到该收集器出现后,在注重吞吐量以及cpu资源敏感的场合,都可以有限考虑Parallel Scavenge+Parallel old收集器。
使用标记-清除算法的老年代多线程收集器。
在重视服务响应速度,希望系统停顿时间最短的应用下,如web服务应用下,非常适合使用该收集器。
它的收集步骤:
优点: 可以与用户线程并行,导致部分用户线程Stop the world。与前几个完全导致用户线程Stop the world相比已经有了相当大的进步。
缺点:
当今收集器中最牛逼的了。
一款面向服务端应用的垃圾收集器。
特点:
g1收集步骤:
多数情况下,对象在新生代Eden区中分配,如果空间不足,则发起一次Minor GC。
大对象指需要大量连续内存空间的对象,比如字符传以及数组,大对象容易触发垃圾回收,而且遇见短命的大对象更是会导致性能下降,避免使用。说了这么多那么多大的才算大对象?!虚拟机提供了-XX:PretenureSizeThreshold参数来指定大于这个值算大对象,分配内存是直接进入老年代。
每个对象上都有一个年龄计数器,一个在eden产生的对象,经历过一次MinorGC后还存活,则被移入Survivor区中,年龄设置为1,以后每熬过一次MinorGC就加1岁,当年龄增加到默认15岁时会被晋升到老年代中,可以通过-XX:MaxTenuringThreshold设置。
并不是永远要求年龄大于指定年龄的对象才会进入老年代,如果Survivor空间中相同年龄的所有大象大小的和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代了。
在发送Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,那么minor gc可以确保是安全的,否则虚拟机会查看HandlePromotionFailure设置的值是否允许担保失败,允许则会继续检查老年代最大可用空间是否大于历次晋升老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次MinorGC是有风险的;如果小于或HandlePromotionFailure设置不允许冒险,那么就要Full GC了。
分为7个阶段:加载,连接(验证,准备,解析),初始化,使用,卸载。
加载、验证、准备、初始化、卸载这5个阶段顺序是确定,类的加载过程必须经历该过程。
而类的解析可以在初始化前,也可以在初始化后(如:动态绑定)。
加载阶段虚拟要完成以下3件事:
java支持从zip包中读取一个类的字节流如JAR,EAR,WAR格式;支持从网络中获取;支持从动态代理技术动态生成的二进制字节流加载等所有从io,内存方式的方式加载字节流。
连接的第一个阶段,验证是为了保证class的字节流符合虚拟机的要求,需要经行如何4个验证动作:
该阶段将为类的成员分配对应的内存空间并设置默认值,需要注意的是final标记的变量将在该阶段就被赋值为最终值,除此之外的将在初始化时才进行设置对应的实际默认值。
虚拟机字节码执行引擎