Java中new对象触发类加载、初始化、堆分配、引用建立四步;构造器不自动补默认方法,字段初始化顺序固定,对象销毁不等于立即回收,资源需显式关闭。

Java里new一个对象到底发生了什么
不是简单分配内存,而是触发类加载、初始化、堆空间分配、引用建立四步连环操作。没理解这点,遇到NullPointerException或静态块不执行时容易懵。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 类首次被
new、static字段访问、反射调用时才触发ClassLoader.loadClass()和<clinit></clinit>(静态初始化块) - 真正分配堆内存的是JVM的
object allocation流程,受GC策略影响——比如G1下大对象可能直接进Humongous Region - 栈上只存引用变量(如
MyClass obj),obj本身不存对象,对象实体永远在堆里 - 别在构造器里传
this出去——此时对象可能还没初始化完,其他线程拿到的是半成品
构造方法不是函数,不能随便重载或省略
Java不会自动补默认构造方法,一旦你写了带参构造,无参构造就没了。这是新手写完public Person(String name)后,再new Person()直接报Compilation error: constructor Person() is not defined的根源。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 如果需要无参构造,必须显式写
public Person() {},哪怕空着 - 构造方法里调用另一个构造要用
this(...),且必须是第一行;调用父类构造用super(...),也必须第一行——顺序错会编译失败 - 避免在构造方法里做耗时操作(如IO、网络请求),既拖慢对象创建,又破坏单一职责
- 考虑用静态工厂方法替代构造器,比如
Person.createAdult(String name),语义更清晰,还能返回子类或缓存实例
成员变量初始化顺序决定bug藏在哪
字段声明顺序、初始化块、构造方法三者执行顺序固定:父类静态 → 子类静态 → 父类实例字段/块 → 父类构造 → 子类实例字段/块 → 子类构造。这个顺序错一点,final字段可能被赋值两次,或者String字段还是null就被用了。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 把所有字段初始化写在声明处(如
private List<string> items = new ArrayList();</string>),比放在构造器里更安全、更易读 - 避免在实例初始化块里调用可被重写的方法(
protected或public),子类构造前父类就可能调到子类未准备好的逻辑 -
final字段必须在构造结束前明确赋值,否则编译报错;但若用static final,就得在声明时或静态块里赋值 - 调试时加个
System.out.println("in field init")就能快速验证执行流,比猜快得多
对象销毁不等于内存立刻回收
Java没有析构函数,finalize()已被标记为@Deprecated且不保证执行。你调obj = null只是断开引用,对象是否回收取决于GC时机和可达性分析结果——这点常被当成“内存泄漏”的假警报。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 资源型对象(
FileInputStream、Connection)必须显式close(),用try-with-resources最稳妥 - 监听器、回调、缓存引用这些容易造成强引用链的地方,优先用
WeakReference或SoftReference - 别依赖
System.gc()——它只是建议,JVM可以忽略;真要观察回收行为,用jstat -gc <pid>看实际YGC/FGC次数 - 局部变量超出作用域后,只要没逃逸(escape analysis判定),JVM可能直接栈上分配,根本不上堆——所以别一概而论“所有对象都在堆里”
类设计时最容易被忽略的,其实是字段的可见性与初始化时机耦合。一个private volatile List<String> cache,如果在构造器里只做了cache = new ArrayList<>(),那后续多线程往里add依然可能出问题——得配合同步或用Collections.synchronizedList()。这种细节,光看语法对不对没用,得盯住运行时行为。











