@java
Java是面向对象的语言,所谓“万事万物皆对象”就是Java是基于对象来设计程序的,没有对象程序就没法运行(8大基本类型除外),那么对象是如何建立的?在内存中又是怎么分配的呢?算法
在Java中咱们有几种方式能够建立一个新的对象呢?总共有如下几种方式:数组
为了便于说明和理解,下文仅针对new出来的对象进行讨论。缓存
Java中对象的建立过程就包含上图中的5个步骤,首先须要验证待建立对象的类是否已经被JVM记载,若是没有则会先进行类的加载,若是已经加载则会在堆中(不彻底是堆,后文会讲到)分配内存;分配完内存后则是对对象的成员变量设置初始值(0或null),这样对象在堆中就建立好了。可是,这个对象是属于哪一个类的还不知道,由于类信息存在于方法区,因此还须要设置对象的头部(固然头部中也不只仅只有类型指针信息,稍后也会详细讲到),这样堆中才建立好了一个完整的对象,可是这个对象的成员变量还都是初始值,因此最后会调用init方法按照咱们本身的意愿初始化对象,一个真正的对象就建立好了。
对象的整个建立过程是很是简单的,可是其中还有不少细节,好比对象会在哪里建立?分配内存有哪些方式?怎么保证线程安全?对象头中有哪些信息?下面一一讲解。安全
基本上全部的对象都是在堆中,但并不是绝对,在JDK1.6版本引入了逃逸分析技术。逃逸分析就是指针对对象的做用域进行断定,当一个对象在方法中被定义后,若是被其它方法或其它线程访问到,就称为方法逃逸或线程逃逸。
该技术针对未逃逸的对象作了一个优化:栈上分配(除此以外还有同步消除、标量替换,这里暂时不讲)。这个优化是指当一个对象能被肯定不会在该方法以外被引用,那么就能够直接在虚拟机栈中建立该对象,那么这个对象就能够随着线程的消亡而销毁,再也不须要垃圾回收器进行回收。这个优化带来的收益是明显的,由于有至关一部分对象都只会在该方法内部被引用。逃逸分析默认是开启的,能够经过-XX:-DoEscapeAnalysis参数关闭。下面看一个实例:并发
public class EscapeAnalysisTest { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); for (int i = 0; i < 50000000; i++) {//5000万次---5000万个对象 allocate(); } System.out.println((System.currentTimeMillis() - start) + " ms"); Thread.sleep(600000); } static void allocate() {//逃逸分析(不会逃逸出方法) //这个myObject引用没有出去,也没有其余方法使用 MyObject myObject = new MyObject(2020, 2020.6); } static class MyObject { int a; double b; MyObject(int a, double b) { this.a = a; this.b = b; } } }
加上-XX:+PrintGC参数运行上面的方法,会看到控制台只是打印了执行时间5ms,可是若再加上-XX:-DoEscapeAnalysis关闭逃逸分析就会出现下面的结果:框架
[GC (Allocation Failure) 66384K->848K(251392K), 0.0012528 secs] [GC (Allocation Failure) 66384K->816K(251392K), 0.0010461 secs] [GC (Allocation Failure) 66352K->848K(316928K), 0.0009666 secs] [GC (Allocation Failure) 131920K->784K(316928K), 0.0018284 secs] [GC (Allocation Failure) 131856K->816K(438272K), 0.0009315 secs] [GC (Allocation Failure) 262960K->684K(438272K), 0.0022738 secs] [GC (Allocation Failure) 262828K->684K(700928K), 0.0005052 secs] 308 ms
执行时间大大提高,主要是用在了GC回收上。布局
在HotSpot虚拟机中,对象在内存中分为三块:对象头、实例数据和对齐填充。以下图:
性能
对象的内存布局上面这张图写的很清楚了,其中自身运行时数据了解一下有哪些信息便可,类型指针则是指向对象所属的类,若是对象是数组,则对象头中还会包含数组的长度信息;实例数据就是指对象的字段信息;最后对齐填充则不是必须的,由于为了方便处理和计算,HotSpot要求对象的大小必须是8字节的整数倍,所以当不满8字节的整数倍时,就须要对齐填充来补全。学习
当对象建立完成后就存在于堆中,那么栈中怎么定位并引用到该对象呢?虚拟机规范中自己并无定义这一部分该如何实现,具体的实现取决于各个虚拟机厂商,而目前主流的定位方式有两种:句柄和直接指针。
以上两种方式在各个语言和框架都有使用,而本文所讨论的HotSpot虚拟机使用的是直接指针方式,由于对象的访问是很是频繁的,这时效率就显得格外重要。
JVM不须要咱们手动释放内存,这是Java广受欢迎的缘由之一,那么它是如何作到自动管理内存,回收不须要的对象的呢?既然要回收对象,那么就须要判断哪些对象是能够被回收的,即对象的死活断定,哪些对象不会再被引用?有两种实现方式:引用计数法和可达性分析。
以上4种很是好理解,是重点,须要熟记于心,由于上面4种对象是在方法运行时或常量引用的对象,在对应的生命周期是确定不能被GC回收的,做为GC Roots天然再合适不过。另外还有下面几种能够做为了解:
除了堆中对象须要回收,方法区中的class对象也是能够被回收的,可是回收的条件很是苛刻:
能够看到方法区的回收条件是多么苛刻,因此方法区的回收率通常极低,所以能够经过-Xnoclassgc关闭方法区的回收,提高GC效率,但须要注意,关闭后将会致使方法区的内存永久被占用,致使OOM出现。
经过上文咱们能够发现,对象的存活断定都是基于引用,而Java中引用又分为了4种:
虚拟机提供了一次自我拯救的机会给对象,即finalize方法。若是对象覆盖了该方法,当通过可达性分析后,就会进行一次判断,判断该对象是否有必要执行finalize方法,若是对象没有覆盖该方法或者已经执行过一次该方法都会断定为该对象没有必要执行finalize方法,在GC时被回收。不然就会将该对象放入到一个叫F-Queue的队列中,以后GC会对该队列的对象进行二次标记,即调用该方法,若是咱们要让该对象复活,那么就只须要在finalize方法中将该对象从新与GC Roots关联上便可。
该方法是虚拟机提供给对象复活的惟一机会,可是该方法做用极小,由于使用不慎可能会致使系统崩溃,另外因为它的运行优先级也很是低,经常须要主线程等待它的执行,致使系统性能大大下降,因此基本上能够忘记该方法的存在了。
上文说到对象是在堆中分配内存的,可是堆中也是分为新生代和老年代的,新生代中又分了Eden、from、survivor区,那么对象具体会分配到哪一个区呢?这涉及到对象的分配规则,下面一一说明。
大多数状况,对象直接在Eden区中分配内存,当Eden区内存不足时,就会进行一次MinorGC(新生代垃圾回收,能够经过-XX:+PrintGCDetails这个参数打印GC日志信息)。
什么是大对象?虚拟机提供了一个参数:-XX:PretenureSizeThreshold,当对象大小大于该值时,该对象就会直接被分配到老年代中(该参数只对Serial和ParNew垃圾收集器有效)。为何不分配到新生代中呢?由于在新生代中每一次MinroGC都会致使对象在Eden、from和sruvivor中复制,若是存在不少这样的大对象,那么新生代的GC和复制效率就会极低(关于垃GC的内容后面的文章会详细讲解)。
既然对象优先在新生代中分配,那么何时会进入到老年代呢?这就和上文讲解的对象头中的分代年龄有关了,默认状况下超过15岁就会进入老年代,能够经过-XX:MaxTenuringThreshold参数进行设置。那岁数又是怎么增加的呢?每当对象熬过一次MiniorGC后年龄都会增长1岁。
可是虚拟机并非要求对象年龄必须达到MaxTenuringThreshold才能晋升老年代,当Survivor空间中相同年龄的全部对象的大小总和大于Survivor空间一半时,年龄大于或等于该年龄的对象就会直接晋升到老年代。
在发生MiniorGC以前,虚拟机首先会检查老年代中最大可用的连续空间是否大于新生代全部对象的总和,若是大于则进行一次MiniorGC;不然,则会检查HandlePromotionFailure设置值是否容许担保失败。若是容许则会检查老年代最大连续空间是否大于历次晋升到老年代对象的平均大小,若是大于则进行一次MiniorGC,不然则进行一次FullGC。
为何要这么设计呢?由于频繁的FullGC会致使性能大大下降,而取历次晋升老年代对象的平均大小确定也不是百分百有效,由于存在对象忽然大大增长的状况,这个时候就会出现担保失败的状况,也会致使FullGC。须要注意的是HandlePromotionFailure这个参数在JDK6Update24后就不会再影响到虚拟机的空间分配担保策略了,即默认老年代的连续空间大于新生代对象的总大小或历次晋升的平均大小就会进行MinorGC,不然进行FullGC。
本文概念性的东西很是多,这是学习JVM的难点和基础,但这是绕不开的一道坎,读者只有多看,多思考,写代码复现文中提到的概念,才能真正的理解这些基础知识。另外还有垃圾是怎么回收的?有哪些垃圾回收器?怎么选择?这些问题将在下一篇进行解答。