JavaSE复习(二)

目录:

1、Java中的反射

1.1、说说对Java中反射的理解

2、Java中的动态代理

2.1、写一个ArrayList的动态代理类

2.2、动静态代理的区别,什么场景使用?

3、Java中的设计模式&回收机制

3.1、你说知道的设计模式有哪些?

3.2、单例设计模式

3.3、工厂设计模式

3.4、建造这模式(Builder)

3.5、适配器设计模式

3.6、装饰模式(Decorator)

3.7、策略模式(Strategy)

3.8、观察者模式(Observer)

3.9、JVM垃圾回收机制和常见算法

3.10、谈谈JVM的内存结构和内存分配

3.11、Java中的引用类型都有哪些?

3.12、heap(堆)和stack(栈)有什么区别?

3.13、解释内存中的堆、栈和方法区的用法

4、Java的类加载器

4.1、Java的类加载器的种类都有哪些?

4.2、类什么时候被初始化?

4.3、Java类加载体系之ClassLoader双亲委托机制

4.4、描述下JVM加载class

4.5、获得一个类对象有哪些方式?

4.6、在开发中遇到的内存溢出有哪些?


1、Java中的反射

1.1、说说对Java中反射的理解

Java中的反射首先是能够获取到Java中要反射类的字节码,获取字节码有三种方法:

1.Class.forName(className) 2.类名.class 3.this.getClass()。然后将字节码中的方法,变量,构造函数等映射成相应的Method、Filed、Constructor等类,这些类提供了丰富的方法可以被我们所使用。

2、Java中的动态代理

2.1、写一个ArrayList的动态代理类

final List<String> list = new ArrayList<String>();

List<String> proxyInstance = (List<String>)Proxy.newProxyInstance(list.getClass().getClassLoader(),list.getClass().getInterfaces(),new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(list, args);
    }
});
proxyInstance.add("你好");
System.out.println(list);

2.2、动静态代理的区别,什么场景使用?

静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。

静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。

动态代理是实现JDK里的InvocationHandler接口的invoke方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象。

还有一种动态代理CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理,通过在运行时,动态修改字节码达到修改类的目的。

AOP编程就是基于动态代理实现的,比如著名的Spring框架,Hibernate框架都是动态代理的实现例子。

3、Java中的设计模式&回收机制

3.1、你说知道的设计模式有哪些?

Java中一般认为有23中设计模式,我们不需要所有的都会,但是其中的集中设计模式应该去掌握,下面列出了所有的设计模式。

总体来说设计模式分为三大类:

  • 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
  • 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
  • 行为型模式,共十一中:策略模式、模板方法模式、观察者模式、迭代值模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

3.2、单例设计模式

最好理解的一种设计模式,分为懒汉式和饿汉式。

懒汉式:

public class Singleton {
    // 直接创建对象
    public static Singleton instance = new Singleton();
    
    // 私有化构造函数
    private Singleton() {
    
    }

    // 返回对象实例
    public static Singleton getInstance() {
        return instance;
    }   
}

饿汉式:

public class Singleton {
    // 声明变量
    private static volatile Singleton singleton = null;
    
    // 私有构造函数
    private Singleton() {
    
    }

    // 提供对外方法
    public static  Singleton getInstance() {
        if(singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        } 
        return singleton;   
    }
}

3.3、工厂设计模式

工厂模式分为工厂方法模式和抽象工厂模式。

1. 工厂方法模式:

工厂方法模式分为三种:

  1. 普通工厂模式,就是建立一个工厂,对实现了同一个接口的一些类进行实例的创建。
  2. 多个工厂方法模式,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
  3. 静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

   1.1 普通工厂模式:

public interface Sender {
    public void Send();
}

public class MailSender implements Sender {
    @Override
    public void Send() {
        System.out.println("this is mail sender!");
    }
}

public class SmsSender implements Sender {
    @Override
    public void Send() {
    System.out.println("this is sms sender!");
}
}

public class SendFactory {
    public Sender produce(String type) {
    if ("mail".equals(type)) {
        return new MailSender();
    } else if ("sms".equals(type)) {
        return new SmsSender();
    } else {
        System.out.println("请输入正确的类型!");
        return null;
    }
}

    1.2 多个工厂方法模式:

该模式是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而
多个工厂方法模式是提供多个工厂方法,分别创建对象。

public class SendFactory {
    public Sender produceMail(){
        return new MailSender();
    }

