java解惑之jvm内存区域和类加载机制
前言
本文不会深入原理去讲解,而是通过具体的场景(代码)来理解java的内存区域和类加载机制
java内存区域划分
从jvm角度看,分为堆,栈,方法区,本地方法栈,程序计数器
1.png
从操作系统角度看,分为堆,栈,数据段,代码段
程序计数器
线程私有,指示的是当前线程字节码执行到的位置
虚拟机栈
线程私有,描述的是java方法执行的内存模型,每一个方法从执行到结束,都对应一个方法栈帧的入栈和出栈,方法中的局部变量就存储在栈中,当然指的是基本类型和引用类型的引用,实例还是存放在堆中
栈是一块连续的内存区域,大小由操作系统决定,不会产生碎片
本地方法栈
和虚拟机栈的作用类似,只不过本地方法栈是为jvm使用到的Native方法服务
堆
线程共享区域,存放着几乎所有的实例
一块大但是不连续的内存区域,容易产生碎片,一般说的内存泄漏,主要指的就这块
方法区
线程共享区域,存储已经被jvm加载的类的信息,常量,静态变量,编译器编译后的代码等数据
运行时常量池就是方法区的一部分,用于存放各种字面量和符号引用
字面量:基本类型的包装类,字符串类型,final修饰的常量
代码示例
public class People{
int a = 1;
final int b=2;
static int c=3;
Student s1 = new Student();
public void XXX(){
int d = 1;
Student s2 = new Student();
}
}
问a,b,c,d的内存在哪里,s1,s2的内存在哪里?
记住下面几句话。
成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体),因为他们属于对象,对象都是存储在堆中的
局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。因为他们属于方法当中的变量,生命周期会随着方法一起结束。
常量,静态变量(属于类)存放在常量池
存放在堆中的有a,s1和s1的实例,s2的实例
存放在栈中的有d,s2
存放在方法区(常量池)的有b,c
类加载
什么是类加载
类加载指的是将.class文件的二进制数据读取到内存中,将其放入内存区域中的方法区,然后在堆中创建一个Class对象,这就是我们说的任何类都是Class类的一个实例的原因
什么时候进行类加载
简单说就是类在用到的时候才会被加载,下面我来列举一些情况来说明什么叫被用到
- 创建类的实例
- 调用类的静态变量(final修饰的静态变量除外)
- 调用类的静态方法
- 反射如Class.forName(com.xxx.xxx)
类加载过程
1.类加载
类的加载是由类加载器来完成加载的
说到类加载,我们来简单说下双亲委派机制
java自带的3中类加载器分别是AppClassLoader,ExtClassLoader和Bootstrap
我们自己写的类首先会调用AppClassLoader去加载自身,但它不会马上加载,而是调用ExtClassLoader去加载,它也不会马上加载,而是调用Bootstrap去加载,如果Bootstrap加载不了,就让ExtClassLoader去加载,还加载不了就让AppClassLoader加载
2.类的连接
类连接有3步,分别是验证,准备,解析
这里我只说明一下准备阶段做的工作
为类的静态变量分配内存并赋值默认的初始值
public static int value=123;
在准备阶段value的值是0
3.类的初始化(顺序)
-
没有父类的情况
类的静态属性
类的静态代码块
类的非静态属性
类的非静态代码块
构造方法 -
有父类的情况
父类的静态属性
父类的静态代码块
子类的静态属性
子类的静态代码块
父类的非静态属性
父类的非静态代码块
父类的构造方法
子类的非静态属性
子类的非静态代码块
子类的构造方法
代码示例1
public class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
结果输出
count1=1
count2=0
结果分析
1.调用类的静态方法会触发类的加载
2.在准备阶段为静态变量分配内存并赋值默认初始值
singleTon=null;
count1=0;
count2=0;
3.初始化操作(赋值操作),初始化是从上到下依次执行的
先调用new操作,count1=1,count2=1
count1没有赋值操作
count2进行赋值操作
修改代码如下
public class SingleTon {
public static int count1;
public static int count2 = 0;
private static SingleTon singleTon = new SingleTon();
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
结果输出
count1=1
count2=1
代码示例2
public class HelloA {
public static int a=1;
static{
System.out.println("Static A");
}
{
System.out.println("I am A class");
}
public HelloA(int i){
System.out.println("Hello A");
}
}
public class HelloB extends HelloA{
public static int b=0;
static{
System.out.println("Static B");
}
{
System.out.println("I am B class");
}
public HelloB(){
super(1);
System.out.println("Hello B");
}
public static void main(String[] args) {
new HelloB();
//HelloB.a=1;
}
}
输出结果
Static A
Static B
I am A class
Hello A
I am B class
Hello B
结果分析
参考前面类的初始化顺序