精华内容
下载资源
问答
  • new关键字创建对象的3个步骤:1、在堆内存中创建出对象的实例。当我们用new关键字来创建对象的实例时,JVM首先会检查new这个指令的参数是不是能造常量池中定位成一个类的符号引用,然后再检查该符号引用所对应的类...

    当一个对象被创建了,那在JVM中是如何的从一个对象不存在到存到,然后将对象存放在什么地方呢?这次主要来探讨一下Java对象创建的过程。

    new关键字创建对象的3个步骤:

    1、在堆内存中创建出对象的实例。

    当我们用new关键字来创建对象的实例时,JVM首先会检查new这个指令的参数是不是能造常量池中定位成一个类的符号引用,然后再检查该符号引用所对应的类是不是被正常的加载、连接、初始了,如果木有则必须要完成类的加载过程,当事先的准备阶段都结束之后,接着JVM则为该对象分配内存,当对像加载完之后该对象要分配多少内存其实是已经确定的一件事情了。而在Java堆中内存整体来说是分成2部分的,第一部分内存是已经被使用或者说已经被占用的,而第二部分内存则是空闲的可以被使用的,而已经被占用的空间和未被使用的空间又分为两种情况:

    第一种情况:在堆内存中已经截然有序的将已使用和未使用的内存空间给分离开了,比如说左侧是已经占用的空间,而右侧是未被占用的空间。中间可以通过一个指针来指向,这种情况下如果新创建的对象则会存在于未被占用的空间中,然后指针发生了一个移动指向了下一个可以被使用的内存空间,对于这种case我们可以称之为指针碰撞(前提是堆中的空间通过一个指针进行分割,一侧是已经被占用的空间,另一侧是未被占用的空间)。

    第二种情况:这种Java堆内存并未像第一种情况说得这么理想,而是不归整交织在一起了,这种情况下肯定不能去移动指针这么简单来进行指向了,这时需要记录一个列表用来标识哪些地方是内存已经被使用了的,哪些是未被使用了的,并且还要记录未被使用的大小是多少,这种情况下当要给对象分配内存时,则需要从列表中选出来可以容纳新创建对象大小的空间,然后把新的对象放置在可以容纳的内存当中,并且要修改列表的记录,这种做法则称之为空闲列表(前提是堆内存空间中已被使用与未被使用的空间是交织在一起的,这时,虚拟机就需要通过一个列表来记录哪些空间是可以使用的,哪些空间是已被使用的,接下来找出可以容纳下新创建对象的且未被使用的空间,在此空间存放该对象,同时还要修改列表上的记录)。

    FAQ:为啥会有这两种情况呢?其实是跟垃圾收集【未来会专门学到它】器息息相关的,有一些垃圾收集器是带压缩过程,所谓压缩过程是指垃圾收集器在执行一次垃圾回收的时候,除了把真正垃圾的对象给清除掉之外,此时已使用和未使用的内存一定是不连续的,那么它们在做完清除工作之后还要做一次对象的移动操作,也就是将已被使用的和未被使用的分文别类的给排开,此时就可以用指针碰撞的方式来解决对象存放的问题;而有些垃圾收集器在垃圾回收之后就立马结束了,不会对对象进行一个移动操作,从而导致已使用和未被使用的内存交织在一起的,此时就只能用空闲列表的方式来解决对象存放的问题啦。

    2、为对象的实例成员变量【而非静态成员变量】赋初值。

    这个不多解释了,在之前的类加载中详细说过。

    3、将对象的引用返回。

    对象在内存中的布局【了解既可】:

    对象的内存布局其实就是指一个对象它存放的信息有啥, 总共分为三部分:

    1、对象头。

    它会存放对象自身的一些运行时的数据信息,比如说一个对象有一个hash码、还有分代的一个信息等,把这些信息都放置在对象头里面。

    2、实例数据 (既我们在一个类中所声明的各项信息)。如成员变量。

    3、对齐填充(可选),其实就是起到一些点位符的作用,比如说要求8的倍数,如果不够8的话被0等。

    引用访问对象的方式:

    1、使用句柄的方式。

    2f5fad04b8f3bf25136b6a64672ca462.png

    2、使用直接指针的方式。

    5732d0af5a4735bf7c1093a72db8d518.png

    这两种有啥区别,这里也再贴出来回顾一下,纯之前学的东东:

    47ee3218d6ea2d66af7f893ae539fed6.png

    8ee378d0a8ae5e443ad10ac063f56bbc.png

    3b9aa22ac7a4693ebe585942469da5a5.png

    782a0f965fb291a73f6aa7e537e895e8.png

    好,接下来回到咱们熟悉的代码上来,上面一大堆的理论还得由实践将其进行验证,这里编写一个可以在堆空间出现内存溢出异常的代码,具体做法如下:

    a239243b5b98e0799b8f90e5c192c824.png

    也就是写一个死循环,不断的往堆中新建MyTest1对象,最终肯定会撑爆JVM的堆空间从而来模拟出堆内存溢出,我们知道JVM是可以有参数来调整截内存空间的,为了让这个程序更快的出现,我们可以手动来修改JVM的参数,如下:

    9ccb5a4a7a6de30cc088915c275c724b.png

    31a4bb6016ec358317423c45f669db59.png

    其中还设置了一个当发生内存溢出时来将内存的信息给dump出来,其实就类似于Android中来分析内存也是需要dump内存信息一样,如下:

    3709846ab5b3efec239ef714e9162b09.png

    下面来运行一下,看是不是很快就报内存溢出异常了:

    d6b00f75d2746a51b124dfd4d7fe52c4.gif

    立竿见影嘛,其中内存异常的原因也可以清楚的看到是由于java的堆空间:

    81575ef51efbaa3e7427f1d498fe627d.png

    其中可以看到dump文件已经创建了:

    bab48f221cfea36462276502f8cbd4f3.png

    那咱们在工程中刷新一下,发现貌似木有看到dump文件呀,其实在IntelliJ IDEA中是没有将其显示出来而已,咱们得在目录文件中来查看,瞅下:

    9f317fe6de45bf1199d9a150b59cc0ef.png

    那dump文件生成了怎么查看呢,莫要急,下次再继续学~~

    展开全文
  • Java对象内存分配流程

    2021-01-29 09:42:32
    内存分配流程 针对不同年龄段的对象分配原则 优先分配到Eden区 大对象(过长的字符串、数组)直接分配到老年代,尽量避免程序中出现过多的大对象 长期存活的对象分配到老年代 动态对象年龄判断 如果survivor区中...

    内存分配流程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R7aRvUZ6-1611884478956)(https://note.youdao.com/yws/res/47251/6E800DEEE53C47F39D396196F1FED4AE)]

    针对不同年龄段的对象分配原则

    • 优先分配到Eden区
    • 大对象(过长的字符串、数组)直接分配到老年代,尽量避免程序中出现过多的大对象
    • 长期存活的对象分配到老年代

    动态对象年龄判断

    • 如果survivor区中相同年龄的所有对象所占内存大小的总和大于survivor空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

    空间分配担保:-XX:HandlePromotionFailure

    对象分配过程:TLAB

    • TLAB,全称Thread Local Allocation Buffer, 即:线程本地分配缓存。这是一块每个线程私有的内存分配区域,它存在于Eden区,TLAB空间的内存非常小,仅占有整个Eden空间的1%

    作用:

    • 为了加速对象的分配,由于对象一般分配在堆上,而堆是线程共享的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降
    • 考虑到对象分配几乎是Java中最常用的操作,因此JVM使用了TLAB这样的线程专有区域来避免多线程冲突,提高对象分配的效率,称之为快速分配策略。

    其他TLAB说明

    • 不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
    • -XX:UseTLAB: 设置是否开启TLAB。
    • -XX:TLABWasteTargetPercent: 设置TLAB空间所占用Eden空间的百分比大小。
    • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

    局限性: TLAB空间一般不会太大(占Eden区1%),所以大对象无法进行TLAB分配。

    逃逸分析

    堆中分配对象是唯一的选择吗?

    • 《深入理解JVM虚拟机》:对象在Java堆中分配内存,这是一个普遍的常识了,但是有一种特殊情况,那就是如果经过逃逸分析后发现一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样能尽量减少将对象分配的堆中,减少OldGC或FullGC的次数,提高性能

    什么是逃逸分析?

    逃逸分析的基本行为就是分析对象动态作用域

    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
    • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

    快速判断逃逸:只要在方法内new的对象实体在外部被使用到了,则认为发生了逃逸,不管是不是静态,只要new的对象跑出了方法,注意关注的是对象实体,不是变量名,

    /**
     * 没有发生逃逸,则可以分配到栈上,随着方法的执行而结束。
     */
    public void test1(){
        V v = new V();
        // ...
        return;
    }
    
    
    // 发生了逃逸,对象返回到了方法外面
    // 如果不想发生逃逸,代码可以改写成test3
    public static StringBuffer test2(String s1, String s2){
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;
    }
    public static String test3(String s1, String s2){
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }
    
    // 引用成员变量的值,发生逃逸
    public void test4(){
        V v = getInstance();
        // getInstance().xxx() 同样发生逃逸
    }
    
    // 为成员变量赋值,发生逃逸
    public void test5(){
        this.obj = new xxx();
    }
    
    

    参数设置:

    • 在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。如果使用的是较早的版本,可以通过:
      • -XX: +DoEscapeAnalysis:显式开启逃逸分析
      • XX: +PrintEscapeAnalysis:查看逃逸分析的筛选结果。

    逃逸分析:代码优化

    使用逃逸分析,编译器可以对代码做如下优化:

    一、栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

    二、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

    三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

    代码优化之栈上分配

    • JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
    • 常见的栈上分配的场景:给成员变量赋值、方法返回值、实例引用传递。

    代码优化之同步省略(锁消除)

    • 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
      9 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被到其他线程占有。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

    代码优化之标量替换

    • 标量(Scalar):指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量
    • 相对的,那些还可以分解的数据叫做聚合量(Aggregate) ,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
    • 在JIT阶段,如果经过逃逸分析,如果没有发生逃逸的话,那么经过JIT优化,就会把这个对象拆解成若干个其中的成员变量来代替。这个过程就是标量替换。
    public static void main (string [] args) {
        alloc() ;
    }
    
    private static void alloc() {
        Point point =new Point(1,2) ;
        system.out.println ( "point.x=" + point.x + "point.y=" + point.y) ;
    }
    
    class Point {
        private int x;
        private int y;
    }
    
    • 以上代码,经过标量替换后,就会变成:
    private static void alloc({
        int x = 1;
        int y = 2;
        system.out.println ( "point.x=" + point.x + "point.y=" + point.y) ;
    }
    

    可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。

    那么标量替换有什么好处呢?

    就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。

    标量替换为栈上分配提供了很好的基础。

    标量替换参数设置:

    -XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。

    逃逸分析技术并不成熟

    • 关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。
    • 其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程
    • 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
    • 虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段
    • 注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,oracle HotspotJVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。
    • 目前很多书籍还是基于JDK 7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上
    展开全文
  • JVM对象内存分配流程

    2021-11-16 16:37:55
    对象内存分配流程图 对象栈内分配         通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有引用的时候,需要依靠GC来进行回收内存,如果对象数量较多的...

    对象内存分配流程图

    在这里插入图片描述

    对象栈内分配

            通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有引用的时候,需要依靠GC来进行回收内存,如果对象数量较多的时候,会给GC带来较大的压力,也间接影响了应用的性能.为了减少临时对象在堆内存分配的数量,JVM通过逃逸分析确定该对象会不会被外部访问.如果不会逃逸可以将该对象在栈内分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力.
    **逃逸分析:**就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用.例如:作为参数传递到其他的方法中.

    public class StackAlloc {
    
        public User test1(){
            User user = new User();
            user.setUserId(1);
            user.setUserName("fanqiechaodan");
            // 持久化到DB
            return user;
        }
    
        public void test2(){
            User user = new User();
            user.setUserId(1);
            user.setUserName("fanqiechaodan");
            // 持久化到DB
        }
    }
    

            很显然test1方法中的user被返回了,这个对象的作用域范围不确定,test2方法中的user对象我们可以确定当方法结束后就可以认为是无效对象了,对于这样的对象我们其实可以将其分配在栈内存里,让其在方法结束时跟随栈内存一起被回收掉.
            JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配到栈上 (栈内分配),JDK7之后默认开启逃逸分析, 如果需要关闭使用参数 (-XX:-DoEscapeAnalysis)
    标量替换: 通过逃逸分析确定该对象不会被外部访问,并且对象可以进一步分解时,JVM不会创建该对象,而是将该对象的成员变量分解若干个被这个方法使用的成员变量代替,这些代替的成员变量在栈帧或者寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配.开启标量替换参数(-XX:+EliminateAllocations), JDK默认开启标量替换
    标量与聚合量: 标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量,标量的对立就是可以被进一步分解的量,而这种量就称之为聚合量,而在JAVA中对象就是可以被进一步分解的聚合量.

    栈内分配示例:

    /**
     * @author fanqiechaodan
     * @Classname AllotOnStack
     * @Description 栈上分配,标量替换
     * 代码调用了1亿次test(),如果是分配到堆上,15m是肯定不够用的,必然会触发GC。
     *
     * 使用如下参数不会发生GC
     * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
     * 使用如下参数都会发生大量GC
     * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
     * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
     * @Date 2021/11/16 21:17
     */
    public class AllotOnStack {
    
        public static void main(String[] args) {
            for (int i = 0; i < 100000000; i++) {
                test();
            }
        }
    
        private static void test() {
            User user = new User();
            user.setUserId(1);
            user.setUserName("fanqiechaodan");
        }
    }
    

    结论:栈内分配依赖于标量替换和逃逸分析;

    对象在Eden区分配

            大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够的空间进行分配时,JVM将发起一次minorGC

    • minorGC: 指发生在新生代的垃圾收集动作,minorGC非常频繁,回收速度一般也比较快.
    • FullGC: 一般会回收老年代,年轻代,方法区的垃圾,FullGC的速度一般会比minorGC的慢10倍以上.

    Eden与Survivor区默认8:1:1
            大量的对象被分配在Eden区,Eden区满了以后会触发minorGC,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到空闲的Survivor区,下次Eden区满了后又会触发minorGC,把Eden区和非空闲的Survivor区垃圾对象回收,把剩余的存活的对象一次性挪到另一块空闲的Survivor区,因为新生代的对象都是朝生夕死的,存活的时间很短,所以JVM默认的8:1:1的比例还是很合适的,让Eden区尽量的大,Survivor区够用即可
            JVM默认有这个参数 -XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1的比例自动变化,如果不想这个比例有变化可以设置参数 -XX:-UseAdaptiveSizePolicy

    大对象直接进入老年代

            大对象就是需要大量连续内存空间的对象,例如:字符串,数组.JVM参数 -XX:PretenureSizeThreshold可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在SerialParNew两个收集器下有效.例如:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC,如果对象大小超过了1000000字节,就会直接进入老年代;
    大对象直接进入老年代的优点: 避免大对象分配内存时的复制操作而降低效率.

    长期存活的对象将进入老年代

            既然JVM采用了分代收集的思想来管理内存,那么内存在回收时就必须能识别哪些对象应放在新生代,那些对象应放在老年代中,为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器.如果对象在Eden出生并经过第一次minorGC后仍然能够存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor每经历一次minorGC,年龄就+1,当它的年龄增加到一定的成都(默认15岁,CMS收集器默认6岁,不同的收集器略微会有点不同),就会晋升到老年代中,对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold来设置.

    对象动态年龄判断

            当前非空的Survivor区域里,一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,

    • 例如: 非空的Survivor区域里现在有一批对象,年林1+年龄2+年龄n+年龄等多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄大于等于n的对象都放入老年代

            这个机制其实是希望那些可能是长期存活的对象,尽早的进入老年代.对象动态年龄机制一般是在minorGC之后触发的.

    老年代空间分配担保机制

    • 年轻代每次minor GC之前JVM都会计算下老年代剩余可用空间
    • 如果这个可用空间小于年轻代现有的所有对象大小之和 (包括垃圾对象)
    • 就会看是否设置 -XX:-HandlePromotionFailure(jdk1.8默认设置)
    • 如果有设置,就会查看老年代的可用内存大小,是否大于之前每一次minor GC后进入老年代的对象的平均大小
    • 如果没有设置或者老年代可用内存大小小于之前每一次minor GC后进入老年代的对象的平均大小,那么就会触发一次Full GC,对老年代和年轻代一起回收一次垃圾,如果回收还是没有足够空间释放新的对象就会发生OOM
    • 如果有设置并且老年代可用内存大小小于之前每一次minor GC后进入老年代的对象的平均大小,那么就会触发minor GC,当然如果minor GC之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发Full GC,Full GC完之后如果还是没有空间放置minor GC之后的存活对象,则也会发生OOM
      在这里插入图片描述

    对象内存回收

            堆中几乎存放着所有的对象实例,对垃圾回收前的第一步就是要判断那些对象已经死亡;即不能再被任何途径使用的对象

    引用计数法

            对每个对象的引用进行计数,每当有一个地方引用它时计数器+1,引用失效则-1.引用计数放到对象头中,大于0的对象被认为是存活对象.

    public class ReferenceCounting {
    
        Object eg = null;
    
        public static void main(String[] args) {
            ReferenceCounting referenceCounting1 = new ReferenceCounting();
            ReferenceCounting referenceCounting2 = new ReferenceCounting();
            referenceCounting1.eg = referenceCounting2;
            referenceCounting2.eg = referenceCounting1;
            referenceCounting1 = null;
            referenceCounting2 = null;
        }
    }
    

            如上面代码所示:除了对象referenceCounting1和referenceCounting2相互引用着对方以外,这两个对象之间再无任何引用,但是他们因为互相引用对方,导致它们的引用计数器都不为0;出现相互循环引用的问题,会导致GC回收器无法进行回收,引用计数法是可以解决循环引用问题的,主要是通过Recycler算法进行解决,但是再多线程环境下,引用计数变更也需要进行昂贵的同步操作,性能较低.目前主流的虚拟机并没有选择这个算法来管理内存

    可达性分析

            从GC Root开始进行对象搜索,可以被搜索到的对象即为 可达对象, 此时还不足以判断对象是否存活/死亡,需要经过多次的标记才能更加准确的确定,非可达对象便可以作为垃圾被回收掉. 目前Java中主流的虚拟机均采用此算法.
            GC Root的根节点可以是线程栈的本地变量,静态变量,本地方法栈的变量等等.

    在这里插入图片描述

    展开全文
  • Java所承诺的自动内存管理主要是针对对象内存的回收和对象内存分配.在 Java虚拟机的五块内存空间中,程序计数器、Java虚拟机栈、本地方法栈内存的分配和回收都具有确定性,一半都在编译阶段就能确定下来需要分配的...

    Java

    所承诺的自动内存管理主要是针对对象内存的回收和对象内存的分配.

    在 Java

    虚拟机的五块内存空间中,程序计数器、Java

    虚拟机栈、本地方法栈内存的分配和回收都具有确定性,一半都在编译阶段就能确定下来需要分配的内存大小,并且由于都是线程私有.

    因此它们的内存空间都随着线程的创建而创建,线程的结束而回收.也就是这三个区域的内存分配和回收都具有确定性.

    而 Java

    虚拟机中的方法区因为是用来存储类信息、常量静态变量,这些数据的变动性较小,因此不是 Java

    内存管理重点需要关注的区域.

    而对于堆,所有线程共享,所有的对象都需要在堆中创建和回收.虽然每个对象的大小在类加载的时候就能确定.

    但对象的数量只有在程序运行期间才能确定,因此堆中内存的分配具有较大的不确定性.

    此外,对象的生命周期长短不一,因此需要针对不同生命周期的对象采用不同的内存回收算法,增加了内存回收的复杂性.

    综上所述:Java

    自动内存管理最核心的功能是堆内存中对象的分配与回收.

    a4c26d1e5885305701be709a3d33442f.png

    1.1 对象优先在 Eden

    区中分配

    目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代.

    在新生代中为了防止内存碎片问题,因此垃圾收集器一般都选用 "复制" 算法.因此,堆内存的新生代被进一步分为:Eden 区

    Survior1 区 Survior2 区.

    每次创建对象时,首先会在

    Eden 区中分配.

    若 Eden

    区已满,则在 Survior1 区中分配.若 Eden 区 Survior1 区剩余内存太少,导致对象无法放入该区域时,就会启用

    "分配担保",将当前 Eden 区 Survior1 区中的对象转移到老年代中,然后再将新对象存入 Eden

    区.

    1.2

    大对象直接进入老年代

    所谓 "大对象"

    就是指一个占用大量连续存储空间的对象,如数组.

    当发现一个大对象在

    Eden 区 Survior1 区中存不下的时候就需要分配担保机制把当前 Eden 区 Survior1

    区的所有对象都复制到老年代中去.

    我们知道,一个大对象能够存入 Eden 区 Survior1

    区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及到大量的复制,就会造成效率低下.

    因此,对于大对象我们直接把他放到老年代中去,从而就能避免大量的复制操作.

    那么,什么样的对象才是

    "大对象" 呢?

    通过-XX:PretrnureSizeThreshold 参数设置大对象,该参数用于设置大小超过该参数的对象被认为是

    "大对象",直接进入老年代.

    注意:该参数只对

    Serial 和 ParNew 收集器有效.

    1.3

    生命周期较长的对象进入老年代

    老年代用于存储生命周期较长的对象,那么我们如何判断一个对象的年龄呢?

    新生代中的每个对象都有一个年龄计数器,当新生代发生一次 MinorGC

    后,存活下来的对象的年龄就加一,当年龄超过一定值时,就将超过该值的所有对象转移到老年代中去.

    使用-XXMaxTenuringThreshold

    设置新生代的最大年龄,设置该参数后,只要超过该参数的新生代对象都会被转移到老年代中去.武

    1.4

    相同年龄的对象内存超过 Survior 内存一半的对象进入老年代

    如果当前新生代的

    Survior 中,年龄相同的对象的内存空间总和超过了 Survior

    内存空间的一半,那么所有年龄相同的对象和超过该年龄的对象都被转移到老年代中去.

    无需等到对象的年龄超过

    MaxTenuringThreshold 才被转移到老年代中去.

    1.5 "分配担保"

    策略详解

    当垃圾收集器准备要在新生代发起一次 MinorGC 时,首先会检查 "老年代中最大的连续空闲区域的大小 是否大于

    新生代中所有对象的大小?",也就是老年代中目前能够将新生代中所有对象全部装下?

    若老年代能够装下新生代中所有的对象,那么此时进行 MinorGC 没有任何风险,然后就进行

    MinorGC.

    若老年代无法装下新生代中所有的对象,那么此时进行 MinorGC 是有风险的,垃圾收集器会进行一次预测:根据以往 MinorGC

    过后存活对象的平均数来预测这次 MinorGC 后存活对象的平均数.

    如果以往存活对象的平均数小于当前老年代最大的连续空闲空间,那么就进行 MinorGC,虽然此次 MinorGC

    是有风险的.

    如果以往存活对象的平均数大于当前老年代最大的连续空闲空间,那么就对老年代进行一次 Full

    GC,通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保.

    这个过程就是分配担保.

    注意:

    分配担保是老年代为新生代作担保;

    新生代中使用 "复制"

    算法实现垃圾回收,老年代中使用 "标记-清除" 或 "标记-整理" 算法实现垃圾回收,只有使用 "复制"

    算法的区域才需要分配担保,因此新生代需要分配担保,而老年代不需要分配担保.

    四、了解 Java

    虚拟机的垃圾回收算法

    Java

    虚拟机的内存模型分为五个部分,分别是:程序计数器、Java

    虚拟机栈、本地方法栈、堆、方法区.

    这五个区域既然是存储空间,那么为了避免 Java

    虚拟机在运行期间内存存满的情况,就必须得有一个垃圾收集者的角色,不定期地回收一些无效内存,以保障 Java

    虚拟机能够健康地持续运行.

    这个垃圾收集者就是平常我们所说的

    "垃圾收集器",那么垃圾收集器在何时清扫内存?清扫哪些数据?这就是接下来我们要解决的问题.

    程序计数器、Java

    虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁.

    那么,垃圾收集器在何时清扫这三块区域的问题就解决了.

    此外,Java

    虚拟机栈、本地方法栈中的栈帧会随着方法的开始而入栈,方法的结束而出栈,并且每个栈帧中的本地变量表都是在类被加载的时候就确定的.

    因此以上三个区域的垃圾收集工作具有确定性,垃圾收集器能够清楚地知道何时清扫这三块区域中的哪些数据.

    然而,堆和方法区中的内存清理工作就没那么容易了.

    堆和方法区所有线程共享,并且都在 JVM 启动时创建,一直得运行到 JVM

    停止时.因此它们没办法根据线程的创建而创建、线程的结束而释放.

    堆中存放 JVM

    运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就确定了,但究竟创建多少个对象只有在程序运行期间才能确定.

    方法区中存放类信息、静态成员变量、常量.类的加载是在程序运行过程中,当需要创建这个类的对象时才会加载这个类.因此,JVM

    究竟要加载多少个类也需要在程序运行期间确定.

    因此,堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一些心思.

    1.1

    堆内存的回收

    1.1.1

    如何判定哪些对象需要回收?

    在对堆进行对象回收之前,首先要判断哪些是无效对象.我们知道,一个对象不被任何对象或变量引用,那么就是无效对象,需要被回收.一般有两种判别方式:

    引用计数法:每个对象都有一个计数器,当这个对象被一个变量或另一个对象引用一次,该计数器加一;若该引用失效则计数器减一.当计数器为 0

    时,就认为该对象是无效对象.

    可达性分析法:所有和

    GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots

    没有关联的对象就是无效对象.

    GC Roots

    是指:

    Java

    虚拟机栈所引用的对象 (栈帧中局部变量表中引用类型的变量所引用的对象);

    方法区中静态属性引用的对象;

    方法区中常量所引用的对象;

    本地方法栈所引用的对象.

    两者对比:

    引用计数法虽然简单,但存在一个严重的问题,它无法解决循环引用的问题.

    因此,目前主流语言均使用可达性分析方法来判断对象是否有效.

    1.1.2

    回收无效对象的过程

    当 JVM

    筛选出失效的对象之后,并不是立即清除,而是再给对象一次重生的机会,具体过程如下:

    判断该对象是否覆盖了

    finalize() 方法;

    若已覆盖该方法,并该对象的 finalize() 方法还没有被执行过,那么就会将 finalize() 扔到 F-Queue

    队列中;

    若未覆盖该方法,则直接释放对象内存.

    执行 F-Queue

    队列中的 finalize() 方法;

    虚拟机会以较低的优先级执行这些 finalize() 方法们,也不会确保所有的 finalize()

    方法都会执行结束.

    如果

    finalize() 方法中出现耗时操作,虚拟机就直接停止执行,将该对象清除.

    对象重生或死亡;

    如果在执行

    finalize() 方法时,将 this

    赋给了某一个引用,那么该对象就重生了.如果没有,那么就会被垃圾收集器清除.

    注意:强烈不建议使用

    finalize() 函数进行任何操作!如果需要释放资源,请使用 try-finally.因为 finalize()

    不确定性大,开销大,无法保证顺利执行.

    1.2

    方法区的内存回收

    我们知道,如果使用复制算法实现堆的内存回收,堆就会被分为新生代和老年代,新生代中的对象

    "朝生夕死",每次垃圾回收都会清除掉大量的对象;而老年代中的对象生命较长,每次垃圾回收只有少量的对象被清除掉.

    由于方法区中存放生命周期较长的类信息、常量、静态变量,因此方法区就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉.

    方法区中主要清除两种垃圾:

    废弃常量;

    废弃的类.

    1.2.1

    如何判定废弃常量?

    清除废弃的常量和清除对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉.

    1.2.2

    如何废弃废弃的类?

    清除废弃类的条件较为苛刻:

    该类的所有对象都已被清除;

    该类的

    java.lang.Class

    对象没有被任何对象或变量引用;只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class.这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除;

    加载该类的

    ClassLoader 已经被回收.

    1.3

    垃圾收集算法

    现在我们知道了判定一个对象是无效对象、判定一个类是废弃类、判定一个常量是废弃常量的方法,也就是知道了垃圾收集器会清除哪些数据,那么接下来介绍如何清除这些数据.

    1.3.1

    标记-清除算法

    首先利用刚才介绍的方法判断需要清除哪些数据,并给它们做上标记;然后清除被标记的数据.

    分析:这种算法标记和清除过程效率都很低,而且清除完后存在大量碎片空间,导致无法存储大对象,降低了空间利用率.

    1.3.2

    复制算法

    将内存分成两份,只将数据存储在其中一块上.当需要回收垃圾时,也是首先标记出废弃的数据,然后将有用的数据复制到另一块内存上,最后将第一块内存全部清除.

    分析:这种算法避免了碎片空间,但内存被缩小了一半.而且每次都需要将有用的数据全部复制到另一片内存上去,效率不高.

    解决空间利用率:在新生代中,由于大量的对象都是

    "朝生夕死",也就是一次垃圾收集后只有少量对象存活,因此我们可以将内存划分成三块:Eden、Survior1、Survior2,内存大小分别是

    8:1:1.

    分配内存时,只使用

    Eden 和一块 Survior1.当发现 Eden Survior1 的内存即将满时,JVM 会发起一次

    MinorGC,清除掉废弃的对象,并将所有存活下来的对象复制到另一块 Survior2 中.那么,接下来就使用 Survior2

    Eden 进行内存分配.

    通过这种方式,只需要浪费

    10% 的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题.

    但是,当一个对象要申请内存空间时,发现 Eden Survior 中剩下的空间无法放置该对象,此时需要进行 Minor GC,如果

    MinorGC 过后空闲出来的内存空间仍然无法放置该对象,那么此时就需要将对象转移到老年代中,这种方式叫做

    "分配担保".

    什么是分配担保?

    当 JVM

    准备为一个对象分配内存空间时,发现此时 Eden Survior 中空闲的区域无法装下该对象,那么就会触发

    MinorGC,对该区域的废弃对象进行回收.

    但如果 MinorGC

    过后只有少量对象被回收,仍然无法装下新对象,那么此时需要将 Eden Survior 中的所有对象都转移到老年代中,然后再将新对象存入

    Eden 区.这个过程就是 "分配担保".

    1.3.3

    标记-整理算法

    在回收垃圾前,首先将所有废弃的对象做上标记,然后将所有未被标记的对象移到一边,最后清空另一边区域即可.

    分析:它是一种老年代的垃圾收集算法.

    老年代中的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,因此如果选用 "复制"

    算法,每次需要复制大量存活的对象,会导致效率很低.

    而且,在新生代中使用

    "复制" 算法,当 Eden Survior 中都装不下某个对象时,可以使用老年代的内存进行

    "分配担保",而如果在老年代使用该算法,那么在老年代中如果出现 Eden Survior

    装不下某个对象时,没有其他区域给他作分配担保.

    因此,老年代中一般使用

    "标记-整理" 算法.

    1.3.4

    分代收集算法

    将内存划分为老年代和新生代.老年代中存放寿命较长的对象,新生代中存放 "朝生夕死"

    的对象.然后在不同的区域使用不同的垃圾收集算法.

    1.4 Java

    中引用的种类

    Java

    中根据生命周期的长短,将引用分为 4 类.

    1.4.1

    强引用

    我们平时所使用的引用就是强引用.

    A a = new

    A(); 也就是通过关键字 new 创建的对象所关联的引用就是强引用.

    只要强引用存在,该对象永远也不会被回收.

    1.4.2

    软引用

    只有当堆即将发生 OOM

    异常时,JVM 才会回收软引用所指向的对象.

    软引用通过

    SoftReference 类实现.软引用的生命周期比强引用短一些.

    1.4.3

    弱引用

    只要垃圾收集器运行,软引用所指向的对象就会被回收.

    弱引用通过

    WeakReference 类实现.弱引用的生命周期比软引用短.

    1.4.4

    虚引用

    虚引用也叫幽灵引用,它和没有引用没有区别,无法通过虚引用访问对象的任何属性或函数.

    一个对象关联虚引用唯一的作用就是在该对象被垃圾收集器回收之前会受到一条系统通知.虚引用通过 PhantomReference

    类来实现.

    展开全文
  • 本文重点介绍创建对象和使用对象内存分配问题,我尽量用简单的方法帮助大家理解new语法背后的内存逻辑。 我们来先介绍两个概念: 栈内存:位于通用RAM(随机访问存储器)中,程序通过栈指针可以直接获取到存储在...
  • 一、对象的创建1....2.分配内存在类加载完成后就可以完全确定对象所需内存了,这时内存分配可以分为两种,java堆内存规整和不规整。java堆是否完整取决于垃圾收集器是否带有压缩整理功能。1.指针碰撞...
  • 对象创建要考虑的两个问题1、内存分配算法指针碰撞算法,将内存区域分成两部分中间采用指针分隔开来,分配对象就将指针向一个方向移动,这种需要内存区域规整。不规整就要通过空闲列表来记录那块内存是否空闲。内存...
  • 2 分配内存  类加载完成后,所占用Eden内存大小基本确定(类的属性所占大小等)  分配内存方法: - 指针碰撞(默认指针碰撞)  如果堆中的内存是规整的(即所有用过的内存在一边,没用过的内存在另
  • (1)对象的创建①检查new指令的参数是否能在常量池中定位到一个类的符号引用,检查符号引用代表的类是否已经加载、解析和初始化②虚拟机为新生对象分配内存对象所需内存的大小在类加载之后即可确定(从Java堆中分配...
  • Java对象内存分配过程保证线程安全,对象的内存分配过程就必须进行同步控制。 对象的内存分配过程中,主要是对象的引用指向这个内存区域进行【初始化操作】。 但因为堆是全局共享,在同一时间可能有多个线程在堆上...
  • 那么对应的内存分配为 : ① a是分配在栈内存中,里面存放了一个指向堆内存中存放的new A()的地址。 ② new A()会导致在堆内存中分配一块空间,该内存中的A对象同时会含有a和b。 ③ work()方法会在codesegment区中...
  • 直接指针访问:Sun HotSpot 内存分配策略 对象创建过程 new指令,检查指令参数在常量池中是否定位到一个类的符号引用,并检查符号引用代表的类是否已被加载、解析、初始化过,若没有,则执行类加载过程到方法区;...
  • 本文主要讲述Java对象在虚拟机中创建,分配内存,初始化的过程,以及分配内存,引用对象的几种常见方式。对象创建对象创建分为三部分,首先是类加载,接着是为对象分配内存,最后是初始化。创建虚拟机遇到new指令时...
  • 在类加载检查通过,接下来虚拟机将会新生对象分配对象对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同与把一块确定大小的内存同堆中划分出来,分配方式有两种,指针碰撞和空闲列表,使用这两...
  • 接着上篇《JVM源码分析之Java对象的创建过程》,本文对Java对象内存分配过程进行深入分析,其中有以下几种分配方式:1、从线程的局部缓冲区分配临时内存2、从内存堆中分配临时内存3、从内存堆中分配永久内存新建一...
  • Java虚拟机学习之对象内存分配 java对象使用new关键字新建之后,通过类加载检查后,需要给对象分配内存。java虚拟机主要有两种方式: 1. 指针碰撞(Bump The Pointer) 2. 空闲列表(Free List) 一、指针碰撞 ...
  • 准备:设置测试类的堆内存VM options:-Xms20m -Xmx20m 名词解析: ManagementFactory:是一个提供各种获取JVM信息的工厂类,使用ManagementFactory可以获取大量的运行时JVM信息,比如JVM堆的使用情况,以及...
  • python 列表, 元组内存分配优化1. 空元组与空列表>>> a = ()>>> b = ()>>> a is bTrue>>> id(a)4374097992>>> id(b)4374097992>>> a = []>>> b = ...
  • JVM对象创建与内存分配 前言 在我们创建对象时的一个流程是怎样的,创建的对象又应该在哪里分配给他内存,下面让我们一起来看一下吧。 对象创建的流程 在我们创建一个对象时,主要经历以下几个阶段: 类加载检查...
  • 对象的创建 ...在类加载检查通过后,接下来虚拟机将为新生对象分配内存对象所需内存的大小在类 加载完成后便可完全确定,为 对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。 这
  • 文章目录1、对象的创建1.1、类加载检查1.2、分配内存1.3、初始化1.4、设置对象头1.5、执行方法 1、对象的创建 对象创建的主要流程: 1.1、类加载检查 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能...
  • 引子,对象的创建 对象创建的主要流程: 1 类加载检查 虚拟机遇到一条new指令时,首先将去...对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
  • 前言在java开发中,我们普遍认知中,new出的对象是直接分配到堆空间中,...new出来的对象并非所有都存在堆内存中,其实还有其他另外两个地方可以进行存储new出的对象,称之为栈上分配和TLAB栈上分配为什么需要栈上分配在...
  • 创建对象内存分配在堆上还是栈上面?大部分童鞋的回答是这样的:“肯定分配在堆内存的嘛,栈内存的那是java栈属于子线程专用的内存空间”
  • 在学习jvm内存结构的时候,了解jvm的内存管理,能够按照自己的理解表达出实例化一个对象时jvm内存分配的过程,可以帮助更好的理解和记忆jvm的内存结构。1. 内存申请的过程HeapJava对象所占用的内存 主要是从堆中进行...
  • 当java中new一个对象,背后发生了什么jvm全局观今天我们谈谈 当java中new一个对象,背后发生了什么概括说来,就是 先后执行类加载,分配内存,初始化零值,设置对象头,初始化对象看完本篇文章,读者将能够回答以下问题1....
  • java内存分配

    2021-03-09 00:54:36
    栈、堆、常量池虽同属Java内存分配时操作的区域,但其适用范围和功用却大不相同。本文将深入Java核心,详细讲解Java内存分配方面的知识。Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与...
  • 1 JVM内存分配策略 对象优先在 Eden 区分配 多数情况,对象都在新生代 Eden 区分配,当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC,如果本次 GC 后还是没有足够的空间,则将启用分配担保...
  • 内存分为三种:线程的堆栈、GC堆、LOH(large object heap)堆...当有内存分配或者回收时,垃圾收集器可能会对GC堆进行压缩; LOH堆: 用于分配大对象实例。如果对象的实例大小>=85000字节,将分配在LOH堆上; L
  • 原标题:JVM对象内存分配对象优先在伊甸园分配大对象直接进入老年代JVM参数-XX:+PrintGCDetails在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。Minor GC VS Full GC...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 806,144
精华内容 322,457
关键字:

对象内存分配