    public Sender produceSms(){
        return new SmsSender();
    }
}

public class FactoryTest {
    public static void main(String[] args) {
        SendFactory factory = new SendFactory();
        Sender sender = factory.produceMail();
        sender.send();
    }
}

    1.3 静态工厂模式:

将上面的多个工厂方法模式里的方法置为 静态的,不需要创建实例,直接调用即可。

public class SendFactory {
    public static Sender produceMail() {
        return new MailSender();
    }
    
    public static Sender produceSms() {
         return new SmsSender();
    }
}

public class FactoryTest {
    public static void main(String[] args) {
        Sender sender = SendFactory.produceMail();
        sender.send();
    }
}

2. 抽象工厂模式:

工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?

就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。

public interface Provider {
    public Sender produce();
}

//------------------------------------------------------------
public interface Sender {
    public void send();
}

//------------------------------------------------------------
public class MailSender implements Sender {
    @Override
    public void send() {
        System.out.println("this is mail sender!");
    }
}

//------------------------------------------------------------
public class SmsSender implements Sender {
    @Override
    public void send() {
    System.out.println("this is sms sender!");
    }
}

//------------------------------------------------------------
public class SendSmsFactory implements Provider {
    @Override
    public Sender produce() {
        return new SmsSender();
    }
}
public class SendMailFactory implements Provider {
    @Override
    public Sender produce() {
        return new MailSender();
    }
}

//------------------------------------------------------------
public class Test {
    public static void main(String[] args) {
        Provider provider = new SendMailFactory();
        Sender sender = provider.produce();
        sender.send();
    }
}

3.4、建造这模式(Builder)

工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的 Test 结合起来得到的。

public class Builder {
    private List<Sender> list = new ArratList<Sender>();
    
    public void produceMailSender(int count) {
        for(int i = 0; i < count; i++) {
            list.add(new MailSender());
        }
    }

    public void produceSmsSender(int count) {
        for(int i = 0; i < count; i++) {
            list.add(new SendSender());
        }
    }
}

public class TestBuilder {
    public static void main(String[] args) {
        Builder builder = new Builder();
        builder.produceMailSender(10);
    }
}

3.5、适配器设计模式

适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配锁造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。

1. 类的适配器模式:

public class Source {
    public void method1() {
        System.out.println("this is original method!");
    }

// -------------------------------------------------------------

public interface Targetable {
    /* 与原类中的方法相同 */
    public void method1();
    /* 新类的方法 */
    public void method2();
}

public class Adapter extends Source implements Targetable {
    @Override
    public void method2() {
        System.out.println("this is the targetable method!");
    }
}
public class AdapterTest {
    public static void main(String[] args) {
        Targetable target = new Adapter();
        target.method1();
        target.method2();
    }
}

2. 对象的适配器模式:

基本的思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承Source类,而是持有Source类的实例,以达到解决兼容性的问题。

public class Wrapper implements Targetable {
    private Source source;
    
    public Wrapper(Source source) {
        supper();
        this.source = source;
    }
    
    @Override
    public void method2() {
        System.out.println("this is the targetable method!");
    }

    @Override
    public void method1() {
        source.method1();
    }
}

// --------------------------------------------------------------
public class AdapterTest {
    public static void main(String[] args) {
        Source source = new Source();
        Targetable target = new Wrapper(source);
        target.method1();
        target.method2();
    }
}

3. 接口的适配器模式:

接口的适配器是这样的:有时我们写一个接口中的多个抽象方法,当我们写改接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和改抽象类取得联系,所以我们只写一个类,继承改抽象类,重写我们需要的方法就行

3.6、装饰模式(Decorator)

顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。

public interface Sourceable {
    public void method();
}

// ----------------------------------------------------
public class Source implements Sourceable {
    @Override
    public void method() {
        System.out.println("the original method!");
    }
}

// ----------------------------------------------------
public class Decorator implements Sourceable {
    private Sourceable source;
    public Decorator(Sourceable source) {
        super();
        this.source = source;
    }

