程序计数器是一个较小的内存空间,他可以看做当前线程所执行的字节码的行号指示器,由于java虚拟机的多线程是通过线程轮流切换并分配处理器的执行时间来实现的,任何一个确定的时刻,一个处理器(对应于多核中的一个内核)都只会执行一个线程中的指令,因此为了线程切换后内回到正确的位置,每个线程都有一个独立的程序计数器,各线程之间的程序计数器互不影响,我们称为 线程私有内存 唯一一个没有内存溢出的区域
Java虚拟机栈也是线程私有的,他描述的是java方法执行的内存模型,每个方法执行的同时都要创建一个栈帧,用于储存局部变量表,操作数栈,动态链接,方法出口,等信息,每一个方法从调用到执行完成,都对应一个栈帧在虚拟机栈中从入栈到出栈的过程
局部变量表放置了,编译器可知的各种基本数据类型,和对象引用
一般认为他和虚拟栈为一体
java堆是java虚拟机中占用内存最大的一块,他是线程共享的一块内存,在虚拟机启动时创建,此区域唯一的目的就是放置对象的实例,几乎所有的对象实例都是在这里分配内存,java堆是垃圾回收器的主要区域,也被称作为GC堆,可处于物理内存不了连续的空间
和java堆是一样,是各个线程共享的内存区域,它用于储存已经被虚拟机加载的,类信息,常量,静态变量,即时编译器编译后的代码
是方法区的一部分,class文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。
(1)当虚拟机遇到一个new指令时,首先去检查这个指令的参数是否能在常量池定位到一个类的引用符号,并且检查引用符号代表的类是否已经被加载,解析,初始化过,如果没有必须执行相应的类加载过程。
(2)当类加载检查通过后,虚拟机为为新生对象分配内存,有俩种方法分配内存
(3) 当内存分配完成后,虚拟机需要将分配的内存,都初始化为零值(不包括对象头),这一步保证了,对象的实例字段在java代码中可以不赋初始值就可以使用,程序能访问到,这些字段数据类型对应的零值
(4)接下来,虚拟机要对对象进行必要的设置,比如,这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息,这些信息存放在对象的对象头中。
(5)上面的工作完成后,从虚拟机的角度,一个对象已经产生,单从java的角度来看,对象的创建才刚刚开始,- 方法还没没有执行,所有字段都为零,所以来说,new之后执行,按照程序员的意愿初始化对象,这样一个完整可用的对象才算创建出来
上面介绍了java运行时内存区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈,这三个区域随线程而生,随线程而死,这几个区域内存分配和回收都具有确定性,这几个区域不用过多的考虑回收,因为线程结束,或者方法结束时,内存自然就回收了,而java的堆和方法区不一样,一个接口中多个实现类所需要的内存可能不一样,一个方法中的多个分支,需要的内存也可能不一样,我们只有在程序运行期间,才知道会创建那些对象,这部分的内存分配和回收都是动态的,垃圾回收器主要关注这个部分。
给对象加一个引用计数器,每当有地方引用他时就加一,引用失效时就减一,任何时刻,引用计数为0 就是没有被引用的,但他很难解决,对象互相引用的情况,所以虚拟机并没有使用这种方法。
在主流的实现中都是采用可达性算法,来判定对象是否存活,这个算法的基本思路是通过一个“GC Root” 的对象作为起点,从这些起点向下搜索,搜索做过的路径叫做引用链,当一个对象到“GC Root” 没有任何引用链时,说明此对象是不可用的
强引用是指在代码中普遍存在的Object obj =new Object(),这类的引用有引用变量指向时,永远不会被垃圾回收,JVM宁可抛出OutofMemory也不会回收这种对象,如果想中断某个强引用和对象的之间的联系,可以将引用赋值为null
用来描述一些有用但非必须的对象,对于软引用关联着的对象,在系统发生内存溢出之前,会把这些对象列入回收范围之内,进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出异常,软引用经常用于内存敏感的高速缓存,比如,网页缓存,图片缓存,防止内存泄漏,增强代码的健壮性
Object o = new Object(); SoftReference<Object> softReference = new SoftReference<Object>(o); //如果设置 o=null,如果软引用没有被回收依然可以通过获取object对象 Object o1 = softReference.get(); 复制代码
表示非必须的对象,不管内存是否充足,都会回收该对象 只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
即使在可达性算法中,不可达对象,也不是非死不可,这时他们处于“缓刑”阶段,要宣告一个对象真正死亡需要至少俩个标记阶段, 如果发现对象没有引用链,则会进行第一次标记,并进行一次筛选,筛选的条件是此对象是否有必要进行 finalize()
方法,当对象没有覆盖 finalize()
,或者 finalize()
已经调用过,这俩种都视为“没有必要执行” 若果这个对象被视为 有必要执行,则会放入到一个F-Queue队列 中,并稍后有虚拟机建立一个低优先级的Finalizer线程去执行它,所谓的执行,是指会触发这个方法,但并不承诺等他执行完成,并不建议大家用这个方法做回收,try-finally 更加靠谱
该算法分为标记和清除俩个阶段,首先要标记出需要回收的各个对象,在标记完成后统一回收被标记的对象, 他有俩个不足 1 效率问题:标记清除俩个过程效率都不是很高 2 空间问题:会产生大量的不连续空间
为了解决效率问题,他可以将内存划分为完全相同的俩块,每次只使用其中的一块,当这块用完了,就把还存活的复制到另一块上去,然后把已经使用过的内存空间一次清理掉,不足,会把内存缩小为原来的一半
复制算法在存活较多的情况下,效率较低,而且会浪费掉50%的空间 ,所以老年代不能选择这种算法,根据老年代存活率特别高的特点,又提出一种 标记整理的算法,标记过程和“标记清除” 一样,但后续步骤不是对可回收对象进行清理,而是让所有存活的对象,向一端移动,然后直接清理掉端以外的内存
当前虚拟机大部分采用,分代收集算法,这种算法并没有特别思想,只是根据对象的存活周期不同把内存划分为几块,一般是吧java堆分为新生代和老年代,这样就可以根据年代的特点采用不同的算法,提高效率,新生代每次垃圾回收都会有大量的对象死去,少量存活,那就用复制算法,老年代存活率较低,那就使用标记-清除,或标记-整理法
大多数对象在新生代中创建,其中很多对象的生命周期很短,每次新生代的垃圾回收(又称 Minor GC ),只有少量对象存活,所以选择复制算法,因为少量的复制成本就可以完成 新生代又分为三个区,一个 Eden 区,两个 Survivor 区(一般而言),大部分对象在 Eden 区中生成,当Eden区满了之后,还存活的对象复制到Survivor区中的一个,当这个Survivor区满了之后,此区存活但不满足 晋升 条件的对象,复制到另一个Survivor区,对象每一次Minor GC年龄加一,达到年龄的阈值后,晋升老年区,默认的阈值为15岁
新生代经历n次垃圾回收,还存活的对象就会被放到老年代,此区域中对象存活率高,老年代的垃圾回收,通常用标记清理和标记整理的方法,整堆包括新生代和老年代的垃圾回收称为 Full GC
主要存放元数据,如Class何Method的元数据,与垃圾回收对象的关系不大,相对于新生代和老年代来说,该区划分对垃圾回收影响较小
分配内存不够引起的GC,会stop world,是并发GC,其他线程都会停止,直到GC完成
内存达到一定的阈值,进行的GC,是一个后台GC,不会stop world
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
Minor GC触发条件:当Eden区满时,触发Minor GC。
老年代 GC(Full GC ):指发生在老年代的 GC,经常会伴随至少一次的 Minor GC 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。
Full GC触发条件:(1)老年代空间不足(2)升到老年代的对象大于老年代剩余空间
显示调用时进行的GC,显式调用System.gc时会调用Full GC
参考:深入理解java虚拟机
tech.meituan.com/2017/12/29/…
jsonchao.github.io/2019/08/18/…