前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 ># 自动内存管理机制

# 自动内存管理机制

作者头像
用户1175783
发布2019-10-16 11:43:35
5450
发布2019-10-16 11:43:35
举报

自动内存管理机制

java内存区域与内存溢出异常

运行时区域

​ jvm将所管理的内存划分为多个区域,每个区域都有各自的用途。

1. 程序计数器区

​ 保存当前线程上下文信息,这是一段独立的空间,方便线程的切换。

* 如果当前执行的是java方法,此空间保存的是虚拟机的字节码指令地址 * 如果执行的native方法,此空间值为空(Undefined)

2. java虚拟机栈区

​ 是线程私有的,它的生命周期与线程相同,每个方法的执行都会创建一个栈用与存储局部变量,如java提供的各种基本类型指值类型(boolean,byte,int...)

该区域规定了两种异常:

​ 1. 栈深度大于虚拟机的允许深度,将抛出栈溢出(StackOverflowError),如递归调用

​ 2. 栈的空间不够用时在动态扩展时无法申请更多内存时,抛出OutOfMemoryError异常。

3. 本地方法栈区

​ 与java虚拟机栈类似,不同的是它专门为使用native方法时服务,比如调用c++方法等这种脱离java虚拟机管理的方法,在sun hotshot中本地方法栈与虚拟机栈合并为一个。

​ 同样本地方法栈也会抛出StatckOverflowError或OutOfMemoryError异常信息。

4. java堆区

​ 这是jvm管理的所有下线程共享的一块内存空间。所有引用类型的对象都要在给区域分配空间,或者说放在该空间内的对象都是引用类型的。

​ 这也是java垃圾回收的主要区域,称为GC堆管理。

​ 按照分代管理的额思想,分为 ​ 新生代:Eden空间,From Survivor空间,To Survivor空间

​ 老年代:

5. 方法区

​ 该区也是多线程共享的区域,类似与java堆上分割出来的一块永不会被回收的内存空间。

具体可以这么理解:

​ 我们在写代码的时候通常方法相关的信息都是固定不变的,而变化的是方法内的局部变量和方法内的实例对象,所以这写不变的信息也要被加载进内存才能被执行,只直到jar包被卸载才会导致方法区需要回收,例如:程序的热更新。

6.运行时常量池

​ 作为方法区的一部分。不论是编译时常量还是运行时常量,都可以写入该区域。

​ 在jdk1.7中常量池已经从该区域移出。

直接内存

​ 这块区域不属于运行时区域的而一部分,为了避免一些场景中避免jvm到native堆数据的来回复制,而提高性能。

​ 配置jvm内存的时候要考虑:各个区域内存+直接内存 <= 物理内存限制。

hotspot虚拟机对象
对象的创建
new一个对象创建过程:

​ 1. jvm遇见new指令时,首先检查常量池中否是存在这个符号引用,并检查这个符号引用代表的类被加载、解析和初始化过,没有则先执行响应的加载过程。

 		2. jvm根据类型的大小分配相应的空间,并初始化为0
 		3. 接着jvm设置改空间的基本信息,如:属于哪个类的实例,如何找到该类的元数据信息,对象的哈希,对象的GC分代,以及锁状态等
 		4. 此时从jvm角度看已经完成了初始化,可以理解为非托管的代码已经走完了,接着由托管代码进行初始化,通常理解为调用构造函数等。
内存申请的两种方式
  1. 指针碰撞:这种模式假设内存时规整的,也就是各个实例内存间不存在零碎的片段,每次申请新内存都是追加模式,但是通常很难做到,因为压缩GC时比较耗时的。
  2. 空闲列表:这种模式是保存一个表记录内存中哪些是空闲的哪些是已被占用的,然后根据实例的大小,从空闲的内存配凑出所需要的大小。
多线程竞争问题

​ 当一个线程中正在创建了一个共享对象时,另一个线程就要访问改对象,由于对象还没有被初始化完成,是不能被访问的或者没有达到预期的效果,这时需要使用同步技术来等待改实例被创建完成。

​ 另一种解决办法时,jvm提供线程缓存空间来创建这个实例,由于线程内的局部变量是不会被共享的,所以可以保证安全,等对象被创建成功后,再使用同步技术,将对象复制到指定的位置。

对象的内存布局
由3部分组成:对象头,实例数据,对齐填充。