    @Override
    public void method() {
        System.out.println("before decorator!");
        source.method();
        System.out.println("after decorator!");
    }
}

// ----------------------------------------------------
public class DecoratorTest {
    public static void main(String[] args) {
        Sourceable source = new Source();
        Sourceable obj = new Decorator(source);
        obj.method();
    }
}

3.7、策略模式(Strategy)

策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数。策略模式的决定权在用户,系统本身提供不同算法的实现,新增或删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。

public interface ICalculator {
    public int calculate(String exp);
}

// ---------------------------------------------------------
public class Minus extends AbstractCalculator implements ICalculator {
    @Override
    public int calculate(String exp) {
        int arrayInt[] = split(exp, "-");
        return arrayInt[0] - arrayInt[1];
    }
}

// ---------------------------------------------------------
public class Plus extends AbstractCalculator implements ICalculator {
    @Override
    public int calculate(String exp) {
        int arrayInt[] = split(exp, "\\+");
        return arrayInt[0] + arrayInt[1];
    }
}

// --------------------------------------------------------
public class AbstractCalculator {
    public int[] split(String exp, String opt) {
        String array[] = exp.split(opt);
        int arrayInt[] = new int[2];
        arrayInt[0] = Integer.parseInt(array[0]);
        arrayInt[1] = Integer.parseInt(array[1]);
        return arrayInt;
    }
}
public class StrategyTest {
    public static void main(String[] args) {
        String exp = "2+8";
        ICalculator cal = new Plus();
        int result = cal.calculate(exp);
        System.out.println(result);
    }
}

3.8、观察者模式(Observer)

观察者模式很好理解,类似于邮件订阅和 RSS 订阅,当我们浏览一些博客或 wiki 时,经常会看到 RSS 图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。其实,简单来讲就一句话:当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。

public interface Observer {
    public void update();
}

public class Observer1 implements Observer {
    @Override
    public void update() {
        System.out.println("observer1 has received!");
    }
}

public class Observer2 implements Observer {
    @Override
    public void update() {
        System.out.println("observer2 has received!");
    }
}

public interface Subject {
    /*增加观察者*/
    public void add(Observer observer);

    /*删除观察者*/
    public void del(Observer observer);
    /*通知所有的观察者*/
    public void notifyObservers();
    /*自身的操作*/
    public void operation();
}

public abstract class AbstractSubject implements Subject {

    private Vector<Observer> vector = new Vector<Observer>();

    @Override
    public void add(Observer observer) {
        vector.add(observer);
    }

    @Override
    public void del(Observer observer) {
        vector.remove(observer);
    }

    @Override
    public void notifyObservers() {
        Enumeration<Observer> enumo = vector.elements();
        while (enumo.hasMoreElements()) {
            enumo.nextElement().update();
        }
    }
}

public class MySubject extends AbstractSubject {
@Override
public void operation() {
System.out.println("update self!");
notifyObservers();
}
}

public class ObserverTest {
public static void main(String[] args) {
Subject sub = new MySubject();
sub.add(new Observer1());
sub.add(new Observer2());
sub.operation();
}

3.9、JVM垃圾回收机制和常见算法

理论上来讲Sun 公司只定义了垃圾回收机制规则而不局限于其实现算法,一次不同厂商生产的虚拟机采用的算法也不尽相同。

GC(Garbage Collector)在回收对象前首先必须发现那些无用的对象,如何去发现定位这些无用的对象?常用的搜索算法如下:

1)引用计数器算法(废弃)

引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为 0 的时候,JVM 就认为对象不再被使用,是“垃圾”了。

引用计数器实现简单,效率高;但是不能解决循环引用问问题(A 对象引用 B 对象,B 对象又引用 A 对象,但是A,B 对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在 JDK1.1 之后,这个算法已经不再使用了。

2)根搜索算法(使用)

根搜索算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象没有被 GC Roots 的引用链连接的时候,说明这个对象是不可用的。

GC Roots 对象包括:

  • a) 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  • b) 方法区域中的类静态属性引用的对象。
  • c) 方法区域中常量引用的对象。
  • d) 本地方法栈中 JNI(Native 方法)的引用的对象。

通过上面的算法搜索到无用对象之后,就是回收过程, 回收算法 如下:

1)标记—清除算法(Mark-Sweep)(DVM 使用的算法)

标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。

2)复制算法(Copying)

复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的 JVM 用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是 1:1(大概是 8:1)。

3)标记—整理算法(Mark-Compact)

标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。

4)分代收集(Generational Collection)

分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。

