什么是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位平台下内存的消耗,启用指针压缩功能
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为好
关于对齐填充:对于大部分处理器,对象以8字节整数倍来对齐填充都是最高效的存取方式。
对象逃逸分析
对象主要是存储在堆内存中,堆内存也是所有线程共享的变量池,堆内存的资源是很宝贵的;同时虚拟机进行垃圾回收(YoungGC和FullGC)都会先计算整理,找出哪些对象能被回收的,这是一个耗时耗服务器计算资源的过程,如果一个对象能确定不会发生逃逸行为,那么虚拟机就可以将该对象分配到方法栈内存上面,这个对象就会随着方法栈帧出栈的时候一起销毁,不需要垃圾回收器去计算和回收了。在很多方法里面,这类没有发生逃逸的对象还是有很多的,如果这些对象在栈内存分配能充分利用起来,将会大大缓解垃圾收集器的压力。虚拟机在将字节码转换成机器码的时候,会对这个对象是否发生逃逸进行分析的过程称之为逃逸分析
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一个程序会发现大对象直接进了老年代
为什么要这样呢?
为了避免为大对象分配内存时的复制操作而降低效率。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。
老年代空间分配担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”
对象内存回收
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题
可达性分析算法
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
常见引用类型
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
finalize()方法最终判定对象是否存活
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1. 第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,对象将直接被回收。
2. 第二次标记
如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
finalize()方法的运行代价高昂, 不确定性大, 无法保证各个对象的调用顺序, 如今已被官方明确声明为不推荐使用的语法。 有些资料描述它适合做“关闭外部资源”之类的清理性工作, 这完全是对finalize()方法用途的一种自我安慰。 finalize()能做的所有工作, 使用try-finally或者其他方式都可以做得更好、更及时, 所以建议大家完全可以忘掉Java语言里面的这个方法。
如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类呢?
类需要同时满足下面3个条件才能算是 “无用的类” :
- 该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
--------------------------------------------------------------------------
对象创建方式
使用new关键字 调用了构造函数
使用Class的newInstance方法 调用了构造函数
使用Constructor类的newInstance方法 调用了构造函数
使用clone方法 没有调用构造函数
使用反序列化 没有调用构造函数
对象创建过程
虚拟机遇到一条new指令时,先检查 常量池 是否 已经 加载相应的类,如果没有,必须先执行相应的类加载,也就是 检查 这个new指令的参数 是否能 在常量池中 定位到一个类的 符号引用,并且检查 这个符号引用代表的类 是否已经被加载、解析、初始化过
类加载通过后,接下来分配内存,对象所需的内存大小 在 类加载完成后 便可以确定。为对象分配空间 = 把一块确定大小的内存 从 java堆中划分出来
内存分配完成后,jvm要将 分配到的内存空间 都初始化为零值。这一步操作保证了 对象的实例字段 在java代码中 可以不赋初始值就能直接访问,程序能访问到 这些字段的 数据类型 所对应的零值
接下来jvm要对 对象 进行必要的设置,例如,这个对象 是哪个类的实例、如何能找到类的元数据信息、对象的hash码、对象的GC分代年龄等信息。这些信息 存放在 对象的对象头中
执行new指令之后会接着执行init方法,把对象 按照程序员的意愿进行初始化
为对象分配内存
指针碰撞
如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边
分配内存时 将 位于中间的指针指示器 向空闲的内存移动一段 与 对象大小相等的距离,这样便完成分配内存工作
空间列表
如果Java堆的内存不是规整的,则需要 由虚拟机 维护一个列表 来记录 那些内存是可用的,这样在分配的时候 可以从列表中 查询到 足够大的内存分配给对象,并在分配后更新列表记录
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整 又由 所采用的 垃圾收集器 是否 带有压缩整理功能决定
处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,有可能出现正在给对象A分配内存,指针还没来及修改,对象B又同时使用了原来的指针来分配内存的情况
两种解决方案
一种是对分配动作做同步处理-CAS,采用 CAS + 失败重试 来保障 更新操作的原子性
一种是把分配动作按照线程 划分到不同的空间之中执行,即每个线程 在java队中 都预先分配一小块内存,称为 本地线程分配缓冲(Thread Local Allocation Buffer TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定
TLAB
TLAB内存空间位于Eden区。默认TLAB大小为占用Eden Space的1%
-XX:+UseTLAB,默认是开启的
-XX:+TLABSize,自定义调整TLAB大小
-XX:+PrintTLAB,打印TLAB信息
-XX:TLABRefillWasteFraction,设置维护 进入TLAB空间 单个对象大小,比例值,默认1/64,对象大于该值会去堆创建
-XX:TLABWasteTargetPercent,设置TLAB空间所占用Eden空间的百分比大小
-XX:-ResizeTLAB,自动调整TLABRefillWasteFraction阈值
对象的内存布局
对象 在内存中的 存储布局分为三块区域:
对象头header
实例数据instance data,对象真正存储的有效信息,这部分的存储顺序 会受到 虚拟机分配策略参数 和 字段在java中定义顺序 的影响。HotSpot分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops。父类中定义的变量会出现在子类之前
对齐填充 padding,不是必然存在的,仅仅起占位符的作用,是因为jvm对 对象的大小 必须是8字节的倍数
对象头包括两部分信息:
存储 对象自身的运行时 的数据,hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,被称为mard word(这里有可能会被引申到锁的相关问题)
类型指针,即 对象 指向他(自己)的 类(的)元数据 的指针,jvm通过这个指针 来确定 这个对象 是哪个类的实例
如果对象是一个数组,header中还必须有一块用于记录 数组长度的 数据。因为jvm无法通过数组的 元数据中 确定数组的大小
对象访问定位
Java程序需要通过 JVM 栈上的引用 访问 堆中的具体对象
对象的访问方式取决于 JVM 虚拟机的实现
目前主流的访问方式有 句柄 和 直接指针 两种方式
句柄访问方式好处:reference中存储的是 句柄地址,在对象 被移动时,只改变 句柄中的 实例数据指针, reference本身不需要修改
指针访问方式好处:速度快,节省了一次指针定位的时间开销