JAVA的初始化的坑以及内存分配

Java程序猿学习笔记:变量初始化、内存分配与类加载顺序

最近在看JAVA程序猿16课,看到了变量的初始化和内存分配,然后觉得挺多坑的,就总结了下来。(顺便,书真的挺好的)

PART 1:首先,看一个例子:

package test;

public class TestInit
{
   public static void main(String[] args)
   {
       new Dervied();
   }
}

class Base
{

   static
   {
       System.out.println("Base static block");
   }

   public static int a = 10;
   
   {
       System.out.println("base block");
   }

   public Base()
   {
       // TODO Auto-generated constructor stub
       System.out.println("base constructor");
   }

}

class Dervied extends Base
{
   public static int b = 10;
   
   static
   {
       System.out.println("de static block");
   }

   {
       System.out.println("de block");
   }

   public Dervied()
   {
       // TODO Auto-generated constructor stub
       System.out.println("de constructor");
   }

}
//输出的结果是
Base static block
de static block
base block
base constructor
de block
de constructor

使用,javap -c *.class工具查看编译结果,字节码,可以看到运行的顺序。


在这个例子当中,我们把static变量和static block的位置兑换了一下,他的字节码



看到Field的编译提前了,类似的非static变量和非static的Field调换位置,也可以发现他们的编译顺序是调换了的。所以得出的结论是:

(父类static变量,父类static block)-->(子类static 变量,子类static block)-->(父类的非static block,非static Field)-->父类构造器-->(子类非static block,子类非static FIeld)-->子类构造器,这是初始化的顺序,在没有添加final关键字和其他关键字,只有static和权限修饰符之下的情况。

其中,括号内的表示需要看他们的顺序,比如父类的static变量和static block,谁在代码顺序的前面编译时候就先执行。

//注,javap <optioins> <classes>的使用

1,-c:分解方法代码

2,-l指定显示的行号和局部变量的列表                                                3,-public|protected|package|private:用于指定显示那种级别的类成员,分别对应JAVA的四种访问权限

4,-verbose:指定显示更进一步的详细信息


PART 2:

先看例子:


package test;

public class PriceTest
{
	public static void main(String[] args)
	{
		System.out.println(Prince.INISTANCE.currentPrice);
		Prince p = new Prince(2.8);
		System.out.println(p.currentPrice);
	}
}

class Prince
{
	static Prince INISTANCE = new Prince(2.8);
	static double initPrice = 20;//加上final之后,编译会提前
	double currentPrice;

	public Prince(double discount)
	{
		// TODO Auto-generated constructor stub
		this.currentPrice = initPrice - discount;
	}
}
//结果是
-2.8
17.2

为什么不都是17.2呢?首先,根据前面的结论,我们知道系统先回编译INISTANCE 这个变量,他是static的,然后,他回去先执行new Prince(2.8),去掉用构造方法,但是,这个时候,我们的initPrice的值是0.0(默认值,不是初始值),所以是-2.8,然后第二次的时候,系统调用了new一个对象之前,我们的static变量和static block以及执行完(这里没有block)


PART 3:

package test1;

public class Test
{
	public static void main(String[] args)
	{
		new Derived();
	}

}

class Base
{
	public int i = 2;

	public Base()
	{
		System.out.println(this.i);
		this.display();
		System.out.println(this.getClass());
	}

	public void display()
	{
		System.out.println(i);
	}
}

class Derived extends Base
{
	public int i = 22;

	public Derived()
	{
		i = 222;
	}

	public void display()
	{
		System.out.println(i);
	}
}
//结果
2
0
class test1.Derived

为什么this.i = 2,而输出的display()当中的i是0。首先,我们知道,JAVA为每一个成员变量(不管是不是静态的)都会有一个默认值(没有初始化),就是声明这个变量的时候,比如int的默认值是0,就是int a,a作为成员变量,他的默认值就是0,初始化是指为a赋值。