3.10、谈谈JVM的内存结构和内存分配

1. Java的内存模型:

Java 虚拟机将其管辖的内存大致分三个逻辑部分:方法区(Method Area)、Java 栈和 Java 堆。

1、方法区是静态分配的,编译器将变量绑定在某个存储位置上,而且这些绑定不会在运行时改变。常数池,源代码中的命名常量、String 常量和 static 变量保存在方法区。

2、Java Stack 是一个逻辑概念,特点是后进先出。一个栈的空间可能是连续的,也可能是不连续的。最典型的 Stack 应用是方法的调用,Java 虚拟机每调用一次方法就创建一个方法帧(frame),退出该方法则对应的 方法帧被弹出(pop)。栈中存储的数据也是运行时确定的。

3、Java 堆分配(heap allocation)意味着以随意的顺序,在运行时进行存储空间分配和收回的内存管理模型。堆中存储的数据常常是大小、数量和生命期在编译时无法确定的。Java 对象的内存总是在 heap 中分配。我们每天都在写代码,每天都在使用 JVM 的内存。

2. java 内存分配

  • 1、基础数据类型直接在栈空间分配;
  • 2、方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;
  • 3、引用数据类型,需要用 new 来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;
  • 4、方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;
  • 5、局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待 GC 回收;
  • 6、方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放;
  • 7、字符串常量在 DATA 区域分配 ,this 在堆空间分配;
  • 8、数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小!

3.11、Java中的引用类型都有哪些?

Java 中对象的引用分为四种级别,这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
1. 强引用(StrongReference)

这个就不多说,我们写代码天天在用的就是强引用。如果一个对象被被人拥有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

Java 的对象是位于 heap 中的,heap 中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚。对于对象是属于哪种可及的对象,由他的最强的引用决定。如下代码:

String abc=new String("abc"); //1
SoftReference<String> softRef=new SoftReference<String>(abc); //2
WeakReference<String> weakRef = new WeakReference<String>(abc); //3
abc=null; //4
softRef.clear();//5

第一行在 heap 堆中创建内容为“abc”的对象,并建立 abc 到该对象的强引用,该对象是强可及的。

第二行和第三行分别建立对 heap 中对象的软引用和弱引用,此时 heap 中的 abc 对象已经有 3 个引用,显然此
时 abc 对象仍是强可及的。

第四行之后 heap 中对象不再是强可及的,变成软可及的。

第五行执行之后变成弱可及的。

2. 软引用(SoftReference)

如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。

软引用是主要用于内存敏感的高速缓存。在 jvm 报告内存不足之前会清除所有的软引用,这样以来 gc 就有可能收集软可及的对象,可能解决内存吃紧问题,避免内存溢出。什么时候会被收集取决于 gc 的算法和 gc 运行时可用内存的大小。当 gc 决定要收集软引用时执行以下过程,以上面的 softRef 为例:

  1. 首先将 softRef 的 referent(abc)设置为 null,不再引用 heap 中的 new String("abc")对象。
  2. 将 heap 中的 new String("abc")对象设置为可结束的(finalizable)。
  3. 当 heap 中的 new String("abc")对象的 finalize()方法被运行而且该对象占用的内存被释放, softRef被添加到它的 ReferenceQueue(如果有的话)中。

注意:对 ReferenceQueue 软引用和弱引用可以有可无,但是虚引用必须有。

被 Soft Reference 指到的对象,即使没有任何 Direct Reference,也不会被清除。一直要到 JVM 内存不足且没有 Direct Reference 时才会清除,SoftReference 是用来设计 object-cache 之用的。如此一来SoftReference 不但可以把对象 cache 起来,也不会造成内存不足的错误 (OutOfMemoryError)。
3. 弱引用(WeakReference)

如果一个对象只具有弱引用,那该类就是可有可无的对象,因为只要该对象被 gc 扫描到了随时都会把它干掉。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4. 虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

建立虚引用之后通过 get 方法返回结果始终为 null,通过源代码你会发现,虚引用通向会把引用的对象写进referent,只是 get 方法返回结果为 null。先看一下和 gc 交互的过程再说一下他的作用。

  1. 不把 referent 设置为 null, 直接把 heap 中的 new String("abc")对象设置为可结束的(finalizable)。
  2. 与软引用和弱引用不同, 先把 PhantomRefrence 对象添加到它的 ReferenceQueue 中.然后在释放虚可及的对象。

