Java对象在内存中实例化的过程
Java对象在内存中实例化的过程
在讲 Java 对象在内存中的实例化过程前,先来说下在类的实例化过程中,内存会使⽤到的三个区域:栈区、堆区、⽅法区。
堆区:
存储的全部都是对象,每个对象包含了⼀个与之对应的 class 类的信息。
jvm 只有⼀个堆区(steap),它会被所有线程共享,堆中不存放基本数据类型和对象引⽤,它只存放对象本⾝。
栈区:
每个线程都包含⼀个栈区,栈中只保存基本数据类型的值和对象以及基础数据的引⽤。
每个栈中的数据(基本数据类型和对象的引⽤)都是私有的,其它栈是⽆法进⾏访问的。
栈分为三个部分:基本类型变量区、执⾏环境上下⽂、操作指令区(存放操作指令)。
⽅法区:
⼜被称为静态区,它跟堆⼀样,被所有的线程共享,⽅法区包含所有的 class 信息 和 static修饰的变量。
⽅法区中包含的都是整个程序中永远唯⼀的元素,如:class、static变量。
简谈 JDK8 前后 JVM 内存变化
JDK8 之前对内存划分为:新⽣代(YOUNG)—⽼年代(Tenured)—永久代(PermGen)
新⽣代:
新⽣代⼜分为伊甸区(Eden)存活区(Survivor),其中存活区⼜分为两个⼤⼩空间⼀样的s0、s1,⽽且s0 和 s1 可以互相转化,存活区保存的⼀定是在伊甸区保存了很久的,并且经过好⼏次⼩的GC还存活下来的对象,存活区⼀定会有两块⼤⼩相等的空间。⽬的是⼀块存活区未来的晋升,另⼀块存活区是为了对象的回收。需要注意的是:这两块存活区⼀定有⼀块是空的。
新⽣代中的 GC:
新⽣代⼤⼩(PSYoungGen total 9216K)=eden⼤⼩(eden space 8192K)+1个survivor⼤⼩(from space 1024K)
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认⽐例为8(Eden):1(⼀个survivor),⼀般情况下,新创建的对象都会被分配到Eden区(⼀些⼤对象特殊处理),这些对象经过第⼀次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过⼀次Minor GC,年龄就会增加1岁,当它的年龄增加到⼀定程度时,就会被移动到⽼年代中。
  因为年轻代中的对象基本都是朝⽣⼣死的(80%以上),所以在年轻代的垃圾回收算法使⽤的是复制算法,复制算法的基本思想就是将内存分为两块,每次只⽤其中⼀块,当这⼀块内存⽤完,就将还活着的对象复制到另外⼀块上⾯。复制算法不会产⽣内存碎⽚。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进⾏GC,Eden区中所有存活的对象都会被复制到“To”,⽽在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到⼀定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年⽼代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden 区和From区已经被清空。这个时候,“From”和“To”会交换他们的⾓⾊,也就是新的“To”就是上次GC前的“From”,新
的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会⼀直重复这样的过程,直
到“To”区被填满,“To”区被填满之后,会将所有对象移动到⽼年代中。
为什么要设置两个Survivor区?
设置两个Survivor区最⼤的好处就是解决了碎⽚化;
假设现在只有⼀个survivor区,我们来模拟⼀下流程:
刚刚新建的对象在Eden中,⼀旦Eden满了,触发⼀次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下⼀次Eden满了的时候,问题来了,此时进⾏Minor GC,Eden和Survivor各有⼀些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎⽚化。
碎⽚化带来的风险是极⼤的,严重影响Java程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有⾜够⼤的连续内存空间,接下去如果程序需要给⼀个内存需求很⼤的对象分配内存。。。画⾯太美不敢看。。。这就好⽐我们上学时背包⾥所有东西紧挨着放,最后就可能省出⼀块完整的空间放饭盒。如果每件东西之间隔⼀点空隙乱放,很可能最后就要⼿提⼀路了。
1.1、Java 中的数据类型
Java 中的数据类型有两种:
1、基本类型(primitive types): 共有8种,即:int、short、long、byte、char、float、double、boolean(注意并没有 String 的基本类型),这8中类型的定义是通过诸如:int a = 5;long b = 22L;的形式来定义的,称为⾃动变量。注意:⾃动变量存的是字⾯值,不是类的实例,即不是类的引⽤,这⾥并没有类的存在;
如:int a = 5; 这⾥的 a 是⼀个指向 int 类型的引⽤,指向 5 这个字⾯值,这些字⾯值的数据由于⼤⼩可知,⽣存期可知( 这些字⾯值固定定义在某个程序块⾥⾯,程序块退出后,字段值就消失了 ),出于追求速度的原因,这些字⾯值就存在于栈区中;
另外,栈有⼀个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义
   int a = 3;
  int b = 3;
  编译器先处理int a = 3;⾸先它会在栈中创建⼀个变量为a的引⽤,然后查有没有字⾯值为3的地址,