然后是this关键字:代表的是正在初始化的对象或者是传入的对象,在这里,我们看到this.getClass()输出的是Derived类不是Base,所以这里的this是Derived对象而不是Base对象,因为我们再Test的main函数里面new Derived()。然后Derived是继承了Base,调用了Base的默认构造器,到了Base构造器里面,this就是指Derived对象,但是,他是Base的引用指向了Derived对象。当编译类型和运行类型不同的时候,通过变量访问他的引用的对象的实例变量时,该实例变量由声明该变量的类型决定,但是通过该变量调用它引用的对象的实例方法的时候,该方法将有他的实际所引用的对象决定。这里的引用是this,属于Base类型,它指向的对象是Derived对像。所以他调用成员变量就是调用Base的成员变量而不是Derived的。然后它调用方法,假如该方法子类存在而且是父类可以访问到的(就是override的),就是调用子类方法,否则是调用父类的对应方法。这里的结果就是这样,this.i是Base的i,然后this.display()或者是diaplay()就是Derived的方法(Derived存在该方法,override了,不存在的时候调用的是父类的),但是这时候子类的i还没有初始化,所以是0。


PART4:继承方法和继承变量的不同:

package test;

public class Test
{
	public static void main(String[] args)
	{
		Base1 b = new Base1();
		System.out.println(b.count);
		b.display();
		Deriver d = new Deriver();
		System.out.println(d.count);
		d.display();
		Base1 db = new Deriver();
		System.out.println(db.count);
		db.display();
		Base1 d2b = d;
		System.out.println(d2b.count);
	}
}

class Base1
{
	int count = 2;
	public void display()
	{
		System.out.println(this.count);
	}
}

class Deriver extends Base1
{
	int count = 20;
	@Override
	public void display()
	{
		System.out.println(this.count);
	}
}
//结果
2
2
20
20
2
20
2

当使用父类引用指向子类对象的时候,父类引用调用的成员变量时父类引用的成员变量,父类引用调用成员方法的时候,假如是子类有override的就是调用子类的方法,否则是父类的方法。在子类的对象(也就是堆当中),除了存在子类的对象的变量的值之外,还有一块内存用于给父类的变量。


PART 5:final修饰符

一句话:final修饰的变量在声明时候初始化,那么他的作用就相当于一个宏变量。他的值是在编译时期就已经决定下来。在运行的时候,遇到改变了就会进行替换。

在PART 2的initPrice 变量中,加上final关键字,结果会不一样,都是17.2.

package test;

public class FinalTest
{
	public static void main(String[] args)
	{
		final String book = "疯狂Java讲义:" + 99.0;// 宏变量
		final String str = "疯狂Java讲义:99.0";
		final String book2 = "疯狂Java讲义:" + String.valueOf(99.0);// 无法确定值。不能再编译期间确定,不会被当做宏变量,因为调用了String的方法,具有不缺定性
		System.out.println(book == "疯狂Java讲义:99.0");
		System.out.println(book2 == "疯狂Java讲义:99.0");
		System.out.println(str == book);
		System.out.println(str == book2);
	}
}
//结果:
true
false
true
false
我们知道String字符串都是的存储是在常量池当中,首先是当常亮池没有这个字符串,我们天津进去,当有这个字符串的时候,我们就不会在新建一个字符串放在常量池当中。直接比如String a="A";String b = "A";。开始A不存在常量池,存放进去,a指向A,然后b的值"A"以在常量池存在,直接b指向A,这时候a == b的值是true。在这个demo也是类似的。

package test;

public class FinalInstanceVaribaleTest
{

	public static void main(String[] args)
	{
		String s1 = "ABC";
		String s2 = "AB" + "C";
		System.out.println(s1 == s2);
		String s3 = "DEF";
		String s4 = s1 + s3;
		String s5 = "ABCDEF";
		System.out.println(s4 == s5);

	}

}
//结果
true
false
把s1和s3的修饰符改为final之后,是
true
true
原因:s1和数是变量,s4不能再编译时期确定值,s5在编译时期确定值,然后使用了final之后s1和s3是宏变量,确定了s4,所以是true。