​ 对象头:用来存储程对象在jvm中信息,如:哈希码,GC分代信息,锁状态,以及数据的长度等。

​ 实例数据:我们程序创建的实例的信息

​ 对齐填充:如补齐4的倍数长度,可以加快数据的访问

模拟StackOverflowError,OutOfMemoryError异常

​ 前面介绍了运行时区域可能存在的两种异常信息,下面来证明这些异常的发生。

模拟堆溢出得例子
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);
        }
    }
}
模拟jvm栈溢出
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);
        }
    }
}
模拟本地方法栈溢出(jdk1.8 无效)
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();
    }
}
运行时常量溢出(jdk1.8 无效)
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对象

当以对象到GC Roots没有任何引用连接时,证明对象时不可用的。

可作为GC Roots的对象有一下几种:
  • 虚拟机栈(栈中的本地变量表)中引用的对象
  • 方法去中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(指Native方法)应用的对象
四种引用类型
  1. 强引用 指程序中普遍存在的,如:object obj=new object()这类引用,只要强引用存在,就不会被垃圾回收回收掉。
  2. 软引用 用来描述一些还有用但并非必需的对象。对于弱引用关联着的对象,再系统将要法僧内存溢出异常之前,会将这些对列入回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  3. 弱引用 用来描述并非必需对象,比软引用更若一些, 被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾回收器工作时,无论当前内存是否足够,都回回收掉只被弱引用关联的对象。
  4. 虚引用 最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被回收时受到一个系统通知。
两次标记才能判断为可回收对象
  1. 一个对象到GC Roots的路径不可达,则会被第一次标记并添加到F-Queue
  2. F-Queue调用每个成员的finalize()方法,如果finalize()有被调用且在finalize()方法中对象没有将自己添加到引用连上,也就是没有将this设置为引用连上其它成员在的子成员,那么判定为可回收对象
方法区的回收

无用类的判定条件:

  1. 该类所有实例已被回收,java堆中不存在该类的任何实例。
  2. 加载类的ClassLoader已被回收。
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
引用基数算法

​ 给对象添加一个引用计数器,每当被引用时加1,释放时减1,当引用数为0时说明对象可以被清理,但是对于相互引用的情况就导致引用计数一直>=0,导致不能为gc清理,所以java没有采用该方式。

标记-清除算法

​ 分为两个阶段:先标记出需要清理的对象,然后进行统一清理。

​ 这种方式导致内存空间不连续,在遇见大对象分配时因空间不足而触发垃圾回收。

复制算法

​ 将内存分为两部分,每次只使用其中一部分,当内存被用完时就将存活的对象复制到另一部分,然后将已使用过的那一部分清理掉。缺点给也很明显内存只有1/2可用。

标记-整理算法

​ 分为三个阶段:先标记需要被回收的对象,然后将存货的对象向一段移动,也就是将活动的对象地址空间整理成连续的,最后直接清理掉活动内存边界之外的内存。

分代收集算法

​ 根据对象存活周期的不同将内存划分为:新生代和老年代,在根据它们的特点采用合适的清理算法。

​ 在新生代中,存在大量一次性对象,所以采用复制算法。

​ 在老年代中,因为对象存活率高、所以采用标记-清理、标记-整理算法。

HotSpot算法实现
枚举根节点

​ GC Roots节点的枚举过程会导致Stop The World,可以通过OopsMap类型的数据结构加快这个过程。

​ OopMap记录了对象内什么偏移量上是什么类型的数据也会在特定位置记录下栈和寄存器中哪些位置是引用,这样GC在扫描时就可以直接得到这些信息。

安全点

​ 由于导致OopMap内容变化的指令很多,如果每个指令都记录对应的OopMap会需要大量的存储空间,从而导致gc成功变高,所以HotSopt只在特定的位置生成了这些OopMap数据,这里称为安全点(SafePoint)。

​ 太多、太少的关键点都不利于gc的运行,通常方法的调用、循环的跳转、异常跳转等这些具有明显特征的位置才会生成安全点。

​ 对于安全点,gc发生时会让除了执行JNI线程外的所有线程停顿下来,这里有两种停顿方案:

  1. 抢占式中断:首先中断所有线程,然后将没有到达安全点的线程恢复让它们跑到安全点。几乎没有虚拟机使用这个方式。
  2. 主动式中断:当gc要中断线程时,不直接对线程操作,仅仅设置一个标记,待各个线程到达安全点时去判断中断标记,从而确定是否要挂起线程。
