1、面向对象
面向过程就是分析出实现需求所需要的步骤,通过函数一步一步实现这些步骤,接着依次调用即可。
面向对象是把整个需求按照特点、功能划分,将这些存在共性的部分封装成对象,创建对象不是为了完成某一个步骤,而是描述某个事物在解决问题的步骤中的行为
面向对象(Object-Oriented Programming)是将每一个步骤抽象为行为,便于复用和扩展
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低,类调用时需要实例化,开销比较大,比较消耗资源
2、四大特征
1、封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。
高内聚 :类的内部数据操作细节自己完成,模块内部要高度聚合,单一,独立。
低耦合 :仅对外暴露少量的方法用于使用,减少类与类之间依赖关系,使拓展时类改变造成的影响降低。
尽量使用数据耦合,模块之间通过参数来传递数据。
2、继承,继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了软件的可重用性和可扩展性。
父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私的结构。只因为封装性的影响,使得子类不能直接调用父类的结构而已。
3、多态,多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
编译,看左边;运行,看右边。
Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;
而对于多态,只等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。
在运行时根据实际情况决定调用函数
多态存在的三个必要条件:
继承
重写(子类继承父类后对父类方法进行重新定义)
父类引用指向子类对象 向上转型
4、抽象,抽象包括两个方面:一个是数据抽象,一个是过程抽象。数据抽象也就是对象的属性。过程抽象是对象的行为特征。在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类。接口和抽象类为我们提供了一种将接口与实现分离的更加结构化的方法。
如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰,抽象类可不包含抽象方法。
现实逻辑,面向对象,设计,内存都不希望我们实例化抽象类,所以 编译器规定不能实例化。
抽象类中可以有main方法,一定有构造器
可以用抽象类来实现接口,这个时候就不需要实现接口的所有方法了
抽象类不能直接通过new去实例化一个对象,那它就是不能实例化,要获取抽象类的对象, 需要先用一个类继承抽象类, 然后去实例化子类。也可以用匿名内部类,在抽象类中创建一个匿名的子类,继承抽象类,通过特殊的语法实例化子类的对象。
1、抽象类可以没有抽象方法,也可以有普通方法,接口中不能有实例方法去实现业务逻辑,;
2、抽象方法不能声明为静态,抽象方法只需声明无需实现,没有主体,普通方法有主体,抽象类不能被final修饰,因为被final修饰的类无法被继承;
2、抽象类的子类必须实现父类的抽象方法,否则该子类也是抽象类;
3、抽象类可以有构造方法,被继承时子类必须继承父类的一个构造方法;
3、面向对象基础
1、构造器
如果在类中你提供了其他有参的构造器,则编译器不会提供默认的无参构造器。
如果父类有无参构造方法,子类构造器可以可以不写super(),隐式调用,如果父类没有无参构造方法,则要求子类构造方法必须显式直接或间接指定调用父类哪个构造方法并且放在有效代码的第一行。
4、一些关键字
1、static
用来修饰成员变量和成员方法,也可以形成静态static代码块。静态方法中不能用this和super关键字,初始化静态方法方法时对象还未实例化,不能直接访问所属类的实例变量和实例方法。
通过一个指向子类对象的父类引用变量来调用父子同名的静态方法时,只会调用父类的静态方法,静态方法可以继承,子类不能重写,只能被隐藏。
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
对于实例变量,每创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。
2、interface
方法被隐式指定为public abstract(JDK1.8之前),接口中的变量被隐式的指定为public static final,只能是public, JDK1.8接口除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法。1.9可以有私有方法,私有静态方法。
如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没重写此方法的情况下,默认调用的是父类中的同名同参数的方法。–>类优先原则
如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,
那么在实现类没重写此方法的情况下,报错。–>接口冲突。
这就需要我们必须在实现类中重写此方法
如何在子类(或实现类)的方法中调用父类、接口中被重写的方法
super.method3();//调用的是父类中声明的
//调用接口中的默认方法
CompareA.super.method3();
接口和抽象类的区别
抽象类是对根源的抽象(这个对象是什么),把多种对象的共有特性抽象出来,一个四不像,对类的抽象,是一种模板设计,接口是对动作的抽象(这个对象能做什么),一种规范。
1、抽象类可以有方法体,有静态代码块,接口不行(1.8之前),1.8可以有静态方法、默认方法,可以有方法体。1.9可以有私有方法,私有静态方法。
2、抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型
3、实现接口的关键字为implements,继承抽象类的关键字为extends。一个类只能继承一个抽象类,可以继承多个接口。
4、接口没有构造器,抽象类有
相同点:不能实例化;都可以包含抽象方法的。
3、访问控制修饰符
4种权限都可以用来修饰类的内部结构:属性、方法、构造器、内部类
修饰类的话,只能使用:缺省、public
private
以应用于类、方法或字段(在类中声明的变量)。 只能在声明 private(内部)类、方法或字段的类中引用这些类、方法或字段。在类的外部或者对于子类而言,它们是不可见的。
protected
可以在声明 protected 类、方法或字段的类、同一个包中的其他任何类以及任何子类(无论子类是在哪个包中声明的)中引用这些类、方法或字段。
protected相对于default,可以不同包子类进行访问,default不同包子类不可继承。
public
其他任何类或包中引用 public 类、方法或字段
protected可以访问不同包子类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fNmmlScf-1630412487954)(java基础1/访问控制修饰符.JPG)]
4、final、finally、finalize
final 关键字可以应用于类,以指示不能扩展该类(不能有子类)。final 关键字可以应用于方法,以指示在子类中不能重写此方法,修饰变量,变量不可变。String类就是final的。
static final 用来修饰属性:全局常量
finally,一种异常处理机制,是异常处理语句结构的一部分,表示总是执行。
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。
5、关键字
native关键字可以应用于方法,以指示该方法是用 Java 以外的语言实现的。
super关键字用于引用使用该关键字的类的超类。
我们可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定的构造器
"super(形参列表)"的使用,必须声明在子类构造器的首行!
我们在类的构造器中,针对于"this(形参列表)"或"super(形参列表)“只能二一,不能同时出现
在构造器的首行,没显式的声明"this(形参列表)“或"super(形参列表)”,则默认调用的是父类中空参的构造器:super()
在类的多个构造器中,至少一个类的构造器中使用了"super(形参列表)”,调用父类中的构造器
this关键字用于引用当前实例。
transient,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。
volatile关键字用于表示可以被多个线程异步修改的成员变量。保证可见性和有序性。 Volatile的写 和 读的时候,加入屏障,防止出现指令重排的。
instanceof关键字用来确定对象所属的类。
a instanceof A:判断对象a是否是类A的实例。如果是,返回true;如果不是,返回false。
向下转型使用
6、异常处理
编译时异常:执行javac.exe命名时,可能出现的异常,受检异常(检查时异常),必须处理,例如io,连接数据库。
运行时异常:执行java.exe命名时,出现的异常,不受检异常,不强制要求处理,运行时由系统抛出。
java处理异常中,把可能出现的异常代码放入到try块中,用于监听,由catch来进行捕获异常,而将必须输出的信息,放到的finally语句中。
throw 关键字用于引发异常。 Try至少需要一个catch或一个finally.finally执行完后才去执行try catch中的return或throw.
throw 语句将 java.lang.Throwable 作为参数。Throwable 在调用栈中向上传播,直到被适当的 catch 块捕获。 只能抛出一个异常对象名。
方法:
throw new Exception()
调用者:
catch (Exception e) {
e.printStackTrace();//处理异常
}
throws 声明一个异常可能被抛出,后面跟异常类,且可以跟多个异常类
throws 关键字可以应用于方法,以便指出方法引发了特定类型的异常。 throws用来声明一个方法可能产生的所有异常,不做任何处理而是将异常往上传,谁调用我我就抛给谁。
有两种方式进行处理throws:
1.使用try-catch捕获异常
2.使用throws继续声明,如果调用者不打算处理此异常,可以继续通过throws声明异常,让上一级调用者处理异常。main()方法声明的异常将由Java虚拟机来处理
throw和throws抛出异常时,抛出异常的方法并不负责处理,顾名思义,只管抛出,由调用者负责处理。
throws和throw一起使用,throw是告诉异常信息。
public static int divide(int x,int y) throws DivideDivideByMinusException
{
if(y<0)
{
throw new DivideDivideByMinusException("被除数是负数");
}
int result = x/y;
return result;
}
1、throws出现在方法函数头;而throw出现在函数体。
2、throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常。
3、throws抛出异常时,它的上级(调用者)也要申明抛出异常或者捕获,不然编译报错。而throw的话,可以不申明或不捕获(这是非常不负责任的方式)但编译器不会报错。
RuntimeException 类及其子类运行异常,运行时异常一般是程序中的逻辑错误引起的,在程序运行时无法修复。例如 数据取值越界。编译时异常自己检查。
Throwable类:所有异常类型都是Throwable类的子类,它有两个派生类,分别是Error和Exception
Error类:表示仅靠程序本身无法恢复的严重错误,如内存溢出、虚拟机错误。应用程序不应该抛出这种类型的对象(一般由虚拟机抛出)
Exception类:由java应用程序抛出和处理的非重要错误,如数组下标越界、类型转换等。它的不同的子类分别对应不用类型的异样。
体会开发中应该如何选择两种处理方式?
如果父类中被重写的方法没throws方式处理异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方法中异常,必须使用try-catch-finally方式处理。
执行的方法a中,先后又调用了另外的几个方法,这几个方法是递进关系执行的。我们建议这几个方法使用throws的方式进行处理。而执行的方法a可以考虑使用try-catch-finally方式进行处理。
5、一些知识点和类
知识点
如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值。
如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值。
java没有引用传递,只有值传递。
Java中使用的求值策略就是传共享对象调用(共享对象传递),也就是说,Java会将对象的地址的拷贝传递给被调函数的形式参数。
代码块要是使用修饰符,只能使用static
实例化子类对象时,涉及到父类、子类中静态代码块、非静态代码块、构造器的加载顺序:由父及子,静态先行。
属性:加载到堆空间中 (非static)
局部变量:加载到栈空间
浅拷贝拷贝引用的值,同一对象,深拷贝新建拷贝的对象,不同对象。
基本数据类型存放位置
基本数据类型是放在栈中还是放在堆中,取决于基本类型声明的位置。
在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因。
在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。
String
string
final 声明的常量类,不能被任何类所继承,而且一旦一个String对象被创建, 包含在这个对象中的字符序列是不可改变的, 包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁。
一个 String 字符串实际上是一个 char 数组。
常量池:Java运行时会维护一个String Pool(String池), 也叫“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。
String str = new String(“xyz”);如果常量池没有xyz,会创建两个对象,否则一个。
String str1 = "hello";
String str2 = "helloworld";
String str3 = str1+"world";//编译器不能确定为常量(会在堆区创建一个String对象)
String str4 = "hello"+"world";//编译器确定为常量,直接到常量池中引用
System.out.println(str2==str3);//fasle
System.out.println(str2==str4);//true
String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
System.out.println(str1==str2);//true
System.out.println(str1==str3);//fasle
System.out.println(str2==str3);//fasle
当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,若存在则直接返回常量池中相应Strnig的引用;若不存在,jdk1.7后在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用.jdk8中字符串常量池存放在堆中.字符串常量池保存字符串的引用。
String str1 = "hello";//字面量 只会在常量池中创建对象,常量池的引用
String str2 = str1.intern();
System.out.println(str1==str2);//true
String str3 = new String("world");//new 关键字只会在堆中创建对象
String str4 = str3.intern();
System.out.println(str3 == str4);//false
String str5 = str1 + str2;//变量拼接的字符串,会在常量池中和堆中都创建对象,JVM会先创建一个String对象,常量池是str1和str2,通过String.append()方法将str1与str2的值拼接,然后通过String.toString()返回一个堆中的String对象的引用,赋值给str5
String str6 = str5.intern();//这里由于堆中有对象了,池中没对象,直接返回的是引用,也这份引用指向堆中的对象
System.out.println(str5 == str6);//true
String str7 = "hello1" + "world1";//常量拼接的字符串,只会在常量池中创建对象
String str8 = str7.intern();
System.out.println(str7 == str8);//true
String s9 = new String("1") + newString("1")//这行代码在字符串常量池中生成“1” ,并在堆空间中生成s9引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
采用new关键字新建一个字符串对象时,JVM首先在字符串池中查找有没有"aaa"这个字符串对象,如果有,则不在池中再去创建"aaa"这个对象了,直接在堆中创建一个"aaa"字符串对象,然后将堆中的这个"aaa"对象的地址返回赋给引用str3,这样,str3就指向了堆中创建的这个"aaa"字符串对象;如果没有,则首先在字符串池中创建一个"aaa"字符串对象,然后再在堆中创建一个"aaa"字符串对象,然后将堆中这个"aaa"字符串对象的地址返回赋给str引用.
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);//true,final变量在编译后会直接替换成对应的值
一些例子
value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实的数据,只要能够操作堆中的数组,依旧能改变数据。而且 value 是基本类型构成,那么一定是可变的,即使被声明为 private,我们也可以通过反射来改变。
String,StringBuilder,StringBuffer三者的区别
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,如下就是,可知这两种对象都是可变的。char[] value;
运行速度快慢为:StringBuilder > StringBuffer > String
StringBuilder由于没有加锁释放锁导的过程,执行速度上比StringBuffer要快。
StringBuilder是线程不安全的,而StringBuffer,String是线程安全的
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
object类
Object类是所Java类的根父类
方法:equals() / toString() / getClass() /hashCode() / clone() / finalize()
wait() 、 notify()、notifyAll()
equals()只能适用于引用数据类型.比较内容需要重写。
== :运算符
可以使用在基本数据类型变量和引用数据类型变量中
如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。(不一定类型要相同)
如果比较的是引用数据类型变量:比较两个对象的地址值是否相同.即两个引用是否指向同一个对象实体
总结:判断两个对象相等,重写equal方法,判断内容相等,否则和==一样判断地址是否相等,是否引用同一对象。
1、两个对象,如果a.equals(b)==true,那么a和b是否相等?
相等,但地址不一定相等。
2、两个对象,如果hashcode一样,那么两个对象是否相等?
不一定相等,判断两个对象是否相等,需要判断equals是否为true。
两个对象hashcode不同,一定不是同一对象,可能两个不同对象,内容相等。
一般equal和hashcode都会并且一起被重写。
Type类
Type是Java语言中所有类型的公共父接口,实现了Type接口的子接口为GenericArrayType(泛型数组类型),ParmeterizedType(参数化类型),TypeVariable(类型变量),WildcardType(通配符类型)。实现了Type接口的子类有Class(类),属于原始类型,是Java反射的基础,对Java类的抽象;在程序运行期间,每一个类都对应一个Class对象,这个对象包含了类的修饰符、方法,属性、构造等信息,所以我们可以对这个Class对象进行相应的操作,这就是Java的反射。Type也主要是为在运行时期获取泛型而服务,解决泛型擦除问题。
原始类型,不仅仅包含我们平常所指的类,还包括枚举、数组、注解等;
参数化类型,就是我们平常所用到的泛型List、Map;
数组类型,并不是我们工作中所使用的数组String[] 、byte[],而是带有泛型的数组,即T[] ;
基本类型,也就是我们所说的java的基本类型,即int,float,double等
包装类
基本数据类型<—>包装类:JDK 5.0 新特性:自动装箱 与自动拆箱
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
Integer total = 99;
执行上面那句代码的时候,系统为我们执行了:
Integer total = Integer.valueOf(99);
int totalprim = total;
执行上面那句代码的时候,系统为我们执行了:
int totalprim = total.intValue();
基本数据类型、包装类—>String:调用String重载的valueOf(Xxx xxx)
String—>基本数据类型、包装类:调用包装类的parseXxx(String s)
内部类
匿名内部类使用有参构造器时,()传参,调用父类构造器。编译后和正常类一样。
成员内部类(静态、非静态 ) vs 局部内部类(方法内、代码块内、构造器内)
//创建静态的Dog内部类的实例(静态的成员内部类):
Person.Dog dog = new Person.Dog();
//创建非静态的Bird内部类的实例(非静态的成员内部类):
//Person.Bird bird = new Person.Bird();//错误的
Person p = new Person();
Person.Bird bird = p.new Bird();
POJO和JavaBean
“Plain Ordinary Java Object”,简单普通的java对象。这个类没有实现/继承任何特殊的java接口或者类,不遵循任何主要java模型,约定或者框架的java对象。在理想情况下,POJO不应该有注解。普通的JavaBeans,支持业务逻辑的协助类。
JavaBean符合一定规范编写的Java类。一个可重用的组件,它的方法命名,构造及行为必须符合特定的约定:
-
所有属性为private。
-
这个类必须有一个公共的缺省构造函数。即是提供无参数的构造器。
-
这个类的属性使用getter和setter来访问,其他方法遵从标准命名规范。
-
这个类应是可序列化的。实现serializable接口。
因为这些要求主要是靠约定而不是靠实现接口,所以许多开发者把JavaBean看作遵从特定命名约定的POJO。
POJO主要用于数据的临时传递,它只能装载数据, 作为数据存储的载体,而不具有业务逻辑处理的能力。
Java集合框架
概述
Java中的集合类可以分为两大类:一类是实现Collection接口,存储一个元素集合;另一类是实现Map接口,图(Map),存储键/值对映射。java集合框架位于java.util包中。
Iteraor接口
(迭代器接口)用于遍历集合中元素的接口,主要包含三种方法:
boolean hasNext()
E next()
void remove()
Iterable中封装了Iterator接口,为了使实现类可以使用foreach进行迭代。
因为Iterator接口的核心方法next()或者hasNext() 是依赖于迭代器的当前迭代位置的。 如果Collection直接实现Iterator接口,势必导致集合对象中包含当前迭代位置的数据(指针)。 当集合在不同方法间被传递时,由于当前迭代位置不可预置,那么next()方法的结果会变成不可预知。 除非再为Iterator接口添加一个reset()方法,用来重置当前迭代位置。 但即时这样,Collection也只能同时存在一个当前迭代位置。 而Iterable则不然,每次调用都会返回一个从头开始计数的迭代器。 多个迭代器是互不干扰的。
集合Collection、List、Set都是Iterable的实现类,所以他们及其他们的子类都可以使用foreach进行迭代。
collection
List接口
介绍
是一个有序, 元素可重复的 Collection。
——ArrayList:线程不安全,查询速度快。底层都是基于数组来储存集合元素,封装了一个动态的Object[]数组,是一种顺序存储的线性表。
——Vector:线程安全,但速度慢,已被ArrayList替代。
——LinkedList:线程不安全,增删速度快,查询慢,没有同步方法,是一个链式存储的线性变,本质上是一个双向链表。
增:add(Object obj)
删:remove(int index) / remove(Object obj)
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele)
长度:size()
遍历:
① Iterator迭代器方式
② 增强for循环
③ 普通的循环
ArrayList的源码分析
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没创建长度为10的数组
list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
LinkedList源码分析
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
linkedlist查询
public E get(int index) {//得到元素
checkElementIndex(index);
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;//返回节点
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
LinkedList的查询逻辑
根据传入的index去判断是否为LinkedList中的元素,判断逻辑为index是否在0和size之间,如果在则调用node(index)方法,否则抛出IndexOutOfBoundsException;
调用node(index)方法,将size右移1位,即size/2,判断传入的size在LinkedList的前半部分还是后半部分
如果在前半部分,即index < size/2,则从fisrt节点开始遍历匹配
如果在后半部分,即index > size/2,则从last节点开始遍历匹配。
linkedlist删除
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {//x是首元素
first = next;
} else {
prev.next = next;
x.prev = null;//置空
}
if (next == null) {//x是尾元素
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
Set接口
介绍
存储唯一,无序的对象。
无序性不等于随机性
HashSet 基于HashMap来实现的,是一个不允许有重复元素的集合,允许有null值,线程不安全。在HashSet插入的对象都需要实现hashCode和equals方法,只有当hashcode相同和equal返回为true的时候才认为对象是相等的。进行修改只能先删后增。
//构造方法java
public HashSet() {
map = new HashMap<>();
}
//关注的只是key元素, 所有 value元素默认为 Object类对象.
元素添加过程:(以HashSet为例)
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置,判断
数组此位置上是否已经元素:
如果此位置上没其他元素,则元素a添加成功。 —>情况1
如果此位置上其他元素b(或以链表形式存在的多个元素,则比较元素a与元素b的hash值:
如果hash值不相同,则元素a添加成功。—>情况2
如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功。—>情况2
TreeSet
TreeSet:有序的存放,线程不安全,可以对Set集合中的元素进行排序,由红黑树来实现排序,TreeSet实际上也是SortedSet接口的子类,其在方法中实现了SortedSet的所有方法,并使用comparator()方法进行排序。
TreeSet:
1.自然排序中,比较两个对象是否相同的标准为:compareTo()返回0.不再是equals().
2.定制排序中,比较两个对象是否相同的标准为:compare()返回0.不再是equals().
//方式一:自然排序
@Test
public void test1(){
TreeSet set = new TreeSet();
//举例二:
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
//方式二:定制排序
@Test
public void test2(){
Comparator com = new Comparator() {
//照年龄从小到大排列
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com);
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Mary",33));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
LinkedHashSet:底层由链表实现,按照元素插入的顺序进行迭代,即迭代输出的顺序与插入的顺序保持一致,底层数据结构由哈希表和链表组成。
map
Map保存的每项数据都是key-value对,Map里的key是不可重复的
数组:采用一段连续的存储单元来存储数据。
哈希表的主干就是数组。
Map中的entry:无序的、不可重复的,使用Set存储所的entry。
二叉排序树和红黑树
二叉排序树:(三种,没有等于,左等,右等)
1.左子树上所有结点的值均小于或等于它的根结点的值。
2.右子树上所有结点的值均大于它的根结点的值。
3.左、右子树也分别为二叉排序树。
红黑树:(自平衡的二叉排序树)
1.节点是红色或黑色。
2.根节点是黑色。
3.每个叶子节点都是黑色的空节点(NIL节点)。
4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
左旋中的“左”,意味着“被旋转的节点将变成一个左节点”。红黑树的插入、删除、查找各种操作性能都比较稳定,大量数据需要插入或者删除时,AVL需要rebalance的频率比之高。
HashMap
HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。JDK1.8里当链表超过8数组长度大于等于64时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能。
HashMap在jdk7中实现原理:
HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table。
…可能已经执行过多次put…
map.put(key1,value1):
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
如果equals()返回false:此时key1-value1添加成功。----情况3
如果equals()返回true:使用value1替换value2。
元素必须重写equal和hashcode
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来。
HashMap在jdk8中相较于jdk7在底层实现方面的不同:
new HashMap():底层没创建一个长度为16的数组
1、jdk 8底层的数组是:Node[],而非Entry[]
2、首次调用put()方法时,底层创建长度为16的数组
3、jdk7底层结构只:数组+链表。jdk8中底层结构:数组+链表+红黑树。
形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
HashMap底层典型属性的属性的说明:
DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap.
区别就在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node.
collections和collection
Collection 是一个集合接口,
Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。
例如Collections.synchronizedXXX(collection)
这个工厂方法封装了指定的集合并返回了一个线程安全的集合。XXX可以是Collection、List、Map、Set、SortedMap和SortedSet的实现类。
collections常用方法
reverse(List):反转 List 中元素的顺序
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所旧值
Collection集合与数组间的转换
//集合 --->数组:toArray()
Object[] arr = coll.toArray();
for(int i = 0;i < arr.length;i++){
System.out.println(arr[i]);
}
//拓展:数组 --->集合:调用Arrays类的静态方法asList(T ... t)
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(list);
List arr1 = Arrays.asList(new int[]{123, 456});
System.out.println(arr1.size());//1
List arr2 = Arrays.asList(new Integer[]{123, 456});
System.out.println(arr2.size());//2
遍历的实现
Iterator iterator = coll.iterator();
//hasNext():判断是否还下一个元素
while(iterator.hasNext()){
//next():①指针下移 ②将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}
Collections工具类支持两种排序方法:
Collections.sort(List<T> list);
Collections.sort(List<T> list, Comparator<? super T> c)
Java比较器
对于Comparable接口来说,自然排序,它往往是进行比较类需要实现的接口,它仅包含一个有compareTo()方法,只有一个参数,返回值为int,返回值大于0表示对象大于参数对象;小于0表示对象小于参数对象;等于0表示两者相等
对于Comparator接口来说,它的实现者被称为比较器,它包含一个compare()方法,有两个参数,返回值与Comparable的compareTo()方法一样,不同之处是Comparator接口一般不会被集合元素类所实现,而是单独实现或者匿名内部类方式实现。
例子:
Arrays.sort(goods,com);
Collections.sort(coll,com);
new TreeSet(com);
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”(自定义比较器)。实现了Comparable接口,就意味着“该类支持排序”。
jdk官方默认是升序,是基于:
<return -1
= return 0
>return 1
如果要降序就必须完全相反:
<return 1
= return 0
>return -1
代码示例
public class Goods implements Comparable{
private String name;
private double price;
//指明商品比较大小的方式:照价格从低到高排序,再照产品名称从高到低排序
@Override
public int compareTo(Object o) {
// System.out.println("**************");
if(o instanceof Goods){
Goods goods = (Goods)o;
//方式一:
if(this.price > goods.price){
return 1;
}else if(this.price < goods.price){
return -1;
}else{
// return 0;
return -this.name.compareTo(goods.name);
}
//方式二:
// return Double.compare(this.price,goods.price);
}
// return 0;
throw new RuntimeException("传入的数据类型不一致!");
}
// getter、setter、toString()、构造器:省略
}
Comparator com = new Comparator() {
//指明商品比较大小的方式:照产品名称从低到高排序,再照价格从高到低排序
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Goods && o2 instanceof Goods){
Goods g1 = (Goods)o1;
Goods g2 = (Goods)o2;
if(g1.getName().equals(g2.getName())){
return -Double.compare(g1.getPrice(),g2.getPrice());
}else{
return g1.getName().compareTo(g2.getName());
}
}
throw new RuntimeException("输入的数据类型不一致");
}
}
JVM
1、Java类加载过程
JVM将类描述数据从.class文件中加载到内存,并对数据进行,解析和初始化,最终形成被JVM直接使用的Java类型。 类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、连接(验证、准备、解析)、初始化、使用和卸载七个阶段。
加载的类信息被存放于一块称为方法区的内存空间。
类加载一次,实例化可多次。
加载
加载(Loading)就从文件系统或者网络中查找并把类文件.class文件读入Java虚拟机中; (加载到内存,存放到方法区。)
在加载阶段, 虚拟机需要完成以下三件事情
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2 )将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3 ) 将类的class文件读入内存,并为之创建一个java.lang.Class对象(在堆中),也就是说当程序中使用任何类时,系统都会(在堆中)为之建立一个java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。
连接
连接(Linking)就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去 。连接阶段分为三个子步骤——验证(Verification)、准备(Preparation)和解析(Resolution)。
- 验证步骤确保了Java类型数据格式正确并且适于Java虚拟机使用。四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
- 准备步骤则负责为该类型分配它所需的内存、比如为它的类变量分配内存,并设置默认初始值,这时候进行内存分配的仅包括类, 实例变量会随着对象一起分配到java堆中。(这里不包含final修饰的static,因为final在编译时候就会分配了,准备阶段会显示初始化。)这些内存都将在方法区中进行分配(类变量在方法区)。
- 解析步骤则负责把常量池中的符号引用转换为直接引用。(符号引用是用一组符号描述所引用的目标;直接引用是指向目标的指针),解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。
常量池中保存的是一个 Java 类引用的一些常量信息,包含一些字符串常量及对于类的符号引用信息等。Java 代码编译生成的类文件中的常量池是静态常量池,当类被载入到虚拟机内部的时候,在内存中产生类的常量池叫运行时常量池。
初始化
- 这个初始化是类的初始化,不是实例的初始化。为类的静态变量赋予正确的初始值。
- 初始化阶段就是执行类构造方法()的过程,此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
- 虚拟机必须保证一个类的()方法在多线程下被同步加锁。
接口也有初始化过程,和类是一致的。不过接口中不能使用“static{}”语句块,但编译器仍然会为接口生成“clinit()”类构造器,用于初始化接口中所定义的成员变量。一个接口被初始化时,它的父接口并不要求被初始化。
总结:
1、父类的静态变量和静态块赋值(按照声明顺序)
2、自身的静态变量和静态块赋值(按照声明顺序)
3、main方法
4、父类的成员变量和块赋值(按照声明顺序)
5、父类构造器赋值
6、自身成员变量和块赋值(按照声明顺序)
7、自身构造器赋值
8、静态方法和实例方法只有在调用的时候才会去执行
一定会发生类的初始化(加载)事件(类的主动引用)
(1)new一个类的对象。
(2)使用类的静态成员(除了final常量)和静态方法。
(3)使用java.lang.reflect包的方法对类进行反射调用(例如:Class.forName(“com.shxt.A”).
(4)虚拟机启动,java Hello,则一定会初始化Hello类,说白了就是先启动main方法所在类。
(5)当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类。
注:类的被动引用,不会发生类的初始化(加载)
(1)通过数组定义类引用,不会发生此类的初始化(例如A [] arr = new A[10]).
(2) 使用类的常量不会触发此类的初始化(常量在编译阶段就存入常量池中了)
(3)当访问一个静态变量时,只有真正声明这个变量的类才会被初始化。
public static void main(String [] args){
System.out.println(B.width);
}
class B extends A{
}
class A {
public static int width=100;
}
//此时,A会被初始化,但B不会被初始化。
类加载器的分类
- JVM支持两种类型的类加载器,分别为 引导类加载器(Bootstrap ClassLoader) 和自定义类加载器(User-Defined ClassLoader)。
- 将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
启动类加载器(引导类加载器,Bootstrap ClassLoader):
- 这个类加载器是使用C/C++语言实现的,嵌套在JVM内部,在java中获取不到。
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
- 并不继承自java.lang.ClassLoader,没有父加载器。
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类。
- 父类加载器为启动类加载器。
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动被扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader)
- Java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类。
- 父类加载器为扩展类加载器。
- 它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库。
- 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
双亲委派机制:
1、如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行;
2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3、如果父类加载器能完成类加载任务,则返回成功;倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。
并不是所有的类加载器都采用双亲委托机制。
tomcat服务器类加载器也使用代理模式,所不同的是它首先尝试自己去加载这个类,如果找不到在代理给父类加载器,这与一般的类加载器的顺序是相反的。
正常情况下,同一个类文件被同一个类加载器对象只能加载一次,不过我们可以通过Unsafe的defineAnonymousClass来实现同一个类文件被同一个类加载器对象加载多遍的效果,因为并没有将其放到SystemDictonary里,因此我们可以无穷次加载同一个类。
2、JVM内存模型及知识点
JVM概念
java运行时环境,运行在操作系统之上的,它与硬件没有直接的交互。Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
jvm和jmm的区别
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,对于所有线程都是共享的。为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。
volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。volatile禁止指令重排。
好处:
- 一次编写到处运行
- 自动内存管理,垃圾回收
- 数组下标越界检查
- 多态
JDK中包含JRE和编译器javac和工具Javadoc等,在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
JRE:是java程序的运行环境,它包含JVM。
三者的关系:JDK(JRE(JVM))
jvm是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的,够运行Java字节码(Java bytecode)的虚拟机。
内存模型
程序计数器:
-
定义: 一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码,这样线程切换后就可以知道从哪里开始执行了),由执行引擎读取下一条指令。
-
作用: 记住下一条jvm指令的执行地址,用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。。
Java虚拟机栈:栈内存,主管Java程序的运行,描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表(通常所说的栈内存,8大基本数据类型,对象引用)、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,对应着一个栈帧在虚拟机中入栈到出栈的过程。
虚拟机栈的特点
内存小,跨平台性,可以少量存储一些变量,内存地址,用来管理Java方法的调用。基本数据类型、对象的引用都存放在这。
栈也就是方法,一个线程就是一个栈,所以说是线程安全的,是线程私有的—>声明周期与线程同步
先进后出(执行完就出)栈帧相当于执行方法,调用完就出
因为栈比较小,栈不存在GC问题(OOM)
本地方法栈和虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机执行Native方法服务的。一个Native Method就 是一个Java调用非Java代码的接口。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
堆:
- 线程共享,堆中的对象都存在线程安全的问题。
- 垃圾回收,垃圾回收机制重点区域。
根据垃圾回收的划分,逻辑上将堆划分为:
- 新生代Young Generation
- Eden伊甸园
- 幸存区Survivor From
- 幸存区Survivor To
- 老年代Tenure generation
- JDK7之前为Permanent永久区,JDK8之后为元空间
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据(metadata) 空间并不在虚拟机中,而是使用本地内存。方法区(线程共享)主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。**(类信息和运行时常量池),方法存在于方法区,存放的是代码,方法区是方法的静态表现,栈区栈帧是动态。元空间与堆不相连。
堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。1.8后,字符串常量池从永久代中剥离出来,存放在堆中。
JVM内存指的是堆内存。
堆外内存也被称为直接内存,直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分, 也不是Java虚拟机规范中定义的内存区域, 但是这部分内存也被频繁地使用, 而且也可能导致OutOfMemoryError异常出现。常见于 NIO 操作时,用于数据缓冲区
创建对象的步骤有六种:
1、判断对象对应的类是否经过类加载子系统加载过
2、为对象分配内存
3、处理并发安全的问题(TLAB)(CAS)
4、初始化分配到空间
5、设置对象的对象头
6、执行init方法进行初始化
对象头里面有什么?
类型指针—>指向类源数据,确定对象所属类型
运行时元数据
Java解释器:是JVM的一部分,将字节码文件翻译为对应平台的机器指令,能够把高级编程语言一行一行直接转译运行。
Java编译器:将java源代码编译成中间代码字节码文件,字节码文件是一种和任何具体机器环境及操作系统环境无关的中间代码,它是一种二进制文件。
Java为什么跨平台:因为Java程序编译之后的代码不是能被硬件系统直接运行的代码,而是一种“中间码”——字节码。然后不同的硬件平台上安装有不同的Java虚拟机(JVM),由JVM来把字节码再“翻译”成所对应的硬件平台能够执行的代码。Java之所以能跨平台,是因为java虚拟机能跨平台。java代码不是直接运行在CPU上,而是运行在java虚机(简称JVM)上的。cpu的指令集不同, 不同平台编译出来的结果格式都不同。
3、Java回收机制
由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。
Java堆是进行垃圾回收的主要区域,故其也被称为GC堆
判断是否回收
那些内存需要回收?(对象是否可以被回收的两种经典算法: 引用计数法 和 可达性分析算法)
任何引用计数为0的对象实例可以被当作垃圾收集,很难解决对象之间相互循环引用的问题。
可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
对于可达性分析算法而言,未到达的对象并非“非死不可”,若要宣判一个对象死亡,至少要经历两次标记阶段。
1.如果对象在进行可达性分析的时候发现没有GC ROOTS有引用链,会被进行第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视为不必要执行该对象的finalize方法,即该对象会被回收。反之,该对象会被放置在一个FQueueu的队列中,而之后会有虚拟机自动创建的、优先级低的Finalzer线程去执行,而虚拟机不需要等待该线程结束。
2、在Finalzer对象中进行第二次标记,如果对象在finalize方法中拯救了自己,比如把this关键字赋值给其他变量,与GC ROOTS关联上了引用链,那么在第二次标记的时候就会从即将回收的集合中移除。如果对象没有 拯救自己,就会被回收
GC 管理的区域是 Java 堆,虚拟机栈、方法区和本地方法栈不被 GC 所管理,因此选用这些区域内引用的对象作为 GC Roots,是不会被 GC 所回收的。
java中可作为GC Root的对象有
1.虚拟机栈中引用的对象(本地变量表)
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)
四种引用
引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。
强引用:如果一个对象具有强引用,那垃圾收器绝不会回收它。例如引用New的对象都是强引用。
软引用:用来描述一些还有用但并非必须的对象。软引用可用来实现内存敏感的高速缓存。
弱引用:无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
虚引用:用来跟踪对象被垃圾回收的活动,虚引用必须和引用队列(ReferenceQueue)联合使用。
垃圾回收机制和算法
什么时候回收? (堆的新生代、老年代垃圾回收时机,MinorGC 和 FullGC)
垃圾回收有两种类型,Minor GC 和 Full GC。
Minor GC:对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。复制算法。
Full GC:也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、System.gc()被显式调用等。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old)。新生代 ( Young ) 又被划分为三个区域:Eden、FromSpace和ToSpace, 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
年轻代用来存放新近创建的对象,尺寸随堆大小的增大和减小而相应的变化,默认值是保持为堆大小的1/15,
对象更新速度快,在短时间内产生大量的“死亡对象”。
Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。
Jvm区域总体分两类,heap区和非heap区。heap区又分:Eden Space(伊甸园)、Survivor Space(幸存者区)、Tenured Gen(老年代-养老区)。 非heap区又分:Code Cache(代码缓存区)、Jvm Stack(java虚拟机栈)、Local Method Statck(本地方法栈)。
如何回收?(三种经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法 和 七种垃圾收集器)
标记清除
标记:哪些对象可以当成垃圾(不被GC Root间接引用的)+清除(是否标记的那些空间)。没有需要就标记为不需要了。
缺点:效率不高,两个过程效率都不高。会造成内存碎片。空闲的区域都是小碎片,放不了大的对象。空间碎片太多可能会导致后续事宜中无法找到足够的连续内存而提前触发另一次垃圾搜集动作。
标记整理
清除碎片的过程中会把后面的对象往前移到可用的内存
定义:Mark Compact 没有内存碎片
缺点:速度慢
复制
将可用内存划分为两块(两块Survivor区 To和From),每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来半块内存空间一次性清除掉,整理过程。清掉整块的速度非常快,但是浪费内存,一半不可用
即新生代和老年代。不会有内存碎片
缺点:需要占用双倍内存空间,在对象存活率较高的时候,效率有所下降。如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
HotSpot虚拟机GC算法采用分代收集算法:
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
内存泄漏和内存溢出
内存溢出 : out of memory 指程序在申请内存时,没有足够的内存空间供其使用,出现out fo memory 比如申请一个integer 但给它存了long才能存下的数那就是内存溢出
内存泄露 : memory leak 指程序在申请内存后,无法释放已经申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后的结果很严重,无论多少内存,迟早会被占光
即为:不再会被使用的对象的内存不能被回收,就是内存泄露
内存溢出产生的原因:
1.内存中加载的数据量过于庞大,如一次从数据库中取出过多的数据
2.集合类中有对对象的引用,使用完后未清空
3.代码中存在死循环或循环产生过多重复的实体对象
4.使用的第三方软件中的bug
5.启动参数内存值设定的过小
内存溢出的解决方案:
1.修改JVM启动参数,直接增加内存 (-Xms –Xms 参数一定不要忘记加)
2.检查错误日志查看 OutOfMemory 错误前是否有其他异常或错误
3.对代码进行分步运行分析,找出可能发生溢出的位置
虽然Java拥有垃圾回收机制,但同样会出现内存泄露问题
1、 HashMap、Vector 等集合类的静态使用最容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。
private static Vector v = new Vector();
public void test(Vector v){
for (int i = 1; i<100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
}
在 for 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是虽然我们将 o 引用置空,但当发生垃圾回收时,我们创建的 Object 对象也不能够被回收。因为垃圾回收在跟踪代码栈中的引用时会发现 v 引用,而继续往下跟踪就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说,尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。
2、各种资源连接包括数据库连接、网络连接、IO连接等没有显式调用close关闭,不被GC回收导致内存泄露。
3、监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。