<think> 嗯,用户深入询问了Java初始化时变量和常量的内存分配机制,这需要结合Java虚拟机的内模型来详细解释。从对话历史看,用户已经掌握了基础概念,现在需要更底层的实现细节。 用户提供的四篇参考文献中,[1]和[4]详细说明了变量类型及其生命周期,[2]强调变量声明规范,[3]则给出具体示例。特别是[1]提到静态初始化块优先执行这个特性很重要,这关系到类加载阶段的准备和初始化过程。 在内存分配方面,需要重点区分三种场景:首先是类加载时的处理,static修饰的变量会进入方法区;其次是对象实例化时,堆内会为实例变量分配空间;最后是方法执行时,局部变量在栈帧中创建。对于常量,final关键字的处理要区分编译期常量折叠和运行时常量池引用。 注意到用户可能是Java中级开发者,正在研究性能优化或内泄漏问题。回答时需要补充两个关键细节:一是字符串常量在堆外空间的放位置(字符串常量池),二是基本类型自动装箱产生的隐藏内开销。这些在实际开发中容易引发问题。 考虑到用户上次询问过C++的内管理,可以适当对比Java的自动内回收机制,但主要焦点还是放在Java特性上。最后生成的问题要引导用户思考实践中的应用场景,比如内泄漏分析和常量池优化。 </think> 在Java初始化过程中,变量和常量的内存分配机制如下(基于JVM内模型): ### 一、内存分配机制 1. **静态变量(类变量)** - **分配时机**:类加载阶段(`<clinit>()`方法执行时) - **内区域**:方法区(JDK 8+的元空间) - **特点**: ```java static int classVar; // 默认初始化为0 static final int CONSTANT = 100; // 编译期常量折叠 ``` 2. **实例变量(成员变量)** - **分配时机**:对象实例化时(`<init>()`方法执行) - **内区域**:堆内(每个对象独立储) - **初始化流程**: ```mermaid graph LR A[对象创建] --> B[分配堆内] B --> C[默认初始化-零值] C --> D[显式初始化/构造器赋值] ``` 3. **常量(final修饰)** - **编译期常量**(如`static final int X=10`): - 编译时直接替换为字面值(常量折叠) - 不占用运行时内[^1][^4] - **运行时常量**(如`final Object obj`): - 分配在堆内(实例常量)或方法区(静态常量) - 赋值后引用不可变(但对象内容可能可变) 4. **局部变量** - **分配时机**:方法调用时 - **内区域**:栈帧中的局部变量表 - **特点**: ```java void method() { int localVar = 20; // 栈内存分配 } ``` ### 二、初始化顺序(关键步骤) 1. 父类静态变量/静态块(首次加载类时) 2. 子类静态变量/静态块 3. 父类实例变量/非静态块 → 父类构造器 4. 子类实例变量/非静态块 → 子类构造器[^1] > **示例场景**: > ```java > class Parent { > static { System.out.println("父类静态块"); } // 步骤1 > { System.out.println("父类实例块"); } // 步骤3 > } > class Child extends Parent { > static final int CONST = 10; // 步骤2 (编译期优化) > { System.out.println("子类实例块"); } // 步骤4 > } > ``` ### 三、常量特殊处理 - **字符串常量**: 储在堆内的**字符串常量池**(String Table) ```java String s1 = "abc"; // 常量池引用 String s2 = new String("abc"); // 堆中新对象 ``` - **基本类型常量**: `final int X=10`编译后等价于直接使用字面值`10` - **空final变量**: `final int x;`必须在构造器结束前初始化[^2] ### 四、性能影响 | 类型 | 内开销 | 访问速度 | |--------------|---------------|----------| | 静态常量 | 无(编译优化) | 最快 | | 实例常量 | 堆内/对象 | 中等 | | 非final变量 | 堆/栈 | 可能较慢 | > 💡 **最佳实践**:优先使用`static final`组合,既能获得常量优化,又避免重复内存分配[^4]。 --- **内存分配验证代码**: ```java public class MemoryTest { static int staticVar; final int instanceConst = 10; public static void main(String[] args) { MemoryTest obj = new MemoryTest(); System.out.println(staticVar); // 方法区数据 System.out.println(obj.instanceConst); // 堆内数据 int local = 20; // 栈内存分配 } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值