上一篇类加载机制中,将.class文件加载到内存中,根据.class二进制文件将不同的信息放入到运行时数据区的不同部分,其中关于new的对象分配在运行时的数据区的哪里在这一篇详解
1.对象的创建以new Math(); 为例
1.类加载检查虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
检查类是否已经完成了类加载过程,也就是去常量池找是否有关于Math的字节码文件等信息2.加载类
如果类加载检查以后,发现没有加载,就使用双亲委派机制开始加载类,等一系列的loadClass,defineClass过程3.分配内存
问题一: 在哪里分配 方案一: 指针碰撞法: 如果我们的对象是整齐排列的,使用一个指针记录没分配的地址,寻址时碰撞到那个标志位指针,直接分配即可 方案二: 空闲列表法: 如果对象并不似整齐排列的,那么就需要一个空闲列表记录没分配过的内存地址,进行分配,并从空间列表将分配的剔除
问题二: 并发分配冲突 当多线程情况下,同一个地址被两个线程都想分配发生冲突怎么办? 方案一: CAS: 虚拟机采用CAS配上失败重试的方式保证更新 *** 作的原子性来对分配内存空间的动作进行同步处理。如果当前分配地址已经被另一个线程分配,就使用另外地址进行创建 方案二: TLAB 本地线程分配缓冲: 每个线程在Java堆中预先分配一小块内存,这样每个线程的对象创建就不会冲突了,但是如果线程分配的小块内存分配满了,就要使用CAS机制在这个小空间以外进行分配 -XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB),-XX:TLABSize 指定TLAB大小4.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步 *** 作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值5.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
对象的组成
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
32位对象的组成
64位对象头
对象头的组成
1,Mark Word: hashCode 锁信息 分代年龄(4位 最大15) 锁标志位 是否为偏向锁信息 2,指向类的指针: 这个指针指向的是方法区放置类元信息的地址 指针压缩(4byte) 不压缩(8byte) 3,数组长度(只有数组对象才有): 4字节
Class对象和类元数据的区别
类元数据: 放在方法区中,由C++代码等进行实现,对于我们Java开发调用C++十分不方便 Class对象: 放在堆中,由类加载时创建,是类元数据中一些主要信息的副本,用于我们的开发过程java代码方便调用 因此,我们对象的头部指针指向的是类元信息,而不是指向一个为了方便创建的副本2.对象大小
import org.openjdk.jol.info.ClassLayout; public class JOLSample { public static void main(String[] args) { ClassLayout layout = ClassLayout.parseInstance(new Object()); System.out.println(layout.toPrintable()); System.out.println(); ClassLayout layout1 = ClassLayout.parseInstance(new int[]{}); System.out.println(layout1.toPrintable()); System.out.println(); ClassLayout layout2 = ClassLayout.parseInstance(new A()); System.out.println(layout2.toPrintable()); } // -XX:+UseCompressedOops 默认开启的压缩所有指针 // -XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer // Oops : Ordinary Object Pointers public static class A { //8B mark word //4B Klass Pointer 如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8B int id; //4B String name; //4B 如果关闭压缩-XX:-UseCompressedOops,则占用8B byte b; //1B Object o; //4B 如果关闭压缩-XX:-UseCompressedOops,则占用8B } } 运行结果: java.lang.Object object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) //mark word 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) //mark word 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) //Klass Pointer 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total [I object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 0 int [I.N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total com.tuling.jvm.JOLSample$A object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 61 cc 00 f8 (01100001 11001100 00000000 11111000) (-134165407) 12 4 int A.id 0 16 1 byte A.b 0 17 3 (alignment/padding gap) 20 4 java.lang.String A.name null 24 4 java.lang.Object A.o null 28 4 (loss due to the next object alignment) Instance size: 32 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total 一定是8byte的倍数,不足使用对齐补充 头大小(Mark word+类型指针) + 实例数据 + 数组大小(不是数组没有) + 对齐 Object o = new Object(); 这段代码大小: o (分配在栈中的普通指针) + new Object(堆中的对象大小)
指针压缩
指针类型
指针分为两类: 普通指针: 栈中指向堆对象的指针,对象中实例数据的指针 类型指针: 对象头中指向类元信息的指针
指针压缩
压缩指针: 这两个默认都开启 +UseCompressedClassPointers 使用压缩类型指针,compressed--压缩、oop(ordinary object pointer)--对象指针 +UseCompressedOops(OOPs--> Ordinary Object Pointers) 普通对象指针 禁止指针压缩:-XX:-UseCompressedOops
为什么要压缩指针?
在32位的 *** 作系统中,32位可以表示32个二进制数,2^32个数,每一个数可以对应找到一个内存地址,那么最多可以寻址2^32个内存地址,也就是4G 正常来说在jvm中,32位地址最大支持4G内存(2的32次方),但是可以通过对对象指针的存入堆内存时压缩编码、取出到cpu寄存器后解码方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G),但是cpu实际运算还是35位的寻址
那么现在64位了为什么还要压缩?
64位 *** 作系统可以表示的内存大小为2^64,最大可以表示几T的内存,不存在寻址不够的情况 1. 在64位平台的HotSpot中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力 2. 为了减少64位平台下内存的消耗,启用指针压缩功能 3. 堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间 4. 堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好 总结: 寻址范围变大,相应的寻址等成本变大,压缩可以节省空间,既能减小成本,又能节省空间,何乐而不为 一般堆空间也不会超过32G3.对象内存分配
在上面对象创建的描述中,提到了对象的分配位置,粗略的描述了对象分配在内存中,但是JVM对内存进行了详细的划分,分别有自己的名字
1.对象栈上分配结论:栈上分配依赖于逃逸分析和标量替换
正常我们的对象是创建在堆上的,对象的回收要通过GC来处理,对象较多时,GC性能下降 能否有一种不需要GC,自动回收的分配方式: 对象分配在栈帧中,通过这个当前方法的结束,自动销毁栈帧,同时销毁创建在栈帧的对象
那么当我们的对象创建以后如果需要当前方法以外使用怎么办?
逃逸分析
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。 例: 如下两个方法中对象的作用域并不一样 JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,如果逃逸就堆上分配,如果不逃逸就栈上分配,JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
//这个方法的返回值为这个方法内创建的对象,如果方法结束对象被回收方法外面的方法就找不到这个对象了 public User test1() { User user = new User(); user.setId(1); user.setName("zhuge"); //TODO 保存到数据库 return user; } //这个方法创建的对象,只在方法内部使用,回收也不会造成影响 public void test2() { User user = new User(); user.setId(1); user.setName("zhuge"); //TODO 保存到数据库 }
标量替换
2.对象在Eden分配标量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等) 聚合量: 标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。我们的对象在栈上分配时,有可能一个对象比较大,栈帧上没有一整块空间可以分配,那么JVM将会给我们标量替换,将这个对象的属性拆分开,分成多个小部分分配在栈帧上,并且有一些数据标记这些拆分开的属性是数据这一个大对象的 标量替换的前提是对象打算在栈上分配,也就是对象不逃逸 开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。
3.对象直接进入老年代大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。我们来进行实际测试一下。 在测试之前我们先来看看 Minor GC和Full GC 有什么不同呢? Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。 Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。 Eden与Survivor区默认8:1:1 大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可, JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy
4.长期存活的对象将进入老年代情况一: 对象由Eden区直接进入老年代(对象动态年龄判断) 当我们的Eden区快要满了的时候此时Eden区对象为O1,这时又来的新创建的对象O2,但是这个Eden剩余的空间不足够放O2,那么就要发生Minor GC(Eden区GC),GC以后O1对象存活,那么O1对象要进入Survior空间,但是Survior空间也不够大,那么对象O1对象不经过Survior,直接进入老年代情况二: 对象直接在Old区(老年代)分配 当我们的大对象,想要在堆上分配空间时,由于它是(大)对象,可以不在eden区分配,直接在Old区分配 多大才算大? 什么垃圾回收器可以使用? JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代 这个参数只在 Serial 和ParNew两个收集器下有效。
5.对象动态年龄判断既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
6.老年代空间分配担保机制当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。 例1: survior大小为100M,现在Eden区发生了一次GC,幸存下来的对象大小是49M,分代年龄为1,现在想要放入到survior区域,此时survior区域存在分代年龄为2(2M),3(15M)的对象,此时1 < 50% ,1+2的和>50%,此时就会将>=n也就是2和3放入到老年代,1放在survior 例2: survior大小为100M,现在Eden区发生了一次GC,幸存下来的对象大小是60M,分代年龄为1,现在想要放入到survior区域,此时不管survior中分代年龄2,3还是5,都会将1和他们一起放入到老年代,因为年龄1>50%,会把年龄1(含)以上的对象都放入老年代
4.对象内存回收年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间 如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。 如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM" 当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”1. minor gc前,判断老年代空间是否够大 2. 如果不够大,查看是否有-XX:-HandlePromotionFailure参数,如果有进一步通过之前FGC的平均数判断 3. 如果进一步判断还是不够大或者没有进一步平均值判断,就FGC 4. 这种做法就是在minor gc前多次判断够不够大,不够大FGC,够大直接minor gc,是一种优化minor gc的方式,避免先minor gc,然后不够大,再FGC,这样minor gc的时间短,效率高
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
1.引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。
2.可达性分析算法
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象 GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等GC ROOT对象都有哪些? 当前正在被调用的方法里局部变量引用的对象,即虚拟机栈的局部变量表中引用的对象;没有结束的方法 方法区中静态变量引用的对象; 方法区中常量引用的对象; 本地方法栈中Native方法引用的对象;
3.常见引用类型
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
强引用:普通的变量引用
public static User user = new User();
软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
public static WeakReference
虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
4.判断一个类是无用的类(类元信息)
方法区主要回收的是无用的类,那么如何判断一个类是无用的类呢?
类需要同时满足下面3个条件才能算是 “无用的类” :
-
该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
-
加载该类的 ClassLoader 已经被回收。
-
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
由此可见,回收一个方法区中的类元数据信息十分的困难,因为三个类加载器一旦被创建就不会进行回收 但是我们自定义的类加载器可以被回收,因此我们自定义类加载器加载的对象可以被回收掉
5.安全点与安全区域
安全点
GC要进行垃圾回收时,不能直接进行,有很多是原子性 *** 作,如果这时要GC了直接进行,那么原子性会破坏,非常麻烦
所以要设置安全点,在原子性等一些重要的 *** 作都进行完,才进行GC
这些特定的安全点位置主要有以下几种: 1. 方法返回之前 2. 调用某个方法之后 3. 抛出异常的位置 4. 循环的末尾 大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程 *** 作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和安全点是重合的。
安全区域
Safe Point 是对正在执行的线程设定的。 如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。 因此 JVM 引入了 Safe Region。 Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的 简单理解: Thread.sleep(8000) 在sleep的进行时,对象引用不会变化,这就随时可以GC5.溢出泄露问题
栈溢出
-Xss 线程栈的大小,一般1-2M 有大小 栈溢出: 1.每个线程的栈无法再分配空间,栈中由一个一个栈帧组成,如果在方法中持续递归调用,就会持续生成栈帧,不释放,从而占满线程栈的大小 2.除了一直生产栈帧不释放,还有可能是某个栈帧的大小持续上涨,栈是固定的,但是栈帧不是,这种情况比较少见,栈帧中局部变量表, *** 作数栈可能占用的比较大,但是更少遇到一直生成局部变量的
内存溢出(OOM)
堆内存一直生成无法GC的对象撑爆了 年轻代放不下,老年代放不下,GC以后还是放不下就会OOM
溢出实例:
public class Test1 { public static void main(String[] args) throws InterruptedException { user a; user b; user c; user d; try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } a = new user(); System.out.println(a); b = new user(); // // System.out.println(b); // c = new user(); // d = new user(); while(true) { new user(); System.out.println(1); } } } class user{ private byte[] i = new byte[1024*1024*3]; user() { } } //-Xms10M -Xmx10M 堆内存设置位为10
eden: 2.4M survivor: 1M old: 7M 我们user的对象3M 1.在a = new user()时,年轻代分配不下,直接分配在old区,执行b = new user()时,依然分配不下,old大概占用6M 2.当我们new user()时,此时eden2.4M无法分配,老年代只有1M,也分配不下要进行FGC,由于a,b指向的user被main线程的局部变量a,b引用着,一定无法被回收,所以FGC并没有回收掉对象,此时年轻代和老年代都没有空间OOM
如果我们去掉b = new user(),那么老年代剩余空间大概为4M,所以执行到while(true)的new不会再OOM 但是此时对象都是分配在old区的,如果在分配就要溢出了,所以每两次分配就会进行FGC,而eden区一直没有分配对象,所以YGC和FGC的数量相等是因为每次FGC都会触发YGC
进一步 *** 作: 打印GC日志
-Xms10M -Xmx10M -Xloggc:d:/gc-adjust-%t.log -XX:metaspaceSize=256M -XX:MaxmetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M
可以看到,第一次while时,分配不下一次FGC,年轻代老年代都GC,年轻代的对象都清理掉了,但是老年代的有引用,无法清理,再次FGC,还是清理不掉OOM,综上,OOM在发生的过程中程序一直进行,并且多次尝试GC,想要为对象分配空间.
内存泄露
对象使用完,没有释放掉,内存泄露的最严重结果会是内存溢出
例: 一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)