Java基础入门篇
三、面向对象和JVM底层分析
3.1 面向过程&面向对象
面向过程:代表是C语言
,重点关注的是程序如何执行,适用于过程简单不需要协作的事务。
面向对象:代表是Java语言
和C++语言
,重点关注的是如何设计这个事物,而不是按照什么步骤来执行,适用于需要协作的复杂事务。
举一个简单的例子来区分一下面向过程和面向对象:把大象装进冰箱,我们思考的是需要哪些步骤(打开冰箱,把大象放进冰箱,关上冰箱…)——面向过程。而当我们思考如何制造一辆汽车的时候,我们会去想汽车都包含哪些零部件(车轮,发动机,车门…),然后再去分别进行加工,最后制造出一辆汽车——面向对象。
面向对象可以帮助我们从宏观角度把握分析整个系统,但是从微观上看,面向对象本质上也是有一定的执行顺序一步一步执行的,因此仍然是面向过程。
如果从执行者与设计者来进行比喻的话,面向对象就是一种“设计者思维”,而面向过程则是一种“执行者思维”,这两种方法密切相关且不可分割。
/*
* 构建一个类,并且创建一个对象
*/
public static class Stu{
int id;
int age;
String name;
public void study(){
System.out.println("正在学习...");
}
public void kickball(){
System.out.println("正在踢球...");
}
}
3.2 JVM内存分析
(一)简单内存分析
JVM是Java的虚拟机,也是Java程序实际运行的地方,在JVM的内存中分为三个部分分别是:栈区、堆区、方法区。
**栈区(Stack):**是存放执行方法的地方,例如main方法,
**堆区(Heap):**存放的是创建的对象
**方法区(Method Area):**存放的是类的信息,例如:属性、方法
(二 )创建对象的步骤
创建对象分为四步:1.分配对象空间,并将对象成员变量初始化为0或空、2.执行属性值的显示初始化、3.执行构造方法、4.返回对象的地址给相关的变量。
其中构造器的方法名必须和类名一致,通过new关键字调用,构造器虽然有返回值,但是不能定义返回值类型(即,不能在构造器中定义return)。如果没有定义构造器,则编译器会自动定义一个无参的构造方法,如果已定义则编译器不会自动添加。
/*
* 构造器练习
* 定义一个点(Point)类,用来表示二维空间中的点,有两个坐标,要求:
* 1.可以生成具有特定坐标的点对象
* 2.提供可以设置坐标的方法
* 3.提供可以计算该点距另外一点距离的方法
*/
public static class Point{
double x;
double y;
// 定义一个空的构造器
Point(){}
// 重载构造器
Point(double _x, double _y){
this.x = _x;
this.y = _y;
}
Point(double _x){
this.x = _x;
}
public void distance(Point p){
double dis = Math.sqrt((x-p.x)*(x-p.x) + (y-p.y)*(y-p.y));
System.out.println("两点间距离是:"+dis);
}
}
public void testConstructor(){
Point p1 = new Point(3.0, 4.0);
Point p2 = new Point(0.0, 0.0);
p1.distance(p2);
}
(三)进阶内存分析
Java的虚拟机内存模型:由多份线程组成,每一个线程都有独立的(虚拟机栈、本地方法栈、程序计数器),“堆”中包括新生代和老年代(涉及垃圾回收),“方法区”(存放一些唯一的不变信息,例如类信息,常量池),“栈”描述的是方法执行的内存空间,每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)。
JVM为每一个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等),“栈”属于线程私有,不能实现线程间的共享,存储特性是后进先出。“栈”是由系统自动分配,速度快,同时栈是一个连续的内存空间。
JVM中的“堆”用于存储创建好的对象和数组(数组也是对象),JVM只有一个堆,被所有线程共享。“堆”是一个不连续的内存空间,分配灵活,速度慢。“堆”被所有的线程所共享,在堆上的区域会被垃圾回收器做进一步划分,例如新生代、老年代的划分。
“方法区”本质上也是“堆”,是Java虚拟机的规范,可以有不同的实现,JDK7以前是“永久代”,JDK8是“元数据空间”和“堆”结合起来,去除“永久代”的静态变量、字符串常量池,挪到了堆内存中。“JVM”只有一个方法区,被所有线程共享,用于存储“类、常量等相关信息”,即用来存放程序中永远不变或者唯一的内容
“常量池”主要存放常量,例如文本字符串、final常量值。
(四)垃圾回收机制
Java中引入了垃圾回收机制,应对令C++程序员头疼的内存管理问题,所以说Java虽然“运行效率”不如C++,但是“开发效率”高于C++。Java的内存管理很大程度上就是指的:“堆中对象的管理”,包括对象空间的分配和释放(分配——new关键字,释放——将对象赋值null即可)
垃圾回收过程一般要做两件基本的事情:
- 发现无用的对象
- 回收无用对象占用的内存空间。
“无用的对象”指的是没有任何变量引用该对象,Java的垃圾回收器通过相关算法,发现无用对象,并进行清除和整理。垃圾回收相关算法:
-
引用计数法,
-
引用可达法(根据搜索算法)
“引用计数法”:堆中的每一个对象都对应一个引用计数器,当有引用指向这个对象时,引用计数器+1,而当指向该对象的引用失效时(引用变为null),引用计数器-1,如果最后该对象的引用计数器值=0,则Java垃圾回收器会判断该对象为无用对象,并对其进行垃圾回收
“引用可达法”:程序把所有的引用关系看作是一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点之后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点被认为是没有被引用到的无用节点
“引用计数法”的优点是算法简单,缺点是循环引用无用的对象则无法识别。
通用的“分代”垃圾回收机制,主要是指回收“堆”中的信息,堆包含三部分“年轻代”、“年老代”、“永久代”(本质上就是方法区,用于存放一些不变的内容,在JDK7以前存在,JDK8中永久代用元数据空间——metaspace和堆取代),一般垃圾回收指的是前两类。
“年轻代”包含三个部分(Eden、Survivor1区、Survivor2区)存储从未被垃圾回收的新对象的区域叫做“Eden区”,当Eden区存满了之后,会触发“Minor GC”(专门用来清理年轻代区域),清理无用对象(指的是清理整个年轻代的无用对象),然后将有用的对象复制到“Survivor1区”和“Survivor2区”,同时Suvivor1区和2区中存储的对象在经过Minor GC清理无用对象后,会循环复制存储内容。当年轻代Survivor中交换>15
次后仍然存在的对象被认为是重要的,存储到“年老代”(Tenured区)中,年老代中的垃圾清理机制叫做“Major GC”,Minor GC相较于Major GC更加轻量,启动速度更快。
还有Full GC(全量回收):用于清理年轻代、年老代、永久代,启动成本较高,会对系统性能产生影响。
年轻代Minor GC的目标是尽可能快速的手机掉生命周期短的对象,年老代中存放的是一些生命周期较长的对象,随着年老代中存储对象的增多,需要通过Major GC和Full GC来一次大扫除,全面清理整个堆区的三个部分。
JVM调优的过程中很大一部分工作就是对Full GC的调节,触发Full GC的原因“可能”是:
-
年老代(Tenured)被写满
-
永久代(Perm)被写满
-
System.gc()被显示调用
-
上一次GC之后堆(Heap)的各域分配策略动态变化
/*
* 循环引用示例
*/
public class GarbageCollection{
int id;
GarbageCollection friend;
public void testGarbage(){
/*
* 创建两个对象,同时进行相互的循环引用,当两个对象的引用赋值为null之后,由于存在相互引用导致引用计数器为1≠0,
* 因此无法被识别为无用对象,但是实际上已经无用了
*/
GarbageCollection g1 = new GarbageCollection();
GarbageCollection g2 = new GarbageCollection();
g1.friend = g2;
g2.friend = g1;
g1 = null;
g2 = null;
}
}
3.3 面向对象中的this关键字和static关键字
(一)this关键字
this关键字表示当前对象本身(当前对象的引用地址)。“普通方法”中的this总是指向调用该方法的对象,“构造方法”中的this总是指向正要初始化的对象(注意,构造函数中不写this,默认也是带有this的)。this()调用重载的构造方法,避免相同的初始化代码,但是只能在构造方法中用,并且必须位于构造方法的第一句。this方法不能用于static方法中。普通方法的this是一种隐式参数,由系统自动传递。
/*
* 测试this方法
*/
public static class TestThis{
int a, b, c;
TestThis(){
System.out.println("当前初始化对象: " + this);
}
TestThis(int a, int b){
// 调用构造方法不能通过TestThis();的方法,而是要通过下面的this();的方法
this(); // 调用无参的构造方法,并且必须位于第一行
a = a; // 这里是局部变量而不是成员变量
this.a = a;
this.b = b; // 通过使用this区分成员变量和局部变量
}
TestThis(int a, int b, int c){
this(a, b); // 调用有参的构造方法,并且必须位于第一行
a = a; // 这里是局部变量而不是成员变量
this.c = c;
}
void sing(){
System.out.println("当前 "+ this + " 正在唱歌~");
}
void eat(){
System.out.println("当前对象: " + this);
this.sing();
System.out.println("快回家吃饭吧!");
}
}
public static void testThis(){
TestThis t1 = new TestThis(2, 3);
t1.eat();
}
(二)static关键字
static关键字,由static关键字定义的属性和方法都属于类位于方法区中,而普通对象的属性和方法都位于堆中,类只被加载一次,也只有一份。静态的变量/方法的生命周期和类相同,在整个程序执行期间都有效静态的变量/方法为该类的公用变量,属于类,被该类的所有实例共享,在类载入时被初始化static成员变量只有一份,但是可以有多个变量共同访问同一个static变量。一般用“类名.类的属性/方法”来进行调用。在static方法中不可以直接访问非static的成员。构造方法用于初始化普通属性,静态初始化块用来初始化静态属性,注意静态初始化块中不能直接访问非静态成员。
静态初始化块的执行顺序(结合继承进行理解):上述依次找父类,直到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到类的静态初始化块为止(构造方法的执行顺序同理)
变量的分类:局部变量(属于方法/语句块——位于栈)、成员变量(属于对象——位于堆)、静态变量(属于类——位于方法区)
/*
* 测试static方法
*/
public static class TestStatic{
int id;
String name;
static String company;
// 定一个静态初始化块初始化静态属性
static{
company = "北京同仁堂";
getCompany(); // 静态初始化块中也可以调用静态方法
}
TestStatic(){
System.out.println(this + "欢迎使用");
}
TestStatic(int a, String b){
this.id = a;
this.name = b;
}
public void test01(){
System.out.println("test01");
}
public static void getCompany(){
//静态方法中无法使用非静态方法,因为,静态的方法属于类,非静态方法属于对象,直接使用无法知道是属于哪一个对象的方法
// test01();
// System.out.println(id);
System.out.println(company);
}
}
public static void testStatic(){
TestStatic t1 = new TestStatic(1001, "小明");
t1.company = "石家庄石药集团";
t1.getCompany();
}
(三)包的导入
import
导入类,可以在本类中使用其他包的类。Java会默认导入java.lang包下的所有类,例如String,依次这些类我们可以直接使用。如果有两个同名的类,则只能用包名+类名来显示调用相关类。使用.*会导入该包下的所有类,会降低编译速度,但是不会降低运行速度import static
静态导入作用是用于导入指定类下的静态属性和静态方法,这样我们可以直接使用静态的属性/方法。