1、进一步解析jvm内存
1.1、jvm内存模型
-
class文件通过类加载器加载到运行时数据区,然后再由执行引擎决定是否执行。
-
class文件经过类加载系统,首先启动类加载器(启动类加载器,拓展类加载器,启动类加载器),经过双亲委托机制后加载到运行时数据区;
-
运行时数据区:
- 方法区
- 堆
- 虚拟机栈
- 本地方法栈
- 程序计数器
-
经过运行时数据区之后再由执行引擎决定是否区执行
-
其中方法区和堆区时线程共享区(java程序运行过程中,所有线程共同享有的区域),虚拟机栈,本地方法栈,程序计数器时线程独占区。
1.2、类加载器子系统
类加载器子系统加载过程:
- 加载阶段
- 启动类加载器
- 拓展类加载器
- 应用类加载器
- 链接阶段
- 验证(字节码校验)
- 准备
- 解析
- 初始化
- 类加载器只是加载class文件,这个类最后是否会被执行由执行引擎所决定,每一个class文件都有自己特定的标识------魔数
- jvm并不是只可以加载java编写的字节码文件,只要符合jvm规范或者是标准,都可以加载,不同的编程语言的标识都不相同
- 加载过后的信息都会存放在jvm内存中方法区。
1.3、运行时数据区
方法区包括:
-
类型信息:
- 类的完整名称—限定类名
- 类的直接父类的完整名称(java.lang.Object)
- 类的直接实现接口的有序列表(一个类只可以继承一个类,但可以同时拥有多个接口)
- 类的修饰符(public static abstract final)对这个类进一步说明
-
类型的常量池(运行时常量池)
- 每一个class文件中,都维护这一个常量池(这个保存在类文件中,方法区中的叫做运行时常量池,在类加载的时候,会被复制到运行时常量池),里面存放着编译时期生成的各种 **字面值和符号引用;
- 字面值:在编译期间可以确定的值,死值。
-
类变量(static变量,不用创建对象就可以直接使用)
- 非final类变量
- 在jvm使用一个类之前,要先为每一个final类变量分配内存空间
- final的作用就是不可更改,可以理解为固定值,在编译期间就被复制到了运行时厂里囊吃,每一个使用它的类都会保存这一个对它的引用
-
堆
堆可以分为新生代和老年代,jdk1.8版本已经将永久代删除,堆是jvm内存空间中最大的。
-
新生代
-
当一个对象创建时,都会优先存放在新生代中,新生代存放的对象大都使用完之后就会被回收,存活率很低,再每进行垃圾回收时,一般可以回收百分之七十到百分之九十五的空间(垃圾回收机制就是为了释放空间),回收效率很高。
-
hotspot将新生代划分为三块
- Eden(较大的空间)
- Survicor0
- Survicor1
三者的比列默认为:8:1:1。划分的目的就是为了采取复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。当Eden区的内存不足时候会发起一次 minor GC (垃圾回收)
-
复制算法:
GC开始时,对象只会存在于Eden区和Survivor 0区,S1区是空的(作为保留区域)。GC进行时,
Eden
区中所有存活的对象都会被复制到S1区,而在S0
区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header
中)的对象会被移到老年代中。没有达到阀值的对象会被复制到
s1
区。接着清空Eden
区和s0
区,新生代中存活的对象都在s1
区。接着,s0
区和s1
区会交换它们的角色,也就是新的s1
区就是上次GC清空的s0
区,新的s0
区就是上次GC的s1
区,总之,不管怎样都会保**s1
区在一轮GC后是空的**。GC时当s1
区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
-
-
老年代
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
-
垃圾回收
新生代GC(Minor GC):Minor GC指发生在新生代的GC,因为新生代的Java对象大多都是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Minor GC
老年代GC(Full GC/Major GC):Full GC指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Minor GC过程中从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。
-
虚拟机栈
描述的是Java方法执行的内存模型
-
每个方法被执行(压栈)的时候都会创建一个栈帧,用来存储局部变量(包括参数)、操作栈,方法出口等信息
-
每个方法被调用执行完的过程,就对应这一个 栈帧在虚拟机中从入栈搭配出栈的过程,栈帧由四部分组成
- 局部变量表(局部变量表一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量)
- 操作数栈
- 动态链接
- 返回地址
执行都是当前的栈帧,谁在栈顶就先执行谁(先进后出)
本地方法栈
本地方法栈与虚拟机栈类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。
程序计数器(PC寄存器)
pc寄存器是最小的一块内存区域,它的作用是当前线程所执行的字节码文件的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,异常处理,线程恢复等基础功能都需要依赖计数器完成。
存储下一条指令的地址,也就是下一个即将执行的代码。
1.4、执行引擎区
执行引擎区会从pc寄存器中,钩出一条指令执行
执行殷勤:
- 解释器
- JIT
如果钩出来的代码时热点代码,编译器期间就确定,并且之间缓存,下次可以直接执行,非热点代码就解释一行cpu就执行一行
2、Java面对对象编程
2.1、OOP面向对象编程
-
把构成问题的各种事物,抽象成为对象,这些对象解决问题的行为称为方法,方法可以解决类似的问题,而不是单一解决一个问题。
-
面对对象思想,是一种程序设计思想,Java就是面向对象编程语言
-
对象泛指现实世界中的一切事物,当然也有虚拟的,抽象的,没有具体的,例如淘宝购物车,只是有着一个概念,但没有具体的形象。在计算机程序设计过程中,参照这些事物,将其的属性特征,行为方法抽象成为一种数据类型
-
面对对象的思想,主要强调的是通过调用对象的行为来实现功能
-
面向对象编程中的三大特征
- 封装
- 继承
- 多态
2.2、类
Java中对数据类型的描述和定义,都是抽象的,每一种数据类型都是对同一类型数据的抽象描述,描述了这种数据的基本类型
数据类型都不能当作具体的数据使用或参与运算
- 要想使用一个类,首先要对这个类进行变量的声明,再用变量接收数据或者时对象,然后就可以使用这个变量来进行操作
2.3、对象
简单来说,我们所能看见的具体的某一个事物就是对象,当然有些虚拟的,并没有具体的形状的也是对象
对象和类的区别
类是一个具有相同特征对象的集合,比如狗是一个类,而具体的某一只狗就是对象,动物是一个类 ,狗就是动物中的一个对象。
- 类是对一类事物的描述,是抽象的
- 对象是一类事物的实例,是具体的
- 类是对象的模板,对象是类的实体
对象和对象都会有自己的属性特征,还有操作行为(方法)
2.4、引用
引用类型变量称为引用
引用可以用来指向对象,简称引用指向对象
再使用类创建出一个对象时候,此时对象存放再对空间中,不方便使用,此时就需要声明一个引用来指向这个对象,便于通过引用来操作对象。
//使用new 加上 Student类中构造器,来创建Student类的对象
new Student();
//为了能方便的使用这个对象,就可以给这个对象起一个名字
//这个过程,其实就是之前学习过的 =号赋值操作
//把新创建的学生对象,赋值给了引用stu
Student stu = new Student();
//后面就可以使用引用stu来操作对象了,也就是访问对象中的属性和调用对象中的方法
stu.name = "tom";
stu.sayHello();
当然采用引用也可以使用对象,但是一次性的
(new Student()).sayHello();
//下面就无法再使用上面创建出来的这个对象,因为他没有名字
...
//如果再这样写一次,其实是创建了第二个对象,并进行使用,但是后面也无法使用这个对象,因为没有名字
(new Student()).sayHello();
引用、对象、类之间的关系
在类中创建一个对象,再通过引用来操作这个对象,
例如,工厂根据电视机图纸(类),生产出了很多台电视机(对象),其中一台电视机卖给了张三,张三坐在沙发上,使用遥控器(引用),可以对这台具体的电视机(对象)进行很方便的操作。
2.5、内存
注意1,类加载过程,把Student.class文件内容加载到方法区中
注意2,main方法运行时,整个main方法的代码都被加载到栈区中
注意3,创建对象,给name属性赋值,这些操作的代码也在main方法中,但由于代码太长,图中标注到右边
注意4,根据类(相当于模板)创建出的对象,都是在堆区中
注意5,对象中的属性和方法,和类中定义的保持一致,因为对象就是根据类创建出来的
注意6,对象是具体的,我们可以给它的属性赋一个具体的值,例如tom
注意7,引用stu在栈去中,它保存了这个对象在堆区中的地址(0x123),形象的描述为,引用指向对象
注意8,=号赋值操作,其实就把对象的内存地址,赋值给了引用stu
注意9,如果有需要,可以根据类,继续在堆区中,创建出其他具体的学生对象
从这里可以看出,引用指向对象后,为什么就可以使用引用来操作对象,例如访问对象的属性和调用方法
2.6、方法
方法定义在类中,属于类的成员,所以也可以叫做成员方法。类似的,类中的属性,也可以称为成员变量
修饰符:
-
public、static、abstract、final等这些都属于修饰符,可以用来修饰方法、属性、类
-
一个方法上,可以同时拥有多个不同的修饰符,例如程序入口main方法
public static void main(String[] args){...}
这里使用俩个修饰符public和static来修饰main方法 -
如果方法上有多个修饰符,这些修饰符是没顺序之分的
例如,这俩个写法最终的效果是一样的
public static void main(String[] args){}
static public void main(String[] args){}
-
方法上也可以不写修饰符
返回类型:
-
方法执行完,如果有要返回的数据,那么在方法上就一定要声明返回数据的类型是什么,如果没有要返回的数据,那么在方法上就必须使用void进行声明
public int getNum(){...}
public void print(){...}
-
只有一种特殊的方法没有返回类型,也不写void,那就是构造方法,也就是构造器
public class Student{ //构造方法 public Student(){} }
-
声明有返回类型的方法,就要使用
return
关键字在方法中,把指定类型的数据返回public int test(){ return 1; }
思考,如果一个方法的返回类型声明为void,那么在这个方法中还能不能使用return关键字?
方法名:
- 只要满足java中标识符的命名规则即可
- 推荐使用有意义的方法名
参数列表:
-
根据具体情况,可以定义为无参、1个参数、多个参数、可变参数。(一个方法中有且只有一个可变参数,如果一个方法有多个参数,且包含可变参数,必须放在末尾)
public void test(){} public void test(int a){} public void test(int a,int b,int c){} public void test(int... arr){} public void test(int a,String str){}
抛出的异常类型:
-
在方法的参数列表后,可以使用
throws
关键字,表明该方法在j将来调用执行的过程中,【可能】会抛出什么类型的异常 -
可以声明多种类型的异常,因为在方法执行期间,可能会抛出的异常类型不止一种。
public void test()throws RuntimeException{} public void test()throws IOException,ClassNotFoundException{}
2.7、参数传递
java方法的参数,分为形参和实参。
形参:
-
形式上的参数
public void test(int a){}
其中,参数a就是test方法形式上的参数,它的作用就是接收外部传过来的实际参数的值
实参:
-
实际上的参数
public class Test{ public void test(int a){} } public static void main(String[] args){ Test t = new Test(); t.test(1); int x = 10; t.test(x); }
其中,调用方法的时候,所传的参数1和x,都是test方法实际调用时候所传的参数,简称实参
值传递:
方法的参数是基本类型,调用方法并传参,这时候进行的是值传递。
例如,
public class Test{
//该方法中,改变参数当前的值
public static void changeNum(int a){
a = 10;
}
public static void main(String[] args){
int a = 1;
System.out.println("before: a = "+a);//传参之前,变量a的值
changeNum(a);
System.out.println("after: a = "+a);//传参之后,变量a的值
}
}
值传递,实参把自己存储的值(基本类型都是简单的数字)赋值给形参,之后形参如何操作,对形参一点影响没有。
引用传递:
方法的参数是引用类型,调用方法并传参,这时候进行的是引用传递。
这时候之所以称之为引用传递,是因为参数和形参都是引用类型变量,其中保存都是对象在堆区中的内存地址。
例如,
public class Test{
//该方法中,改变引用s所指向对象的name属性值
public static void changeName(Student s){
s.name = "tom";
}
public static void main(String[] args){
Student s = new Student();
System.out.println("before: name = "+s.name);//传参之前,引用s所指向对象的name属性值
changeName(s);
System.out.println("after: name = "+s.name);//传参之后,引用s所指向对象的name属性值
}
}
由于引用传递,是实参将自己存储的对象地址,赋值给了形参,这时候俩个引用(实参和形参)指向了同一个对象,那么任何一个引用(实参或形参)操作对象,例如属性赋值,那么另一个引用(形参或实参)都可以看到这个对象中属性的变量,因为俩个引用指向同一个对象。
这个时候就相当于,俩个遥控器,同时控制同一台电视机。
3、作业
1.写一个冒泡排序的方法,传入一个数组,返回一个排好序的数组
bubbling
2.写一个求和的方法,传入数据,返回数据的和
sums
4. 请写出一个数组扩容的方法
capacity
import java.util.Arrays;
public class ObjectProgramming {
public String bubbling(int...arr) {
for(int i = 0;i<arr.length;i++) {
for(int j = 0;j<arr.length-i-1;j++) {
if(arr[j]>arr[j+1]) {
arr[j]=arr[j]^arr[j+1];
arr[j+1]=arr[j]^arr[j+1];
arr[j]=arr[j]^arr[j+1];
}
}
}
return Arrays.toString(arr);
}
public int sums(int...arr) {
int sum = 0;
for(int i = 0;i<arr.length;i++) {
sum+=arr[i];
}
return sum;
}
public String capacity(int...arr) {
arr=Arrays.copyOf(arr,arr.length*2);
return Arrays.toString(arr);
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ObjectProgramming ob=new ObjectProgramming();
int[] arr = {3,1,5,4,7,2};
System.out.println("初始数据:"+Arrays.toString(arr));
System.out.println("冒泡排序:"+ob.bubbling(arr));
System.out.println("数据求和:"+ob.sums(arr));
System.out.println("数组扩容:"+ob.capacity(arr));
}
}
3.请问值传递和引用传递的区别是什么
值传递传递的是具体的数据,引用传递传递的是对象的内存地址
4.请画图类运行内存图
5.请问以下编译是否报错,为什么,如果报错怎么修改
public class A {
public void a(int[] arr) {
System.out.println(arr[3]);
//ArrayIndexOutOfBoundsException 数组下标越界异常 因为数组下标最大是1;
}
public static void main(String... args) {
int[] arr = {1,2};
new A().a(arr);
}
}
6.以下结果为多少,为什么
public class A {
public void a1(int... args) {
args[0] = 13;
args[1] = 15;
System.out.println(args[1]);
}
public static void main(String... args) {
int a = 1;
int b = 2;
new A().a1(a,b);
System.out.println(a + " " + b);
}
}
//15
//1 2
//这里的参数传递是传递到方法中是一个数组,在方法中重新为两个元素赋值,并且在方法中打印输出下标为1的元素,方法结束。在System.out.println(a + " " + b);中,调用的是定义的 a b。main方法中的ab存在于虚拟机栈中, 而a b 并没有指向对应的数组的堆内存,所以数组重新赋值,并不会改变ab的值,数组的堆内存空间被垃圾回收机制回收,但main方法还在栈中,还没出栈,在执行main方法时,ab的值还是 1 和 2。