3.12、heap(堆)和stack(栈)有什么区别?

从以下几个方面阐述堆(heap)和栈(stack)的区别。

1. 申请方式

  • stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间。
  • heap:需要程序员自己申请,并指明大小,在 c 中 malloc 函数,对于 Java 需要手动 new Object()的形式开辟。

2. 申请后系统的响应

  • stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
  • heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

3. 申请大小的限制

  • stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
  • heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

4. 申请效率的比较

  • stack:由系统自动分配,速度较快。但程序员是无法控制的。
  • heap:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

5. heap和stack中的存储内容

  • stack: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
  • heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

6. 数据结构层面的区别

还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第 1 个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因

3.13、解释内存中的堆、栈和方法区的用法

通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用 JVM 中的栈空间;而通过 new 关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为 Eden、Survivor(又可分为From Survivor 和 To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变量、JIT 编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的 100、"hello"和常量都是放在常量池中,常量池是方法区的一部分。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量池空间不足则会引发 OutOfMemoryError。
String str = new String("hello");
上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。

4、Java的类加载器

4.1、Java的类加载器的种类都有哪些?

  • 1. 根类加载(Bootstrap):c++写的,看不到源码。
  • 2. 扩展类加载器(Extension):加载位置(jre\lib\ext中)
  • 3. 系统(应用)类加载器(System\App):加载位置(classpath中)
  • 4. 自定义加载器(必须继承ClassLoader)

4.2、类什么时候被初始化?

只有这 6 中情况才会导致类的类的初始化。

  • 1)创建类的实例,也就是 new 一个对象
  • 2)访问某个类或接口的静态变量,或者对该静态变量赋值
  • 3)调用类的静态方法
  • 4)反射(Class.forName("com.test.load"))
  • 5)初始化一个类的子类(会首先初始化子类的父类)
  • 6)JVM 启动时标明的启动类,即文件名和类名相同的那个类

类的初始化步骤:

  • 1)如果这个类还没有被加载和链接,那先进行加载和链接。
  • 2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)。
  • 3)加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句。

4.3、Java类加载体系之ClassLoader双亲委托机制

java 是一种类型安全的语言,它有四类称为安全沙箱机制的安全机制来保证语言的安全性,这四类安全沙箱分别是:

  • 1) 类加载体系
  • 2) .class 文件检验器
  • 3)  内置于 Java 虚拟机(及语言)的安全特性
  • 4)  安全管理器及 Java API

主要讲解类的加载体系:
java 程序中的 .java 文件编译完会生成 .class 文件,而 .class 文件就是通过被称为类加载器的 ClassLoader加载的,而 ClassLoder 在加载过程中会使用“双亲委派机制”来加载 .class 文件,先上图:

BootStrapClassLoader

启 动 类 加 载 器 , 该 ClassLoader 是 jvm 在 启 动 时 创 建 的 , 用 于 加载$JAVA_HOME$/jre/lib 下面的类库(或者通过参数-Xbootclasspath 指定)。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。
ExtClassLoader

扩展类加载器,该ClassLoader是在sun.misc.Launcher里作为一个内部类ExtClassLoader定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader 会加载 $JAVA_HOME/jre/lib/ext 下的类库(或者通过参数-Djava.ext.dirs 指定)。
AppClassLoader

应用程序类加载器,该 ClassLoader 同样是在 sun.misc.Launcher 里作为一个内部类AppClassLoader 定义的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader 会加载 java 环境变量CLASSPATH 所 指 定 的 路 径 下 的 类 库 , 而 CLASSPATH 所 指 定 的 路 径 可 以 通 过System.getProperty("java.class.path")获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的 class 目录)。
CustomClassLoader

自定义类加载器,该 ClassLoader 是指我们自定义的 ClassLoader,比如 tomcat StandardClassLoader 属于这一类;当然,大部分情况下使用 AppClassLoader 就足够了。
前面谈到了ClassLoader的几类加载器,而ClassLoader使用双亲委派机制来加载class文件的。ClassLoader的双亲委派机制是这样的(这里先忽略掉自定义类加载器 CustomClassLoader):

1)当 AppClassLoader 加载一个 class 时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父

类加载器 ExtClassLoader 去完成。

