1 面向对象进阶
1.1 继承
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属
性和行为,只要继承那个类即可。
- 此处的多个类称为子类(派生类),单独的这个类称为父类(基类或超类)。可以理解为:“子类 is a 父 类”
- 子类继承了父类,就继承了父类的方法和属性。
- 在子类中,可以使用父类中定义的方法和属性,也可以创建新的数据和方法。
- 在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”。
- 类继承语法规则: class Subclass extends SuperClass{ }
- 子类不能直接访问父类中私有的(private)的成员变量和方法。
- Java只支持单继承和多层继承,不允许多重继承
例:
public class Creature {
public void breath(){
System.out.println("呼吸");
}
}
public class Person extends Creature{
String name;
private int age;
public Person(){
}
public Person(String name,int age){
this.name = name;
this.age = age;
}
public void eat(){
System.out.println("吃饭");
sleep();
}
private void sleep(){
System.out.println("睡觉");
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Student extends Person{
// String name;
// int age;
String major;
public Student(){
}
public Student(String name,int age,String major){
this.name = name;
// this.age = age;
setAge(age);
this.major = major;
}
// public void eat(){
// System.out.println("吃饭");
// }
//
// public void sleep(){
// System.out.println("睡觉");
// }
public void study(){
System.out.println("学习");
}
public void show(){
System.out.println("name:" + name + ",age:" + getAge());
}
}
public class ExtendsTest {
public static void main(String[] args) {
Person p1 = new Person();
// p1.age = 1;
p1.eat();
System.out.println("*****************");
Student s1 = new Student();
s1.eat();
// s1.sleep();
s1.name = "Tom";
s1.setAge(10);
System.out.println(s1.getAge());
s1.breath();
Creature c = new Creature();
System.out.println(c.toString());
}
}
1.2 重写
在子类中可以根据需要对从父类中继承来的方法进行改造, 也称为方法的重置、覆盖
override/overwrite。在程序执行时,子类的方法将覆盖父类的方法。
- 子类重写的方法必须和父类被重写的方法具有相同的方法名称、 参数列表
- 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型
- 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限
- 子类不能重写父类中声明为private权限的方法 - 子类方法抛出的异常不能大于父类被重写方法的异常
1.3 修饰符
Java权限修饰符public、protected、(缺省)、private置于类的成员定义前,用来限定对象对该类
成员的访问权限。
对于class的权限修饰只可以用public和default(缺省)。
- public类可以在任意地方被访问。
- default类只可以被同一个包内部的类访问。
例:
class Parent{
private int f1 = 1;
int f2 = 2;
protected int f3 = 3;
public int f4 = 4;
private void fm1() {System.out.println("in fm1() f1=" + f1);}
void fm2() {System.out.println("in fm2() f2=" + f2);}
protected void fm3() {System.out.println("in fm3() f3=" + f3);}
public void fm4() {System.out.println("in fm4() f4=" + f4);}
}
class Child extends Parent{ //设父类和子类在同一个包内
private int c1 = 21;
public int c2 = 22;
private void cm1(){System.out.println("in cm1() c1=" + c1);}
public void cm2(){System.out.println("in cm2() c2=" + c2);}
public static void main(String[] args){
int i;
Parent p = new Parent();
i = p.f2; // i = p.f3; i = p.f4;
p.fm2(); // p.fm3(); p.fm4();
Child c = new Child();
i = c.f2; // i = c.f3; i = c.f4;
i = c.c1; // i = c.c2;
c.cm1(); // c.cm2(); c.fm2(); c.fm3(); c.fm4()
}
}
1.4 super关键词
- super可用于访问父类中定义的属性
- super可用于调用父类中定义的成员方法
- super可用于在子类构造器中调用父类的构造器
- super的追溯不仅限于直接父类
- super和this的用法相像, this代表本类对象的引用, super代表父类的内存空间的标识
1.调用父类方法
public String getInfo() {
return super.getInfo() + "\nschool: " + school;
}
2.调用父类构造函数
public Student(String name, int age, String s) {
super(name, age);
school = s;
}
this和super的区别
1.4 多态
//多态性的使用举例一:
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test = new AnimalTest();
test.func(new Dog());
test.func(new Cat());
}
//执行动物的行为
public void func(Animal animal){//Animal animal = new Dog();
animal.eat();
animal.shout();
if(animal instanceof Dog){
Dog d = (Dog)animal;
d.watchDoor();
}
}
// public void func(Dog dog){
// dog.eat();
// dog.shout();
// }
// public void func(Cat cat){
// cat.eat();
// cat.shout();
// }
}
class Animal{
public void eat(){
System.out.println("动物:进食");
}
public void shout(){
System.out.println("动物:叫");
}
}
class Dog extends Animal{
public void eat(){
System.out.println("狗吃骨头");
}
public void shout(){
System.out.println("汪!汪!汪!");
}
public void watchDoor(){
System.out.println("看门");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
public void shout(){
System.out.println("喵!喵!喵!");
}
}
}
1. 概念解析
- 多态性,是面向对象中最重要的概念, 在Java中的体现:对象的多态性:父类的引用指向子类的对象
- Java引用变量有两个类型: 编译时类型和运行时类型。 编译时类型由声明该变量时使用的类型决 定, 运行时类型由实际赋给该变量的对象决定。
简称: 编译时, 看左边;运行时, 看右边。 - 若编译时类型和运行时类型不一致, 就出现了对象的多态性(Polymorphism)
- 多态情况下, “看左边” : 看的是父类的引用(父类中不具备子类特有的方法)“看右边” : 看
的是子类的对象(实际运行的是子类重写父类的方法) - 多态,提高了代码的通用性,常称作接口重用
2. instanceof关键词
- x instanceof A:检验x是否为类A的对象,返回值为boolean型
3. 强制类型装换
-
对Java对象的强制类型转换称为造型
-
从子类到父类的类型转换可以自动进行
-
从父类到子类的类型转换必须通过造型(强制类型转换)实现
-
无继承关系的引用类型间的转换是非法的
-
在造型前可以使用instanceof操作符测试一个对象的类型
1.5 java.lang.Object对象
Object类是所有Java类的根父类 如果在类的声明中未使用extends关键字指明其父类, 则默认父类为 java.lang.Object类
1 ==和equals
- = =:
- 基本类型比较值:只要两个变量的值相等, 即为true。
- 引用类型比较引用(是否指向同一个对象):只有指向同一个对象时也就是内存地址相等, ==才 返回true。
- equals:
- equals():所有类都继承了Object, 也就获得了equals()方法。 可以重写,默认比较内存地址 也就是==
- 特例:当用equals()方法进行比较时, 对类File、 String、 Date及包装类(Wrapper Class) 来说,是比较类型及内容而不考虑引用的是否是同一个对象;在这些类中重写了Object类的 equals()方法。
- 当自定义使用equals()时, 可以重写。 用于比较两个对象的“内容” 是否都相等
2 toString
-
toString()方法在Object类中定义, 其返回值是String类型, 返回类名和它的引用地址。
-
在进行String与其它类型数据的连接操作时, 自动调用toString()方法
Date now=new Date();
System.out.println(“now=”+now); 相当于
System.out.println(“now=”+now.toString());
- 可以根据需要在用户自定义类型中重写toString()方法如String 类重写了toString()方法, 返回字符 串的值。
s1=“hello”;
System.out.println(s1);//相当于System.out.println(s1.toString());
1.6 自动装箱和封箱
- 装箱:基本数据类型包装成包装类的实例
- 通过包装类的构造器实现: int i = 500; Integer t = new Integer(i);
- 通过字符串参数构造包装类对象: Float f = new Float(“4.56”);
- 拆箱: 获得包装类对象中包装的基本类型变量,方式是调用包装类的.xxxValue()方法: boolean b =bObj.booleanValue();
- 自动装箱,自动拆箱:JDK1.5支持自动装箱和拆箱
- 字符串转换成基本数据类型
- 通过包装类的构造器实现: int i = new Integer(“12”);
- 通过包装类的parseXxx(String s)静态方法: Float f = Float.parseFloat(“12.1”);
- 基本数据类型转换成字符串
- 调用字符串重载的valueOf()方法: String fstr = String.valueOf(2.34f);
- 更直接的方式: String intStr = 5 + “”
装箱,基本数据类型转换成包装类,成为装箱
Integer i1 = 100;
int i = 123;
Integer i2 = new Integer(123);
Integer i3 = new Integer("456");
//基本数据类型int->对应的包装类->Integer
//float-> Float
拆箱:包装类转换成基本数据类型
Integer i1 = new Integer(123);
//拆箱
int i2 = i1.intValue();
Boolean b2 = new Boolean(false);
boolean b3 = b2.booleanValue(); //拆箱
2 面向对象高级
2.1 静态
● 使用范围:在Java类中, 可用static修饰属性、 方法、 代码块、 内部类
● 被修饰后的成员具备以下特点:
- 随着类的加载而加载
- 优先于对象存在
- 修饰的成员,被所有对象所共享
- 访问权限允许时,可不创建对象,直接被类调用
● 类方法,类中使用static修饰的方法
- 没有对象的实例时,可以用类名.方法名()的形式访问由static修饰的类方法。
- 在static方法内部只能访问类的static修饰的属性或方法, 不能访问类的非static的结构
- 因为不需要实例就可以访问static方法,因此static方法内部不能有this。 (也不能有super ? YES!)
- static修饰的方法不能被重写
public class Chinese {
private String name;
private int age;
public static String nation = "大中国";
public void f1() {
System.out.println("f1");
}
public static void f2() {
System.out.println("f2");
}
//自我介绍
public static void show() {
//在静态方法中,不能使用this,super
//静态方法中,只能使用静态变量
System.out.println("自我介绍" + nation);
f2();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package k_static;
public class Test {
public static void main(String[] args) {
Chinese malong = new Chinese();
malong.setName("马龙");
malong.setAge(32);
malong.nation = "中华人民共和国";
Chinese yaoming = new Chinese();
yaoming.setAge(34);
yaoming.setName("姚明");
yaoming.nation = "中国";
System.out.println(malong.getName());
System.out.println(malong.nation);
System.out.println(Chinese.nation);
Chinese.show();
}
}
2.2 单例模式
设计模式:程序在某一个场景的,设计和开发套路,比如围棋和象棋套路
单例模式:要求程序中某一个组件,在程序运行的整个生命周期中,只有一个实例
- 构造函数私有,外面的组件不能主动创建这个对象
- 提供静态方法返回对象实例,返回的是静态实例(静态的特性),所以只有一个实例
1. 饿汉式单例模式
- 好处:线程安全的
- 坏处:加载时,时间比较长
public class Singleton1 {
private Singleton1() {
System.out.println("Singleton1 实例化了");
}
private static Singleton1 instance = new Singleton1(); //因为静态的特性只有一份存在方法区,所以这个对象一定只有一个
public static Singleton1 getInstance() {
return instance;
}
}
2. 懒汉式
存在线程安全问题,没有静态初始化
package l_singleton;
public class Singleton2 {
private Singleton2() {
System.out.println("懒汉式单例模式实例化。。。。");
}
private static Singleton2 instance = null;
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
public class Test {
public static void main(String[] args) {
Singleton1 s1 = Singleton1.getInstance();
Singleton1 s2 = Singleton1.getInstance();
Singleton1 s3 = Singleton1.getInstance();
Singleton1 s4 = Singleton1.getInstance();
Singleton2 s5 = Singleton2.getInstance();
Singleton2 s6 = Singleton2.getInstance();
}
}
2.3 代码块和初始化顺序
代码块:
class A {
//初始化代码块
{
....
}
//静态初始化代码块
static {
}
}
对象初始化顺序
- 静态初始化
- 初始化
- 构造函数
- 父类先执行
public class Parent {
int i = 0; //初始化
{
System.out.println("Parent初始化代码块1");
}
{
System.out.println("Parent初始化代码块2");
}
static int i2 = 0; //静态初始化
static {
System.out.println("Parent静态初始化代码块1");
}
static {
System.out.println("Parent静态初始化代码块2");
}
public Parent() {
System.out.println("Parent的构造函数。。。。");
}
}
public class Children extends Parent {
int i = 0; //初始化
{
System.out.println("Children初始化代码块1");
}
{
System.out.println("Children初始化代码块2");
}
static int i2 = 0; //静态初始化
static {
System.out.println("Children静态初始化代码块1");
}
static {
System.out.println("Children静态初始化代码块2");
}
public Children() {
System.out.println("Children的构造函数。。。。");
}
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// Class<Children> clazz = (Class<Children>) Class.forName("m_order.Children");
// Children bean = (Children) clazz.newInstance(); //反射产生对象
Children c1 = new Children();
}
Parent静态初始化代码块1
Parent静态初始化代码块2
Children静态初始化代码块1
Children静态初始化代码块2
Parent初始化代码块1
Parent初始化代码块2
Parent的构造函数。。。。
Children初始化代码块1
Children初始化代码块2
Children的构造函数。。。。
2.4 final关键词
● 修饰类,表示类不能被继承
● 修饰方法:表示不能被重写
● 修饰成员变量,表示变量不能被改变
//public final class A { //不能被继承
public class A {
// final void f1() { //不嫩被重写
void f1() {
}
}
class B extends A {
final int age = 35;
@Override
void f1() {
// age = 36; //不能被改变
super.f1();
}
}
2.5 抽象类
● 用abstract关键字来修饰一个类, 这个类叫做抽象类。
● 用abstract来修饰一个方法, 该方法叫做抽象方法。抽象方法:只有方法的声明,没有方法的实现。以分号结束:比如: public abstract void talk();
● 含有抽象方法的类必须被声明为抽象类。
● 抽象类不能被实例化。抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。
public abstract class A {
public abstract void f1();
public void f2() {
System.out.println("f2");
}
}
public class B extends A {
@Override
public void f1() {
System.out.println("f1()");
}
//alt+enter:解决一切问题
}
public class Test {
public static void main(String[] args) {
// new A();
new B();
}
}
2.6 接口
- 一方面, 有时必须从几个类中派生出一个子类, 继承它们所有的属性和方法。 但是, Java不支持多重继承。 有了接口,就可以得到多重继承的效果。
- 另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有is-a的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等都支持USB连接。
- 接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。 继承是一个"是不是"的关系,而接口实现则是"能不能"的关系。
- 接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都要遵守。
public interface Usb {
//接口的方法默认public abstract
public abstract void start();
void end();
//接口中不能有普通方法
// void f2() {
// }
}
public interface TypeC {
void transfer();
}
public class Printer implements Usb, TypeC {
@Override
public void start() {
System.out.println("usb 启动");
}
@Override
public void end() {
System.out.println("usb 关闭");
}
public void transfer() {
System.out.println("typeC接口传输数据");
}
}
注:接口和抽象的区别
● 抽象类可以有非抽象方法,接口只能有抽象方法
● 接口中方法默认public abstract
● 接口可以实现多继承,抽象类不可以
2.7 内部类和匿名内部类
2.7.1 内部类
java中可以把一个java放到另一java类的内部,这个类成为内部类
内部类的分类:
- 成员变量位置,成员内部类
- static成员内部类
- 非静态的成员内部类
- 方法中,局部内部类
语法
- 修饰成员变量的所有的修饰符都可以修饰成员内部类
- 内部类可以使用继承
public class Person {
private String name;
private int age;
//内部类,如果一个类只给当前外部类使用,别的组件不用,比如Dog和Cat类只是在Person中使用
//所有的成员变量的修饰符,都可以修饰内部类
public static class Dog {
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Cat {
public void eat() {
System.out.println("猫吃鱼");
}
}
public void f1() {
class Bird {
public void eat() {
System.out.println("鸟吃虫子");
}
}
Bird b = new Bird();
b.eat();
}
}
测试:
public class Test {
public static void main(String[] args) {
//创建静态成员内部类的方法
Person.Dog dog = new Person.Dog();
dog.eat();
//创建非静态成员内部类的方法
Person p = new Person();
Person.Cat cat = p.new Cat();
cat.eat();
p.f1();
}
}
2.7.2 匿名内部类
- 一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。
- 匿名内部类的特点
- 匿名内部类必须继承父类或实现接口
- 匿名内部类只能有一个对象
- 匿名内部类对象只能使用多态形式引用
- 语法new 接口(类){实现};
public interface Usb {
//接口的方法默认public abstract
public abstract void start();
void end();
}
public interface TypeC {
void transfer();
}
测试:
package s_anonymous;
public class Test {
public static void f1(Usb usb) {
usb.start();
usb.end();
}
public static void f2(TypeC typeC) {
typeC.transfer();
}
public static void main(String[] args) {
// //内部类
// class Test$1 implements Usb {
// @Override
// public void start() {
// System.out.println("打印机usb启动");
// }
//
// @Override
// public void end() {
// System.out.println("打印机usb停止");
// }
// }
// Test$1 usb1 = new Test$1();
//方式1
/**
* 1:多态性:编译类型是父类
* 2:new 接口(类),后面跟一个实现
* 3:有一个对象usb1,实现了Usb接口的一个没有名字的类的实例
*
* Test$1->实现了Usb接口->用这个类产生一个对象usb1
*/
Usb usb1 = new Usb() {
@Override
public void start() {
System.out.println("打印机usb启动");
}
@Override
public void end() {
System.out.println("打印机usb停止");
}
};
f1(usb1);
//方式2:匿名内部类直接作为方法参数
f2(new TypeC() {
@Override
public void transfer() {
System.out.println("苹果14 正在传输数据....");
}
});
}
}
2.8 面试题解析
2.8.1 成员变量、局部变量、类变量存储在内存的什么地方
1. 类变量(静态成员变量)
类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁在java7之前把静态变量存放于方法区,在java7时存放在堆中
2. 成员变量
成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分
由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中
3. 局部变量
局部变量是定义在类的方法中的变量
在所在方法被调用时放入虚拟机栈的栈帧中,栈顶是正在执行的方法,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中
2.8.2 下面变量引用存放在哪里
public class StaticObjTest {
static class Test{
// 静态变量
// 一个java.lang.Class类型的对象实例引用了此变量
static ObjectHolder staticObj = new ObjectHolder();
// 实例变量
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
// 局部变量
ObjectHolder localObj = new ObjectHolder()();
System.out.println("done");
}
}
private static class ObjectHolder{
}
public static void main(String[] args) {
Test test = new StaticObjTest.Test();
test.foo();
}
}
● 以上代码中,静态变量staticObj随着Test的类型信息存放在方法区
● 实例变量instanceObj随着Test的对象实例存放在堆区
● 局部变量localObj则是存放在foo()方法栈帧的局部变量表中
● 三个变量引用对应的对象实体都是在堆空间。
2.8.3 HotSpot 方法区变迁
(1)JDK1.2 ~ JDK6
(2)JDK7
由于 GC 分代技术的影响,使之许多优秀的内存调试工具无法在 Oracle HotSpot之上运行,必须单独处理;
并且 Oracle 同时收购了 BEA 和 Sun 公司,同时拥有 JRockit 和 HotSpot,在将 JRockit 许多优秀特性移植到 HotSpot 时,由于 GC 分代技术遇到了种种困难,所以从 JDK8 开始 Oracle HotSpot 开始移除永久代。
JDK7中符号表被移动到 Native Heap中, 字符串常量和类引用被移动到 Java NON_HEAP中。
(3)JDK8
在 JDK8 中1,永久代已完全被元空间(Meatspace)所取代。
2.8.4 为什么调整字符串常量池和静态变量的位置
- JDK7中将字符串常量池放到了堆空间中:因为永久代的回收效率很低,在Full GC时才会触发,而Full GC在老年代的空间不足、永久代不足时才会触发,这就导致字符串常量池回收效率不高;
- 而我们开发中会有大量的字符串被创建,回收效率低会导致永久代内存不足。
- 将字符串常量池放到堆里,能及时回收内存。
2.8.5 为什么用元空间替换永久代
为永久代设置最大空间大小是难以确定的。
- 在某些场景下,如果动态加载类过多,容易产生Perm区的OOM:比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误;而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的最大大小仅受本地内存限制。
- 对永久代进行调优是很困难的。
2.8.6 JDK1.8元空间会产生内存溢出么?在什么情况下会产生内存溢出?
1. 解析
Java8 及以后的版本使用Metaspace来代替永久代,Metaspace是方法区在HotSpot中的实现,它与永久代最大区别在于,Metaspace并不在虚拟机内存中而是使用本地内存也就是在JDK8中,classe meta data(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的native memory。
永久代(java 8 后被元空间Metaspace取代了)存放了以下信息:
- 虚拟机加载的类信息
- 常量池
- 即时编译后的代码
*内存溢出的原因:**加载到内存中的 class 数量太多或者体积太大。
**解决办法:**增加 Metaspace 的大小
-XX:MaxMetaspaceSize=512m
2. 代码演示
模拟Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间是会超过Metaspace指定的空间大小的
查看元空间大小
java -XX:+PrintFlagsInitial
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
测试代码:
public class MetaspaceDemo {
static class OOM{}
public static void main(String[] args) {
int i = 0;//模拟计数多少次以后发生异常
try {
while (true){
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("=================多少次后发生异常:"+i);
e.printStackTrace();
}
}
}
cglib默认开启了UserCache生成的代理类都会复用原先产生在缓存中的类,所以至始至终都只有一个代理类,所以不会产生内存溢出。手动关闭它,enhancer.setUseCache(false);
运行结果:
2.8.7 JVM8的内存结构
1. 程序计数器(Program Counter Register)
程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过他来实现跳转、循环、恢复线程等功能。
-
在任何时刻,一个处理器内核只能运行一个线程,多线程是通过线程轮流切换,分配时间来完成的,这就需要有一个标志来记住每个线程执行到了哪里,这里便需要到了程序计数器。
-
程序计数器是线程私有的,每个线程都已自己的程序计数器。
2. 虚拟机栈(JVM Stacks)
虚拟机栈是线程私有的,随线程生灭。虚拟机栈描述的是线程中的方法的内存模型,每个方法被执行的时候,都会在虚拟机栈中同步创建一个栈帧(stack frame),方法被执行时入栈,执行完后出栈,每个栈帧的包含如下的内容
-
局部变量表:局部变量表中存储着方法里的java基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用(注:这里的基本数据类型
指的是方法内的局部变量) -
操作数栈
-
动态连接
-
方法返回地址
虚拟机栈可能会抛出两种异常:
- 如果线程请求的栈深度大于虚拟机所规定的栈深度,则会抛出StackOverFlowError即栈溢出
- 如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时会抛出OutOfMemoryError即OOM内存溢出
产生StackOverFlowError的原因是:
- 无限递归循环调用(最常见)。
- 执行了大量方法,导致线程栈空间耗尽。
- 方法内声明了海量的局部变量。
3. 本地方法栈(Native Method Stacks)
本地方法栈与虚拟机栈的作用是相似的,都会抛出OutOfMemoryError和StackOverFlowError,都是线程私有的,主要的区别在于:
-
虚拟机栈执行的是java方法
-
本地方法栈执行的是native方法
4. Java堆(Java Heap)
java堆是JVM内存中最大的一块,由所有线程共享, 是由垃圾收集器管理的内存区域,主要存放对象实例,当然由于java虚拟机的发展,堆中也多了许多东西,现在主要有:
对象实例
-
类初始化生成的对象
-
基本数据类型的数组也是对象实例
字符串常量池
- 字符串常量池原本存放于方法区,从jdk7开始放置于堆中。
- 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
静态变量
- 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
线程分配缓冲区(Thread Local Allocation Buffer)
-
线程私有,但是不影响java堆的共性
-
增加线程分配缓冲区是为了提升对象分配时的效率
java堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx和-Xms设定),如果堆无法扩展或者无法分配内存时也会报OOM。
5. 方法区(Method Area)
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
(1)类型信息:
对每个加载的 类型 (类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang. Object,都没有父类)
- 这个类型的修饰符( public, abstract,final的某个子集)
- 这个类型实现接口的一个有序列表。
(2) 域(Field)信息:
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。域的相关信息包括如下内容:
-
域名称
-
域类型
-
域修饰符(public,private,protected,static,final, volatile,transient的某个子集)
(3) 方法(Method)信息:
JVM必须在方法区中保存类型的所有方法的相关信息以及方法的声明顺序。方法的相关信息包括:
-
方法名称
-
方法的返回类型(或void)
-
方法参数的数量和类型(按顺序)
-
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
-
方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)
-
异常表(abstract和
native方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
(4)静态变量(non-final的)
静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
补充说明:被声明为final的静态变量的处理方法则不同,被static和final修饰的变量也称为全局变量,每个全局常量在编译的时候就会被赋值了。
public class Order {
public static int num = 10;
public static final int COUNT = 20;
}
(5)运行时常量池:
理解运行时常量池,需要了解字节码文件(ClassFile)中的常量池;方法区内部包含运行时常量池,字节码文件内部包含了常量池。
常量池:一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息,那就是常量池( Constant Pool Table),包括各种字面量(数量值和字符串值)和对类型、域和方法的符号引用。常量池可以看做是一张表,虚拟机指令根据这张常量表,找到要执行的字面量、类名、方法名、参数类型等。
运行时常量池:
- 运行时常量池(Runtime Constant pool)是方法区的一部分。
- 常量池(Constant Pool
Table)是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 - 在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
- JVM为每个已加载的类型(类或接口)都维护一个运行时常量池,池中的数据项像数组项一样,是通过索引访问的。
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,会转换为真实地址。运行时常量池,相对于Class文件中的常量池的另一重要特征是:具备动态性。比如String.intern()方法会动态地向池中增加内容
- 运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据比符号表要更加丰富。
- 如果构造类或接口的运行时常量池时,所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemoryError异常。
6. 直接内存(Direct Memory)
jdk1.4中加入了NIO(New Input/Putput)类,引入了一种基于通道(channel)与缓冲区(buffer)的新IO方式,它可以使用native函数直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高IO性能,避免了在java堆和native堆来回复制数据。
java 的 NIO 库允许 java 程序使用直接内存。直接内存是在 java 堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于 java 堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在 java 堆外,因此它的大小不会直接受限于 Xmx (虚拟机参数)指定的最大堆大小,但是系统内存是有限的, java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。直接内存位于本地内存,不属于JVM内存,不受GC管理,但是也会在物理内存耗尽的时候报OOM。
注意:direct buffer不受GC影响,但是direct buffer归属的JAVA对象是在堆上且能够被GC回收的,一旦它被回收,JVM将释放direct buffer的堆外空间
直接内存(Direct Memory)的特点:
-
直接内存并非 JVMS 定义的标准 Java 运行时内存。
-
JDK1.4 加入了新的 NIO 机制,目的是防止 Java 堆 和 Native 堆之间往复的数据复制带来的性能损耗,此后 NIO
可以使用 Native 的方式直接在 Native 堆分配内存。 -
直接内存区域是全局共享的内存区域。
-
直接内存区域可以进行自动内存管理(GC),但机制并不完善。
-
本机的 Native 堆(直接内存) 不受 JVM 堆内存大小限制。可能出现 OutOfMemoryError 异常。
2.8.8 方法区和永久代的区别
1. 方法区
方法区属于JVM规范的内容,JVM规范中,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。
方法区 是 JVM 的规范,所有虚拟机 必须遵守的。常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)
2. PermGen(永久代)
PermGen , 就是 PermGen space ,全称是 Permanent Generation space ,是指内存的永久保存区域。
这块内存主要是被JVM存放Class和Meta信息的, Class 在被 Loader 时就会被放到 PermGen space中。
绝大部分 Java 程序员应该都见过 java.lang.OutOfMemoryError: PermGen space 这个异常。
这里的 PermGen space 其实指的就是 方法区 。
PermGen space 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现,PermGen space 是 HotSpot 虚拟机有,而如 JRockit(Oracle)、J9(IBM) 虚拟机有 方法区 ,但是就没有 PermGen space。
强调: 只有 HotSpot 才有 PermGen space。
PermGen space 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现,PermGen space 是 JDK7及之前, HotSpot 虚拟机 对 方法区 的一个落地实现。HotSpot jdk1.6 的 PermGen space 如下:
HotSpot 也有去永久代的趋势,在 JDK 1.7 中 HotSpot 已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。
HotSpot 1.7 永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
永久代 在JDK8被移除, JDK1.8方法区 叫做 元空间:
Metaspace(元空间)是 JDK8及之后, HotSpot 虚拟机 对 方法区 的新的实现。
2.8.9 你知道的几种主要的JVM参数
1. 思路:
可以说一下堆栈配置相关的,垃圾收集器相关的,还有一下辅助信息相关的。
2. 参考答案:
(1)堆栈配置相关
- -Xmx3550m: 最大堆大小为3550m。
- -Xms3550m: 设置初始堆大小为3550m。
- -Xmn2g: 设置年轻代大小为2g。
- -Xss128k: 每个线程的堆栈大小为128k。
- -XX:MaxPermSize: 设置持久代大小为16m
- -XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代),比例为1:4
- -XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
- -XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
(2)垃圾收集器相关
- -XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
- –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合
- -XX:ParallelGCThreads=20: 配置并行收集器的线程数
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合
- -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
- -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
(3)辅助信息相关
- -XX:+PrintGC 输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K),
0.0650971 secs]
- -XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633
secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K),
0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs