一、对象创建过程

执行new关键字过程
1.类加载检查
2.内存分配(对象所需要的的内存大小,在类加载完成后,即可确定
3.初始化(为对象属性设置默认值)
4.设置对象头
5.执行init方法(为对象属性赋值)

1.类加载检查

执行new指令时。首先检查常量池中是否可以定位到一个类的符号引用。检查这个类是否已经被加载、解析、初始化。若没有则进行类加载过程。

2.内存分配

类加载检查通过后,虚拟机为新对象分配内存。对象所需要的的内存大小,在类加载完成后,可以确定确定。分配空间等同于将一定大小的空间从堆中划出(或称为占用)。

指针碰撞方式 (内存规整)

java堆绝对规整,使用的空间在一边,未使用空间在一边,中间使用指针分界,分配内存,只需移动指针
与垃圾回收机制也相关。例如复制删除,内存会保持内存规整。而标记清理:则会导致 内存不规范

空闲列表(内存不规整情况)

java堆内存不规范,使用与未使用内存相互交错,jvm维护可用内存的列表

内存分配中的并发问题

并发情况下,可能正在给A对象分配内存,指针还没有修改,对象B再次使用指向同一处的指针,分配空间。

并发问题解决

1.使用CAS+失败重试方案。保证更新的原子性,对分配空间动作进行同步处理
2.本地线程分配缓冲(TLAB)。把内存分配的动作,按照线程划分到不同空间中进行,每个线程在堆中预先分配一块内存空间,(JVM会默认开启-XX:+UseTLAB)
按照线程划分到不同空间中进行 ???,最终都是在堆中申请空间,为什么没有并发问题。
image.png
设置大对象阈值,长时间存活大对象直接进入老年代,避免不必要的minor gc垃圾回收
JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在Serial 和ParNew两个收集器下有效
设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC

3.初始化

内存分配完成后,虚拟机将分配的内存空间初始化为零值(不含对象头),使用TLAB方式,初始化可在TLAB阶段完成。 保证对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.设置对象头

实例对象,存在于堆中。包含:对象头、实例数据、对齐填充

对象头(Mark word+Klass pointer
1.对象头还存在Klass pointer类型指针,指向元空间类元信息,jvm使用类元信息时,利用Klass pointer类型指针获取
2.程序开发者,利用Class对象,获取元空间中对应的类元信息。
注意,元空间中class类的类元信息,使用c++对象表示的。Class对象中不存在类元信息,只是元信息获取的入口

5.执行init方法

执行init方法,即对象为对象赋值,执行构造方法。

二、对象压缩

可参考synchronized笔记中关于对象头与对象压缩
java对象压缩
1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
2.jvm配置参数:UseCompressedOops,compressed—压缩、oop(ordinary object pointer)—对象指针
3.启用指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops

为什么要进行指针压缩?

1.在64位平台的HotSpot中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能 (1.6版本后,默认开启压缩)
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的存入堆内存时压缩编码、取出到cpu寄存器后解码方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好

三、逃逸分析(1.7后默认开启)

为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问
如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中 。
只有对象不会发生逃逸时,才会进行标量替换。

标量替换

通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。
开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。
标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
结论:栈上分配依赖于逃逸分析和标量替换

四、垃圾回收

Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。

Minor GC 触发

对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC 。minor gc回收eden区与survivo区

Eden与Survivor区为什么默认为8:1:1

大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可(仅存放幸存对象)。

对象在survive区并不一定年龄到达15才进入老年代

1.大对象直接进入老年代。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代(对于ParNew、Serial回收器可以配置,其他回收器不可配置)
2.动态年龄判断机制,当前放对象的Survivor区域里,一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代
为了避免为大对象分配内存时的复制操作而降低效率。

老年代空间分配担保机制(导致父GC)

Eden区在做minor gc前,会和老年代剩余空间进行对比,如果老年代剩余空间大于当前Eden区对象(包含垃圾对象),则做minor gc。否则判断历史minor gc后,所剩对象平均大小。如果old去小于平均大小,则进行full gc,如果回收完还是没有足够空间存放新的对象就会发生”OOM” 。
image.png

对象内存回收算法

1.引用计数法,每增加一个引用,则+1。方法简单、高效,但是很少使用,因为很难解决互相引用
2.可达性分析。将GC root对象作为起点,从这个起点向下搜索引用对象,找到的对象都标记为非垃圾对象。其余的都为垃圾对象
GC Root 线程栈的本地变量、静态变量、本地方法栈变量

方法区无用类回收

  • 该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,

finalize()方法最终判定对象是否存活

finalize()方法最终判定对象是否存活

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,此时它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1. 第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,对象将直接被回收。
2. 第二次标记
如果这个对象覆盖了finalize方法,finalize方法是对象“死里逃生”的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可。
譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法“自救”的机会就一次。

如何判断一个类处于无用的(回收类元信息条件)
1.该类所有的对象实例都被回收,即java堆中
2.加载该类的ClassLoad被回收(一般自定义的类加载器才被回收,例如tomcat中的自定义类加载器)
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任务地方通过反射访问到该类的方法。
同时,满足3个条件,方法区类元信息才能被回收