安全区域

​ 安全点已经几乎完美的解决了如何进入gc的问题,但实际情况会遇到有些被sleep状态或blocked状态的线程并没有机会执行到安全点来判断中断标记,这种情况就需要安全区域来解决。

​ 安全区域指一段代码中,引用关系不会发生变化,在这个区域中的任意地方开始gc都是安全的。

​ 在线程执行到安全区域时,首先标记自己已进入了安全区域,当gc开始时不用去关心状态在安全区域的线程,当线程要离开安全区域时需要判断gc是否完成了gc roots的枚举,直到gc roots的枚举完成后,线程才可以离开安全区域接着运行。

垃圾收集器
Serial收集器

​ 使用复制算法的新生代单线程收集器。

​ 这是一个单线程的收集器,在jdk1.3.1之前是新生代收集的唯一选择,它的运行会停止其它所有线程的运行。

​ 对于桌面程序而言由于需要的内存压力没有web服务端那么大,所以在Serial单线程的执行模式下可以获得最高的单线程收集效率。

ParNew收集器

​ 使用复制算法的新生代多线程收集器。

​ ParNew是Serial的多线程版本,在web服务模式下作为首选的新生代收集器。例外一个与性能无关的原因是,除了Serial,只有ParNew能与cms收集器配合工作。

Parallel Scavenge收集器

​ 使用复制算法的新生代多线程收集器。

​ 与ParNew不同的是它关注的是一个可控制的吞吐量。

​ 吞吐量指cpu用于运行用户代码的时间与cpu总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

​ 因为垃圾收集会导致Stop the world,所以减少来及运行时时间可以更多的运行用户代码,从而提高用户体验。

Serial old收集器

​ 使用标记-整理算法的老年代单线程收集器。

​ 用于收集老年代,同样是一个单线程收集器,不同的是使用标记-整理算法。

​ 这个收集器主要用于桌面模式下,如果在web服务模式下主要两大用途: 一种在jdk1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种是作为cms收集器的后备方案。

Parallel Old收集器

​ 使用标记-整理算法的老年代多线程收集器。

​ 这个收集器在jdk1.6中才开始是使用,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态,原因是Parallel Scavenge收集器不能与除了Serial old之外的老年代收集器配合使用。直到该收集器出现后,在注重吞吐量以及cpu资源敏感的场合,都可以有限考虑Parallel Scavenge+Parallel old收集器。

CMS收集器

​ 使用标记-清除算法的老年代多线程收集器。

​ 在重视服务响应速度,希望系统停顿时间最短的应用下,如web服务应用下,非常适合使用该收集器。

它的收集步骤:

  1. 初始标记:仅标记gc goots能关联到的对象,速度快
  2. 并发标记:指gc roots tracing的过程
  3. 重新标记:是修正并发标记期间因用户程序继续运行而导致标记产生变动的那一步部分对象的标记记录,这个阶段的停顿时间一般会比初始标记耗时长,但远小于并非标记时间
  4. 并发清除:耗时与并发标记相当

优点: 可以与用户线程并行,导致部分用户线程Stop the world。与前几个完全导致用户线程Stop the world相比已经有了相当大的进步。

缺点:

  1. cms对cpu资源非常敏感,cms默认启动的回收线程数时(cpu数量+3)/4,当cpu大于4个时,回收线程占用的cpu资源不会少于25%,当cpu不足4个时,可能导致用户线程的执行速度忽然低于50%,该收集器已经被放弃。
  2. 无法处理浮动垃圾,因为与用户线程并行,所以并行的用户线程中产生来及不能在本地收集中被标记清理。
  3. 因为使用标记-清楚算法,导致碎片化严重,从而触发full gc。
G1 收集器

​ 当今收集器中最牛逼的了。

​ 一款面向服务端应用的垃圾收集器。

特点:

  1. 并行与并发:g1充分利用cpu,多核环境下硬件优势,使用多个cpu缩短stop the world时间,并通过并行的方式让java程序继续执行。
  2. 分代收集:独立管理新/老代,用不同的方式处理新代和已经存活了一段时间、熬过多次gc的对象,以获取更好的收集效果。
  3. 空间整合:使用标记-整理算法实现收集,不会产生空间碎。
  4. 可预测停顿:除了低停顿外,能让使用者指定一个长度为M毫秒的时间片段内,消耗垃圾收集器上的时间不得超过n毫秒。

