Java虚拟机(JVM)内存底层分析
对象和类的详解
类:我们叫做class。 对象:我们叫做Object,instance(实例)。
总结
1.类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
2.类是用于描述同一类型的对象的一个抽象概念,类中定义了这一类对象所应具有的共同的属性、方法。
类的定义:对于一个类来说,有三种成员:属性field、方法method、构造器constructor。
属性(field 成员变量)
1.属性用于定义该类或该类对象包含的数据或者说静态特征。属性作用范围是整个类体。
2.在定义成员变量时可以对其初始化,如果不对其初始化,Java使用默认的值对其初始化。
属性定义格式:[修饰符] 属性类型 属性名 = [默认值] ;
构造方法(构造器 constructor)
构造器用于对象的初始化,而不是创建对象!
声明格式:[修饰符] 类名(形参列表){ //n条语句}
构造器4个要点:
1.构造器通过new关键字调用!!
2.构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。
3.如果我们没有定义构造器,则编译器会自动定义一个无参的构造方法。如果已定义则编译器不会自动添加!
4.构造器的方法名必须和类名一致!
构造方法的重载
构造方法也是方法。与普通方法一样,构造方法也可以重载。
JAVA虚拟机内存模型概念
Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area。
虚拟机栈(简称:栈)的特点如下:
1.栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)
2.JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
3.栈属于线程私有,不能实现线程间的共享!
4.栈的存储特性是“先进后出,后进先出”
5.栈是由系统自动分配,速度快!栈是一个连续的内存空间!
堆的特点如下:
1.堆用于存储创建好的对象和数组(数组也是对象)
2.JVM只有一个堆,被所有线程共享
3.堆是一个不连续的内存空间,分配灵活,速度慢!
4.堆被所有的线程所共享,在堆上的区域,会被垃圾回收器做进一步划分,例如新生代、老年代的划分。
方法区(也是堆)特点如下:
1.方法区是JAVA虚拟机规范,可以有不同的实现。
i. JDK7以前是“永久代”
ii. JDK7部分去除“永久代”,静态变量、字符串常量池都挪到了堆内存中
iii. JDK8是“元数据空间”和堆结合起来。
2.JVM只有一个方法区,被所有线程共享!
3.方法区实际也是堆,只是用于存储类、常量相关的信息!
4.用来存放程序中永远是不变或唯一的内容。(类信息【Class对象,反射机制中会重点讲授】、静态变量、字符串常量等)
5.常量池主要存放常量:如文本字符串、final常量值。
参数传值机制
Java中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。 也就是说,我们得到的是“原参数的复印件,而不是原件”。
1.基本数据类型参数的传值:传递的是值的副本。 副本改变不会影响原件。
2. 引用类型参数的传值:传递的是值的副本。但是引用类型指的是“对象的地址”。
因此,副本和原参数都指向了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变”。
垃圾回收机制(Garbage Collection)
垃圾回收原理和算法
内存管理
Java的内存管理很大程度就是:堆中对象的管理,其中包括对象空间的分配和释放。
1.对象空间的分配:使用new关键字创建对象即可
2.对象空间的释放:将对象赋值null即可。
垃圾回收过程
任何一种垃圾回收算法一般要做两件基本事情:
1.发现无用的对象
2.回收无用对象占用的内存空间。
垃圾回收机制保证可以将“无用的对象”进行回收。
无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。
垃圾回收相关算法
1.引用计数法
堆中的每个对象都对应一个引用计数器,当有引用指向这个对象时,引用计数器加1,而当指向该对象的引用失效时(引用变为null),引用计数器减1,最后如果该对象的引用计算器的值为0时,则Java垃圾回收器会认为该对象是无用对象并对其进行回收。
优点是算法简单,缺点是“循环引用的无用对象”无法别识别。
2.引用可达法(根搜索算法)
程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
通用的分代垃圾回收机制
分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
我们将对象分为三种状态:年轻代、年老代、永久代。同时,将处于不同状态的对象放到堆中不同的区域。
1. 年轻代
所有新生成的对象首先都是放在Eden区。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。
2. 年老代
在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。
3. 永久代
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。JDK7以前就是“方法区”的一种实现。JDK8以后已经没有“永久代”了,使用metaspace元数据空间和堆替代。
Minor GC:
用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中。
Major GC:
用于清理年老代区域。
Full GC:
用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
JVM调优和Full GC
在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满
2.永久代(Perm)被写满
3.System.gc()被显式调用
4.上一次GC之后Heap的各域分配策略动态变化
内存泄漏:指堆内存由于某种原因程序未释放,造成内存浪费,导致运行速度减慢甚至系统崩溃等。
开发中容易造成内存泄露的操作:
1.创建大量无用对象
比如:大量拼接字符串时,使用了String而不是StringBuilder。
2. 静态集合类的使用
像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期 和应用程序一致,所有的对象也不能被释放。
3. 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭
IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网 络连接,不使用的时候一定要关闭。
4. 监听器的使用不当
释放对象时,没有删除相应的监听器
其他要点
- 程序员无权调用垃圾回收器。
2.程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
3.Object对象的finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。
this关键字
this的用法:
1.普通方法中,this总是指向调用该方法的对象。
2.构造方法中,this总是指向正要初始化的对象。
this的其他要点:
1.this()调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。
2.this不能用于static方法中。
3.this是作为普通方法的“隐式参数”,由系统传入到方法中。
static 关键字
静态变量(类变量)、静态方法(类方法):static声明的属性或方法。
静态变量/静态方法生命周期和类相同,在整个程序执行期间都有效。它有如下特点:
1.为该类的公用变量,属于类,被该类的所有实例共享,在类载入时被初始化。
2.static变量只有一份。
3.一般用“类名.类变量/方法”来调用。
4.在static方法中不可直接访问非static的成员
静态初始化块
1.构造方法用于对象的普通属性初始化。
2.静态初始化块,用于类的初始化操作,初始化静态属性。
3.在静态初始化块中不能直接访问非static成员。
变量的分类和作用域
变量有三种类型:局部变量、成员变量(也称为实例变量)和静态变量。
类型 声明位置 从属于 生命周期(作用域)
局部变量 方法或语句块内部 方法/语句块 从声明处开始,到方法或语句块 结束
成员变量(实例变量) 类内部,方法外部 对象 对象创建,成员变量也跟着创建。 对象消失,成员变量也跟着消失;
静态变量(类变量) 类内部,static修饰 类 类被加载,静态变量就有效;
包机制(package、import)
包(package)相当于文件夹对于文件的作用。用于管理类、用于解决类的重名问题。
package的使用有两个要点:
1.通常是类的第一句非注释性语句。
2.包名:域名倒着写即可,便于内部管理类。
注意事项
1.写项目时都要加包,不要使用默认包。
2.com.gao和com.gao.car,这是两个完全独立的包。只是逻辑上看,后者是前者的一部分。
在IDEA项目中新建包
1.在src目录上单击右键,选择new->package
2.在package窗口上输入包名即可
3.即可在src下面看到包
4.接下来,我们就可以在包上单击右键,新建类
JDK中的主要包
Java中的常用包 说明
java.lang 包含一些Java语言的核心类,如String、Math、Integer、System和Thread。
java.awt 包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。
java.net 包含执行与网络相关的操作的类。
java.io 包含能提供多种输入/输出功能的类。
java.util 包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数。
导入类import
如果要使用其他包的类,需使用import,从而在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。
注意要点
1.Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。
2.如果导入两个同名的类,只能用包名+类名来显示调用相关类:java.util.Date date = new java.util.Date();
静态导入(static import): 其作用是用于导入指定类的静态属性和静态方法,这样我们可以直接使用静态属性和静态方法。
面向对象三大特征:继承、封装、多态
继承有两个主要作用:
1.代码复用,更加容易实现类的扩展
2.方便建模
instanceof 运算符
instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。
继承使用要点
1.父类也称作超类、基类。 子类:派生类等。
2.Java中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。
3.Java中类没有多继承,接口有多继承。
4.子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
5.如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object。
方法重写override:子类重写父类的方法,可以用自身行为替换父类行为。重写是实现多态的必要条件。
方法重写需要符合下面的三个要点:
1.= =:方法名、形参列表相同。
2.≤:返回值类型和声明异常类型,子类小于等于父类。
3.≥:访问权限,子类大于等于父类。
final关键字
final关键字的作用:
1.修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
final int MAX_SPEED = 120;
2.修饰方法:该方法不可被子类重写。但是可以被重载!
final void study(){}
3.修饰类: 修饰的类不能被继承。比如:Math、String等。
final class A {}
继承和组合
除了继承,“组合”也能实现代码的复用!“组合”核心是“将父类对象作为子类的属性”。
组合比较灵活。继承只能有一个父类,但是组合可以有多个属性。所以,有人声称“组合优于继承,开发中可以不用继承”,但是,不建议大家走极端。
Object类详解:所有类都是Object类的子类,也都具备Object类的所有特性。
Object类基本特性
1.Object类是所有类的父类,所有的Java对象都拥有Object类的属性和方法。
2.如果在类的声明中未使用extends,则默认继承Object类。
Object类
public class Person {
...
}
//等价于:
public class Person extends Object {
...
}
toString方法
Object类中定义有public String toString()方法,其返回值是 String 类型。
Object类中toString方法的源码为:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。
IDEA快捷键和相关操作:
1.类的结构视图:alt+7
2.看类的源码:ctrl+左键
3.自动生成构造器、get、set方法、equals等:alt+insert
4.查看错误:alt+enter
快捷输出常见字符串:
a) main public static void main(String[] args){}
b) sout System.out.println();
c) soutm System.out.println(“描述:所在类中的,所在方法”);
==和equals方法
1.==代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。
2.equals()提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。
3.equals()默认是比较两个对象的hashcode。但,可以根据自己的要求重写equals方法。
super关键字
1.super“可以看做”是直接父类对象的引用。可通过super来访问父类中被子类覆盖的方法或属性。
2.使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。
3.在一个类中,若是构造方法的第一行没有调用super(...)或者this(...); 那么Java默认都会调用super(),含义是调用父类的无参数构造方法。
继承树追溯
属性/方法查找顺序:(比如:查找变量h)
1.查找当前类中有没有属性h
2.依次上溯每个父类,查看每个父类中是否有h,直到Object
3.如果没找到,则出现编译错误。
4.上面步骤,只要找到h变量,则这个过程终止。
构造方法调用顺序:
1.构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。
封装(encapsulation)封装是面向对象三大特征之一。
封装的作用和含义
我们程序设计要追求“高内聚,低耦合”。
高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。
编程中封装的具体优点:
1.提高代码的安全性。
2.提高代码的复用性。
3.“高内聚”:封装细节,便于修改内部代码,提高可维护性。
4.“低耦合”:简化外部调用,便于调用者使用,便于扩展和协作
封装的实现—使用访问控制符
1.Java是使用访问控制符来控制哪些细节需要封装,哪些细节需要暴露的。
2.Java中4种访问控制符分别为private、default、protected、public。
【注】关于protected的两个细节:
1.若父类和子类在同一个包中,子类可访问父类的protected成员,也可访问父类对象的protected成员。
2.若子类和父类不在同一个包中,子类可访问父类的protected成员,不能访问父类对象的protected成员。
开发中封装的简单规则:
1.属性一般使用private访问权限。
2.属性私有后, 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。
3.方法:一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。
多态(polymorphism)多态指的是同一个方法调用,由于对象不同可能会有不同的行为。
多态的要点:
1.多态是方法的多态,不是属性的多态(多态与属性无关)。
2.多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。
3.父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
对象的转型(casting)
父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。
这时,我们就需要进行类型的强制转换,我们称之为向下转型。
抽象方法和抽象类
抽象方法
1.使用abstract修饰的方法,没有方法体,只有声明。
2。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。
抽象类
1.包含抽象方法的类就是抽象类。
2.通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。
抽象类的使用要点:
1.有抽象方法的类只能定义成抽象类
2.抽象类不能实例化,即不能用new来实例化抽象类。
3.抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。
4.抽象类只能用来被继承。
5.抽象方法必须被子类实现。
接口interface
接口就是一组规范(就像我们人间的法律一样),所有实现类都要遵守。
为什么需要接口?接口和抽象类的区别?
1.接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。
2.接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。
接口和实现类不是父子关系,是实现规则的关系。
比如:我定义一个接口Runnable,Car实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。
就是说,如果它是交通工具,就一定能跑,但是一定要实现Runnable接口。
声明格式:
[访问修饰符] interface 接口名 [extends 父接口1,父接口2…] {
常量定义;
方法定义;
}
定义接口的详细说明:
1.访问修饰符:只能是public或默认。
2.接口名:和类名采用相同命名机制。
3.extends:接口可以多继承。
4.常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
5.方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。
要点
1.子类通过implements来实现接口中的规范。
2.接口不能创建实例,但是可用于声明引用变量类型。
3.一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
4.JDK1.8(不含8)之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
5.JDK1.8(含8)后,接口中可以包含普通的静态方法、默认方法。
接口中定义静态方法和默认方法(JDK8)
JAVA8之前,接口里的方法要求全部是抽象方法。
JAVA8(含8)之后,以后允许在接口里定义默认方法和静态方法。
Java 8及以上新版本,允许给接口添加一个非抽象的方法实现,只需要使用 default 关键字即可,这个特征又叫做默认方法(也称为扩展方法)。
默认方法和抽象方法的区别是抽象方法必须要被实现,默认方法不是。
作为替代方式,接口可以提供默认方法的实现,所有这个接口的实现类都可以得到默认方法。
JDK8新特性_静态方法
JAVA8以后,我们也可以在接口中直接定义静态方法的实现。这个静态方法直接从属于接口(接口也是类,一种特殊的类),可以通过接口名调用。
如果子类中定义了相同名字的静态方法,那就是完全不同的方法了,直接从属于子类。可以通过子类名直接调用。
接口的多继承
接口支持多继承。和类的继承类似,子接口extends父接口,会获得父接口中的一切。
interface A {
void testa();
}
interface B {
void testb();
}
/**接口可以多继承:接口C继承接口A和B*/
interface C extends A, B {
void testc();
}
public class Test implements C {
public void testc() {
}
public void testa() {
}
public void testb() {
}
}
字符串String类详解
String是最常用的类,要掌握String类常见的方法,它底层实现也需要掌握好,不然在工作开发中很容易犯错。
1.String类又称作不可变字符序列。
2.String位于java.lang包中,Java程序默认导入java.lang包下的所有类。
3.Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。
4.Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。
String类和常量池
常量池分了以下三种:
1. 全局字符串常量池
2. class文件常量池
3. 运行时常量池(Runtime Constant Pool)
String类的常用方法列表
方法 解释说明
char charAt(int index) 返回字符串中第index个字符
boolean equals(String other) 如果字符串与other相等,返回true;否 则,返回false。
boolean equalsIgnoreCase(String other) 如果字符串与other相等(忽略大小写),则 返回true;否则,返回false。
int indexOf(String str) 返回从头开始查找第一个子字符串str在字符 串中的索引位置。如果未找到子字符串str, 则返回-1。
lastIndexOf() 返回从末尾开始查找第一个子字符串str在字 符串中的索引位置。如果未找到子字符串str, 则返回-1。
int length() 返回字符串的长度。
String replace(char oldChar,char newChar) 返回一个新串,它是通过用 newChar 替换此 字符串中出现的所有oldChar而生成的。
boolean startsWith(String prefix) 如果字符串以prefix开始,则返回true。
boolean endsWith(String prefix) 如果字符串以prefix结尾,则返回true。
String substring(int beginIndex) 返回一个新字符串,该串包含从原始字符串 beginIndex到串尾。
String substring(int beginIndex,int endIndex) 返回一个新字符串,该串包含从原始字符串 beginIndex到串尾或endIndex-1的所有字符。
String toLowerCase() 返回一个新字符串,该串将原始字符串中的所 有大写字母改成小写字母。
String toUpperCase() 返回一个新字符串,该串将原始字符串中的所 有小写字母改成大写字母。
String trim() 返回一个新字符串,该串删除了原始字符串头 部和尾部的空格。
字符串相等的判断
1.equals方法用来检测两个字符串内容是否相等。如果字符串s和t内容相等,则s.equals(t)返回true,否则返回false。
2.要测试两个字符串除了大小写区别外是否是相等的,需要使用equalsIgnoreCase方法。
3.判断字符串是否相等不要使用==。
内部类
我们把一个类放在另一个类的内部定义,称为内部类(inner class)。
内部类的两个要点:
1.内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
2.内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。但外部类不能访问内部类的内部属性。
注意:
内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。
所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。
非静态内部类
1.非静态内部类对象必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。
2.非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。
3.非静态内部类不能有静态方法、静态属性和静态初始化块。
4.成员变量访问要点:
(1)内部类属性:this.变量名。
(2)外部类属性:外部类名.this.变量名。
内部类的访问:
\1. 外部类中定义内部类:new Inner()。
\2. 外部类以外的地方使用非静态内部类:Outer.Inner varname = new Outer().new Inner()。
静态内部类定义方式:
static class ClassName {
//类体
}
使用要点:
1.静态内部类可以访问外部类的静态成员,不能访问外部类的普通成员。
2.静态内部类看做外部类的一个静态成员。
匿名内部类:适合那种只需要使用一次的类。比如:键盘监听操作等等。在安卓开发、awt、swing开发中常见。
语法:
new 父类构造器(实参类表) \实现接口 () {
//匿名内部类类体!
}
注意
1.匿名内部类没有访问修饰符。
2.匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。
局部内部类
1.定义在方法内部的,作用域只限于本方法,称为局部内部类。
2.局部内部类在实际开发中应用很少。