2)当 ExtClassLoader 加载一个 class 时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给

BootStrapClassLoader 去完成。

3)如果 BootStrapClassLoader 加载失败(例如在$JAVA_HOME$/jre/lib 里未查找到该 class),会使用

ExtClassLoader 来尝试加载;

4)若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败,

则会报出异常 ClassNotFoundException。

下面贴下 ClassLoader 的 loadClass(String name, boolean resolve)的源码:

protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    // 首先找缓存是否有 class
    Class c = findLoadedClass(name);
    if (c == null) {
        //没有判断有没有父类
        try {
            if (parent != null) {
                //有的话,用父类递归获取 class
                c = parent.loadClass(name, false);
            } else {
                //没有父类。通过这个方法来加载
                c = findBootstrapClassOrNull(name);
            }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
        if (c == null) {
            // 如果还是没有找到,调用 findClass(name)去找这个类
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

代码很明朗:首先找缓存(findLoadedClass),没有的话就判断有没有 parent,有的话就用 parent 来递归的 loadClass,然而 ExtClassLoader 并没有设置 parent,则会通过 findBootstrapClassOrNull 来加载 class,而findBootstrapClassOrNull 则会通过 JNI 方法”private native Class findBootstrapClass(String name)“来使用 BootStrapClassLoader 来加载 class。

然后如果 parent 未找到 class,则会调用 findClass 来加载 class,findClass 是一个 protected 的空方法,可以覆盖它以便自定义 class 加载过程。

另外,虽然 ClassLoader 加载类是使用 loadClass 方法,但是鼓励用 ClassLoader 的子类重写findClass(String),而不是重写 loadClass,这样就不会覆盖了类加载默认的双亲委派机制。

双亲委派托机制为什么安全?

举个例子,ClassLoader 加载的 class 文件来源很多,比如编译器编译生成的 class、或者网络下载的字节码。

而一些来源的 class 文件是不可靠的,比如我可以自定义一个 java.lang.Integer 类来覆盖 jdk 中默认的 Integer类,例如下面这样:

package java.lang;
    public class Integer {
        public Integer(int value) {
        System.exit(0);
    }
}

初始化这个 Integer 的构造器是会退出 JVM,破坏应用程序的正常进行,如果使用双亲委派机制的话该Integer 类永远不会被调用,以为委托 BootStrapClassLoader 加载后会加载 JDK 中的 Integer 类而不会加载自定义的这个,可以看下下面这测试个用例:

public static void main(String... args) {
    Integer i = new Integer(1);
    System.err.println(i);
}

执行时 JVM 并未在 new Integer(1)时退出,说明未使用自定义的 Integer,于是就保证了安全性 。

4.4、描述下JVM加载class

JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运
行时系统组件,它负责在运行时查找和装入类文件中的类。

由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对类进行初始化,包括:

如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;

如果类中存在初始化语句,就依次执行这些初始化语句。类的加载是由类加载器完成的,类加载器包括:根加载器
(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。
从 Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的 Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。
下面是关于几个类加载器的说明:

  • Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);
  • Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap;
  • System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量 classpath

或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。

4.5、获得一个类对象有哪些方式?

  • 类型.class,例如:String.class
  • 对象.getClass(),例如:”hello”.getClass()
  • Class.forName(),例如:Class.forName(“java.lang.String”)

4.6、在开发中遇到的内存溢出有哪些?

引起内存溢出的原因有很多种,常见的有以下几种:

  • 1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  • 2.集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收;
  • 3.代码中存在死循环或循环产生过多重复的对象实体;
  • 4.使用的第三方软件中的 BUG;
  • 5.启动参数内存值设定的过小;

内存溢出的解决方案:

  • 第一步,修改 JVM 启动参数,直接增加内存。(-Xms,-Xmx 参数一定不要忘记加。)
  • 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
  • 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

重点排查以下几点:

  • 1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
  • 2.检查代码中是否有死循环或递归调用。
  • 3.检查是否有大循环重复产生新对象实体。
  • 4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中 数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
  • 5.检查 List、MAP 等集合对象是否有使用完后,未清除的问题。List、MAP 等集合对象会始终存有对对象的引用,使得这些对象不能被 GC 回收。
  • 第四步,使用内存查看工具动态查看内存使用情况。

猜你喜欢

转载自blog.csdn.net/MyronCham/article/details/87482343