g1收集步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收
内存分配与回收策略
对象优先在Eden分配

​ 多数情况下,对象在新生代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了。

虚拟机性能监控与故障处理工具

jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具
jinfo:java配置信息工具
jmap:java内存映射工具
jhat:虚拟机堆转储快照分析工具
jstack:java堆栈跟踪工具
hsdis:jit生成代码反汇编
jsconsole:java监视与管理控制台
visualvm:多合一故障处理工具

调用案例分析与实践

虚拟机执行子系统

  1. 类文件结构
虚拟机加载机制
类加载时机

分为7个阶段:加载,连接(验证,准备,解析),初始化,使用,卸载。

加载、验证、准备、初始化、卸载这5个阶段顺序是确定,类的加载过程必须经历该过程。

而类的解析可以在初始化前,也可以在初始化后(如:动态绑定)。

遇见下面5中情况必须对类进行初始化:
  1. 遇到new一个实例,读写静态字段,调用类的静态方法时
  2. 反射调用类的时候
  3. 当初始化一个类的时候如果父类还没有被初始化,则需要先初始化父类
  4. 当虚拟机启动时,main函数所在的类会被初始化
  5. 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发初始化。
类加载的过程
加载

加载阶段虚拟要完成以下3件事:

  1. 通过一个类的完全限定名称来获取类的二进制字节流
  2. 将字节流锁代表的静态存储结构转化为方法去的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据访问入口

java支持从zip包中读取一个类的字节流如JAR,EAR,WAR格式;支持从网络中获取;支持从动态代理技术动态生成的二进制字节流加载等所有从io,内存方式的方式加载字节流。

验证

连接的第一个阶段,验证是为了保证class的字节流符合虚拟机的要求,需要经行如何4个验证动作:

  1. 文件格式验证:比如class数据的前四个字节都是一样的,紧接着第四个字节是版本号相关,该版本号的class文件能不能被当前版本的jvm支持等,直到该class成功进入方法去。
  2. 元数据验证:主要指该类是否符合java语法规范,比如:该类是否有父类,这个类的父类是否被final修饰过,该类非抽象类时是否继承了父类或接口并是否实现了对应的方法,是否覆盖了父类final标记的同名成员等。
  3. 字节码验证:这个阶段主要对类的方法体验证,如方法体的变量是否会存在类型不匹配的情况,条抓指令是否超出了方法体之外的字节码指令上,类型转换等。
  4. 符号引用验证:指对类自身之外的信息进行匹配性校验,如:符号引用中通过字符串描述的完全限定名是否可以找到对应的类,符号引用中的类,字段,方法的访问性是否可以被当前类访问等。
准备

该阶段将为类的成员分配对应的内存空间并设置默认值,需要注意的是final标记的变量将在该阶段就被赋值为最终值,除此之外的将在初始化时才进行设置对应的实际默认值。

解析
  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析
初始化

 虚拟机字节码执行引擎

类加载及执行子系统的案例与实战

程序编译与代码优化

  1. 早期编译优化
  2. 晚期运行优化

高效并发

  1. 内存模型与线程
  2. 线程安全与锁优化
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 自动内存管理机制
    • java内存区域与内存溢出异常
      • 运行时区域
      • hotspot虚拟机对象
    • 垃圾回收器与内存分配策略
      • GC Roots对象
      • 垃圾收集算法
      • HotSpot算法实现
    • 虚拟机性能监控与故障处理工具
      • jps:虚拟机进程状况工具
      • jstat:虚拟机统计信息监视工具
      • jinfo:java配置信息工具
      • jmap:java内存映射工具
      • jhat:虚拟机堆转储快照分析工具
      • jstack:java堆栈跟踪工具
      • hsdis:jit生成代码反汇编
      • jsconsole:java监视与管理控制台
      • visualvm:多合一故障处理工具
    • 调用案例分析与实践
      • 虚拟机加载机制
  • 虚拟机执行子系统
  • 程序编译与代码优化
  • 高效并发
相关产品与服务
应用性能监控
应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档