没到,就开辟⼀个存放3这个字⾯值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引⽤变量后,由于在栈中已经有3这个字⾯值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
特别注意的是:这种字⾯值的引⽤与类对象的引⽤不同。假定两个类对象的引⽤同时指向⼀个对象,如果⼀个对象引⽤变量修改了这个对象的内部状态,那么另⼀个对象引⽤变量也即刻反映出这个变化。相反,通过字⾯值的引⽤来修改其值,不会导致另⼀个指向此字⾯值的引⽤的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字⾯值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
2、 包装类数据:如:String、Integer、Double等将相应的基本数据类型包装起来的类,这些数据全部存放在 堆 中, Java ⽤ new()语句来显⽰地告诉编译器,在运⾏时才根据需要动态创建,因此⽐较灵活,但缺点是要占⽤更多的时间。
1.2、类实例化时内存中发⽣的变化
⾸先我们先对下⾯的代码进⾏分析:
public class People{
String name;// 定义⼀个成员变量 name
实例化类和实例化对象int age;// 成员变量 age
Double height;// 成员变量 height
void sing(){
System.out.println("⼈的姓名:"+name);
System.out.println("⼈的年龄:"+age);
System.out.println("⼈的⾝⾼:"+height);
}
public static void main(String[] args){
String name;// 定义⼀个局部变量 name
int age;// 局部变量 age
Double height;// 局部变量 height
People people =new People();//实例化对象people
people.name ="张三";//赋值
people.age =18;//赋值
people.stuid =180.0;//赋值
people.sing();//调⽤⽅法sing
}
}
代码解析:
这段代码⾸先定义三个成员变量:String name、int age、Double height 这三个变量都是只声明了没有初始化,然后定义了⼀个成员⽅法 sing();
在 main()⽅法⾥同样定义了三个⼀样的变量,只不过这些是局部变量;
在main() 函数⾥实例化对象 people , 内存中在堆区内会给实例化对象 people 分配⼀⽚地址,紧接着我们对实例化对象 people 进⾏了赋值。people 调⽤成员⽅法 sing() 。mian()函数打印输⼊⼈的姓名,⼈的年龄和⼈的⾝⾼,系统执⾏完毕。
下⾯通过图解法展⽰实例化对象的过程中内存的变化:
在程序的执⾏过程中,⾸先类中的成员变量和⽅法体会进⼊到⽅法区,如图:
程序执⾏到 main() ⽅法时,main()函数⽅法体会进⼊栈区,这⼀过程叫做进栈(压栈),定义了⼀个⽤于指向 Person 实例的变量person。如图:
程序执⾏到 Person person = new Person(); 就会在堆内存开辟⼀块内存区间,⽤于存放 Person 实例对象,然后将成员变量和成员⽅法放在 new 实例中都是取成员变量&成员⽅法的地址值 如图:
接下来对 person 对象进⾏赋值, person.name = “⼩⼆” ; perison.age = 13; person.height= 180.0;
先在栈区到 person,然后根据地址值到 new Person() 进⾏赋值操作。
如图:
当程序⾛到 sing() ⽅法时,先到栈区到 person这个引⽤变量,然后根据该地址值在堆内存中到 new Person() 进⾏⽅法调⽤。在⽅法体void speak()被调⽤完成后,就会⽴刻马上从栈内弹出(出站 )
最后,在main()函数完成后,main()函数也会出栈 如图:
以上就是Java对象在内存中实例化的全过程。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。