对象的创建类加载检查:当虚拟机遇到一条新的指令时,首先会检查这条指令的参数是否可以在常量池中定位到该类的符号引用,并检查符号所代表的引用类是否已经加载、解析和初始化。如果没有,则必须先执行相应的类加载过程。分配内存:类加载检查通过后,虚拟机将为非新对象分配内存。对象需要的内存大小可以在类加载后确定,为对象分配空间的过程相当于从Java堆中划分出一定大小的内存。有两种分配方式:“指针冲突”和“空闲链表”。选择哪种分配方式取决于Java堆是否规则,而Java堆是否规则取决于垃圾收集器是否具有compacting功能。1.**指针碰撞:假设Java堆内存是有规律的,所有分配的内存在一侧,未分配的内存在另一侧,中间有一个指针指针将两者隔开,那么分配内存就是把指针指针移动和对象大小相等的例子,这种方法称为“指针碰撞”(**Serial,ParNew垃圾收集器,使用标记排序算法,系统采用的分配算法是指针碰撞,简单高效**)**1.**freelist:如果Java堆空间不连续且不规则,分配的内存和空闲内存混在一起,那么就需要维护一个freelist,记录哪些内存可用,找到足够大的空间分配内存时从链表中获取对象实例,并更新链表上的记录,这种方法称为“空闲链表”(**CMS垃圾收集器,“理论上”只能使用更复杂的空闲链表,为什么是理论上的?因为在CMS中为了更快的分配内存,设计了一个叫做LinearAllocationBuffer的分配缓冲区,通过freelist得到一个freelist,buffer在大块分配后,仍然可以使用指针碰撞来分配**)**1.内存分配方式:以上两种方式选择哪一种取决于Java内存是否规律,Java内存是否规律取决于GC收集器的算法是否规律是“**mark-clear**”或“**mark-sort**”,复制算法内存也是Regular**1。**内存分配并发问题:**在创建对象的时候,有一个很重要的问题就是线程安全,因为在实际的开发过程中,创建对象是一件很频繁的事情,作为虚拟机必须保证线程安全。一般来说,虚拟机使用两种方式来保证线程安全:CAS+失败重试:CAS是一种乐观锁的实现。所谓乐观锁就是每次不加锁,而是假设没有冲突就完成一个操作,如果因为冲突导致失败,就重试,直到成功。虚拟机使用失败重试的CAS来保证更新操作的原子性。TLAB:内存分配的动作是根据线程划分不同的空间,即每个线程在Java堆中预先分配一小块内存作为本地线程缓存(ThreadLocalAllocationBuffer),由JVM分配对象在线程中对于内存,先在TLAB中分配。当对象大于TLAB中的剩余内存或者TLAB中的内存耗尽时,再使用上面的CAS分配(可以使用-XX:+/-UseTLAB参数来启用或禁用TLAB)。3、初始化零值:内存分配完成后,虚拟机需要将所有分配的内存初始化为零值(不包括对象头)。这一步保证了对象的实例域在Java代码中可以在没有初值的情况下被初始化。可以使用,程序可以访问这些字段的数据类型对应的零值。4.设置对象头:零值初始化完成后,虚拟机需要对对象进行必要的设置,比如对象是哪个类的实例,如何找到类的元数据信息,hash对象的code,以及对象的GCGenerationalage等信息。此信息存储在对象标头中。另外,根据虚拟机当前的运行状态,比如是否开启偏向锁等,对象头也会有不同的设置方式。5.执行init方法:上述工作完成后,从虚拟机的角度来看,已经生成了一个新的对象,但是从Java程序的角度来看,对象的创建才刚刚开始,还没有执行init方法被执行了。所有字段也都为零值。所以一般来说,在执行完new指令后,会立即执行init方法,按照程序员的意愿对对象进行初始化,从而完全生成一个真正可用的对象。对象的内存布局在HotSpot虚拟机中,对象在内存中的布局可以分为三个区域:对象头、实例数据和对齐填充。内存分析widget:org.openjdk.joljol-core0.9对象头包括两部分信息。第一部分用于存放对象本身的运行时数据(官方称为MarkWord):在32位的HotSpot虚拟机中,比如对象在没有被同步锁锁定的状态下,MarkWord有25个bits用于存储对象的hashCode,4bits用于存储对象的世代年龄,2bits用于存储lockflag,1bits固定为0。其他状态下lockflagbit的状态如下:在64位HotSpot虚拟机中,MarkWord的存储方式如下:另一部分是类型指针,即对象指向其类元数据的指针,虚拟机通过这个指针来判断对象是哪个类是什么的实例?如果对象是Java数组,那么对象头中肯定有一段数据用来记录数组的长度,因为虚拟机可以通过Java对象的元数据来确定Java对象的大小。如果数组的长度不确定,则无法从元数据信息中推断出数组的大小。实例数据部分是对象实际存储的有效信息,也是程序中定义的各类字段的内容。无论是从父类继承还是在子类中定义,都要记录下来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStype参数)和Java源码中字段定义顺序的影响。HotSpot默认的分配顺序是longs/doules,ints,shorts/chars,bytes/booleans,oops(OrdinaryObjectsPoninters,OOPs)。相同大小的字段总是分配在一起存储。满足此条件时,类中定义的父变量出现在子类之前。如果使用-XX:CompactFields=true(默认为true),子类中较小的变量也可以插入到父类变量的空隙中,以节省一点空间。alignmentpadding部分不是自然存在的,也没有什么特殊意义,只是起到一个占位符的作用。因为Hotspot的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,换句话说,对象的大小必须是8字节的整数倍。对象头部分恰好是8字节的倍数(1倍或2倍),因此,当对象的实例数据部分不对齐时,需要通过对齐填充来完成。对象访问和定位创建对象是为了使用对象。我们的Java程序通过引用栈上的数据,对堆上的特定对象进行操作。对象的访问方式取决于虚拟机的实现。主流的访问方式有两种:句柄:如果使用句柄方式,那么虚拟机堆内存中的一块会被用作句柄池,Java虚拟机栈局部变量表中的引用存储着对象的句柄地址,句柄中包含对象实例数据和类型数据的具体地址信息;directpointer(usedbyHotSpot):如果使用直接指针访问,那么在Java堆对象布局中,需要考虑如何放置访问类型数据的相关信息,引用中存放的地址直接对象堆中的地址。这两种访问对象的方法各有优势。使用句柄访问的最大好处是引用存储了一个稳定的句柄地址。移动对象时,只改变句柄中的实例数据指针,不需要修改引用本身。使用直接指针访问方式最大的好处就是访问速度快,省去了一次指针定位的时间开销。重点:String类和常量池String创建对象的两种方式Stringstr1="abcd";Stringstr2=newString("abcd");System.out.println(str1==str2);//false这两种两种创作方式不同。第一种是直接从常量池中取“abcd”的地址;第二个是由new创建的,它将在堆中创建对象实例“abcd”。所以str1和str2中保存的地址分别是常量池和堆的地址,false。String类型的常量池比较特殊。使用方法:直接用双引号声明的String对象会直接存入常量池。如果不使用双引号声明的String对象,可以使用String提供的intern()方法。String.intern()是一种本地方法。它的作用是:如果常量池中已经包含了等于这个String对象内容的字符串,则返回该字符串在常量池中的引用;如果不存在,则在常量池中创建一个与此String内容相同的字符串,并返回在常量池中创建的字符串的引用。Strings1=newString("Computer");Strings2=s1.intern();//s2保存着字符串在常量池中的地址Strings3="Computer";System.out.println(s2);//ComputerSystem.out.println(s1==s2);//false,因为一个是堆内存中的String对象,另一个是常量池中的String对象,System.out.println(s3==s2);//true,因为都是常量池中的String对象Stringstr1="str";Stringstr2="ing";Stringstr3="str"+"ing";//常量中的对象String池str4=str1+str2;//堆上创建的新对象Stringstr5="string";//常量池中的对象System.out.println(str3==str4);//falseSystem.out.println(str3==str5);//trueSystem.out.println(str4==str5);//false尽量避免使用连接字符串,因为这会创建多个对象,如果需要更改字符串,可以使用Stringbuilder或Stringbuffer。Strings=newString("a")创建多个对象创建两个对象。验证:Strings1=newString("abc");//堆内存的地址值Strings2="abc";System.out.println(s1==s2);//输出false,因为one是a堆内存,一种是常量池的内存,所以两者是不一样的。System.out.println(s1.equals(s2));//输出true解释:首先将字符串“abc”放入常量池,然后将一个新的字符串“abc”放入Java堆(string常量“abc”在编译时已经确定放入常量池,Java堆上的“abc”在运行时初始化阶段确定),然后Java虚拟机栈的s1指向“abc”Java堆。在String#internJDK6中,intern方法会将第一个遇到的字符串实例复制到永久代的字符串常量池中存储,并返回永久代中的字符串引用。在JDK7中,intern方法的实现不需要将字符串的实例复制到永久代中。由于字符串常量池已经移到Java堆中,所以只需要在常量池中记录第一个实例的引用即可。以下代码在JDK6和7中执行结果不同System.out.println(str1.intern()==str1);Stringstr2=newStringBuilder("ja").append("va").toString();System.out.println(str2.intern()==str2);}}JDK6:falsefalsestr1是虚拟机栈的局部变量引用,str1.intern()持有永久代中“计算机软件”的引用。str2持有的是虚拟机栈的局部变量引用,JDK7:truefalsestr8基本数据类型和常量池的包装类Java基本数据类型的包装类大部分都实现了常量池技术,即Byte、Short,长整型,字符型,布尔型;这五个包装类默认会创建相应类型的缓存数据,值为[-128,127],但超出此范围仍会创建新对象。两种浮点数的封装类Float和Double都没有实现常量池技术。Integeri1=33;Integeri2=33;System.out.println(i1==i2);//输出trueIntegeri11=333;Integeri22=333;System.out.println(i11==i22);//输出假双i3=1.2;双i4=1.2;System.out.println(i3==i4);//输出falseInteger缓存源码:/***该方法会一直缓存-128到127(含)范围内的值,可能会缓存其他值在此范围之外。*/publicstaticIntegervalueOf(inti){if(i>=IntegerCache.low&&i<=IntegerCache.high)returnIntegerCache.cache[i+(-IntegerCache.low)];returnnewInteger(i);}应用场景:1.Integeri1=40;Java在编译时会直接将代码封装为Integeri1=Integer.valueOf(40),从而使用常量池中的对象2.Integeri1=newInteger(40);在这种情况下,将创建一个新对象;整数i1=40;整数i2=新整数(40);System.out.println(i1==i2);//更丰富的输出falseInteger的例子:Integeri1=40;Integeri2=40;Integeri3=0;Integeri4=newInteger(40);Integeri5=newInteger(40);Integeri6=newInteger(0);System.out.println("i1=i2"+(i1==i2));System.out.println("i1=i2+i3"+(i1==i2+i3));System.out.println("i1=i4"+(i1==i4));System.out.println("i4=i5"+(i4==i5));System.out.println("i4=i5+i6"+(i4==i5+i6));系统输出。println("40=i5+i6"+(40==i5+i6));结果:i1=i2truei1=i2+i3truei1=i4falsei4=i5falsei4=i5+i6true40=i5+i6true解释:语句i4==i5+i6,因为+this此运算符不适用于Integer对象。首先i5和i6进行自动拆箱操作并加值,即i4==40然后Integer对象不能直接和值进行比较,所以i4自动拆箱并转换为40的int值。最终这条语句变成40==40用于数值比较。本文由博客多发平台OpenWrite发布!