原创

13.Java深入问题


目录介绍

  • 13.0.0.1 通过代码案例分析Java内存分配情况?JVM加载类过程是怎样的?如何对构造方法赋值?分析通过对象调用方法?
  • 13.0.0.2 强引用会被回收吗?软引用的特点?软引用使用场景?当软引用持有多个对象时,如何被回收,回收规则是什么?
  • 13.0.0.3 弱引用有何特点?弱引用被回收是如何做到的?弱引用实际开发案例有哪些?什么时候使用软引用或者弱引用呢?
  • 13.0.0.4 Hash的使用场景有哪些?Hash表是干什么的?hash表具体是如何提高查找的速度,说说你的理解?
  • 13.0.0.5 HashCode的作用?可直接用hashcode判断两个对象是否相等?HashMap中是如何使用HashCode提高去重的逻辑?
  • 13.0.0.6 Hashcode与equal区别?何时需用到hashcode?如何解决Hash冲突?当两个对象 hashcode 相同时如何获取值对象?
  • 13.0.0.9 transient的作用是什么?transient使用场景?transient使用需要注意什么,底层是如何操作的?
  • 13.0.0.8 工作内存和主内存的关系?它们的作用分别是什么?
  • 13.0.1.0 Object有哪些公用方法?这些方法有哪些作用?wait(),notify(),notifyAll()为什么定义在Object类中?

13.0.0.1 通过代码案例分析Java内存分配情况?JVM加载类过程是怎样的?如何对构造方法赋值?分析通过对象调用方法?

  • 通过代码案例分析Java内存分配情况?

    • 以下面代码为例,来分析,Java 的实例对象在内存中的空间分配。
    //JVM 启动时将 Person.class 放入方法区
    public class Person {
    
        //静态变量,直接放到常量池中
        public static final String number = "13667225184";
    
    	//new Person 创建实例后,name 引用放入堆区,name 对象放入常量池
        private String name;
    
    	//new Person 创建实例后,age = 0 放入堆区
        private int age;
    
    	//Person 方法放入方法区,方法内代码作为 Code 属性放入方法区
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
    	//toString 方法放入方法区,方法内代码作为 Code 属性放入方法区
        @Override
        public String toString() {
            return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
        }
    }
    
    //JVM 启动时将 Test.class 放入方法区
    public class Test {
    
    	//main 方法放入方法区,方法内代码作为 Code 属性放入方法区
        public static void main(String[] args) {
    
            //person1 是引用放入虚拟机栈区,new 关键字开辟堆内存 Person 自定义对象放入堆区
            Person person1 = new Person("张三", 18);
            Person person2 = new Person("李四", 20);
    
            //通过 person 引用创建 toString() 方法栈帧
            person1.toString();
            person2.toString();
        }
    }
    
  • JVM加载类过程是怎样的?

    • 首先 JVM 会将 Test.class, Person.class 加载到方法区,找到有 main() 方法的类开始执行。
    • image
    • 分析步骤
      • 如上图所示,JVM 找到 main() 方法入口,创建 main() 方法的栈帧放入虚拟机栈,开始执行 main() 方法。
      • Person person1 = new Person("张三", 18);
      • 执行到这句代码时,JVM 会先创建 Person。实例放入堆区,person2 也同理。
  • 如何对构造方法赋值?

    • 创建完 Person 两个实例,main() 方法中的 person1,person2 会指向堆区中的 0x001,0x002(这里的内存地址仅作为示范)。紧接着会调用 Person 的构造函数进行赋值,如下图:
      • image
    • 如上图所示,新创建的的 Person 实例中的 name, age 开始都是默认值。 调用构造函数之后进行赋值,name 是 String 引用类型,会在常量池中创建并将地址赋值给 name,age 是基本数据类型将直接保存数值。
    • 注:Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte, Short, Integer, Long, Character, Boolean,另外两种浮点数类型的包装类则没有实现。
      | 基本数据类型	| 包装类(是否实现了常量池技术)  |
      | :---------	| :-----------------------		|
      | byte			| Byte	是						|
      | boolean		| Boolean	是					|
      | short			| Short	是						|
      | char			| Character	是					|
      | int			| Integer	是					|
      | long			| Long	是						|
      | float			| Float	否						|
      | double		| Double	否					|
      
  • 分析通过对象调用方法?

    • Person 实例初始化完后,执行到 toString() 方法,同 main() 方法一样 JVM 会创建一个 toString() 的栈帧放入虚拟机栈中,执行完之后返回一个值。
    • image

13.0.0.2 强引用会被回收吗?软引用的特点?软引用使用场景?当软引用持有多个对象时,如何被回收,回收规则是什么?

  • 强引用会被回收吗?
    • 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
    • 通过引用,可以对堆中的对象进行操作。在某个函数中,当创建了一个对象,该对象被分配在堆中,通过这个对象的引用才能对这个对象进行操作。
  • 软引用的特点
    • 如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
  • 软引用使用场景?
    • 正常是用来处理图片这种占用内存大的情况
      • 代码如下所示
      View view = findViewById(R.id.button);
      Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
      Drawable drawable = new BitmapDrawable(bitmap);
      SoftReference<Drawable> drawableSoftReference = new SoftReference<Drawable>(drawable);
      if(drawableSoftReference != null) {
          view.setBackground(drawableSoftReference.get());
      }
      
    • 这样使用软引用好处
      • 通过软引用的get()方法,取得drawable对象实例的强引用,发现对象被未回收。在GC在内存充足的情况下,不会回收软引用对象。此时view的背景显示
      • 实际情况中,我们会获取很多图片.然后可能给很多个view展示, 这种情况下很容易内存吃紧导致oom,内存吃紧,系统开始会GC。这次GC后,drawables.get()不再返回Drawable对象,而是返回null,这时屏幕上背景图不显示,说明在系统内存紧张的情况下,软引用被回收。
      • 使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。
  • 当软引用持有多个对象时,如何被回收,回收规则是什么?
    • 当这个SoftReference所软引用的aMyOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。
    • 在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。
    • 常用的方式为
    SoftReference ref = null;
    while ((ref = (EmployeeRef) q.poll()) != null) {
        // 清除ref
    }
    
  • 注意避免软引用获取对象为null
    • 在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。所以在获取软引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃。

13.0.0.3 弱引用有何特点?弱引用被回收是如何做到的?弱引用实际开发案例有哪些?什么时候使用软引用或者弱引用呢?

  • 弱引用WeakReference有何特点
    • 弱引用–>随时可能会被垃圾回收器回收,不一定要等到虚拟机内存不足时才强制回收。要获取对象时,同样可以调用get方法。
  • 弱引用被回收是如何做到的?
    • 如果一个对象只具有弱引用,那么在垃圾回收器线程扫描的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
    • 弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
  • 弱引用实际开发案例有哪些?
    • 先看一个handler小案例【千万不要忽视淡黄色警告】
      • image
    • 为什么这样会造成内存泄漏
      • 这种情况就是由于android的特殊机制造成的:当一个android主线程被创建的时候,同时会有一个Looper对象被创建,而这个Looper对象会实现一个MessageQueue(消息队列),当我们创建一个handler对象时,而handler的作用就是放入和取出消息从这个消息队列中,每当我们通过handler将一个msg放入消息队列时,这个msg就会持有一个handler对象的引用。因此当Activity被结束后,这个msg在被取出来之前,这msg会继续存活,但是这个msg持有handler的引用,而handler在Activity中创建,会持有Activity的引用,因而当Activity结束后,Activity对象并不能够被gc回收,因而出现内存泄漏。
    • 根本原因
      • Activity在被结束之后,MessageQueue并不会随之被结束,如果这个消息队列中存在msg,则导致持有handler的引用,但是又由于Activity被结束了,msg无法被处理,从而导致永久持有handler对象,handler永久持有Activity对象,于是发生内存泄漏。但是为什么为static类型就会解决这个问题呢?因为在java中所有非静态的对象都会持有当前类的强引用,而静态对象则只会持有当前类的弱引用。声明为静态后,handler将会持有一个Activity的弱引用,而弱引用会很容易被gc回收,这样就能解决Activity结束后,gc却无法回收的情况。
  • 什么时候使用软引用或者弱引用呢?
    • 个人认为,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
    • 还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
    • 另外,和弱引用功能类似的是WeakHashMap。WeakHashMap对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,回收以后,其条目从映射中有效地移除。WeakHashMap使用ReferenceQueue实现的这种机制。
  • 虚引用持有对应引用吗?
    • 注意网上有些文章说虚引用不持有对象的引用,这是有误的,通过构造函数可以看到虚引用是持有对象引用的,但是无法获取该引用。
    • 可以看到get函数返回null,正如前面说得虚引用无法获取对象引用。这里注意:不仅仅是虚引用可以判断回收,弱引用和软引用同样实现了带有ReferenceQueue的构造函数,如果创建时传入了一个ReferenceQueue对象,同样也可以判断。
    public class PhantomReference<T> extends Reference<T> { 
        public T get() { 
            return null; 
        } 
        public PhantomReference(T referent, ReferenceQueue<? super T> q) { 
            super(referent, q); 
        } 
    }
    

13.0.0.4 Hash的使用场景有哪些?Hash表是干什么的?hash表具体是如何提高查找的速度,说说你的理解?

  • Hash的使用场景
    • 比如说我们下载一个文件,文件的下载过程中会经过很多网络服务器、路由器的中转,如何保证这个文件就是我们所需要的呢?我们不可能去一一检测这个文件的每个字节,也不能简单地利用文件名、文件大小这些极容易伪装的信息,这时候,就需要一种指纹一样的标志来检查文件的可靠性,这种指纹就是我们现在所用的Hash算法(也叫散列算法)。
    • 散列算法就是一种以较短的信息来保证文件唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。因此,当原有文件发生改变时,其标志值也会发生改变,从而告诉文件使用者当前的文件已经不是你所需求的文件。
    • 这种标志有何意义呢?之前文件下载过程就是一个很好的例子,事实上,现在大部分的网络部署和版本控制工具都在使用散列算法来保证文件可靠性。
  • Hash表是干什么的?
    • 将k作为输入值,h(k)输出值作为内存地址,该内存地址用来存放value,然后可以通过k获取到value存放的地址,从而获取value信息。
    • 也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。哈希表的实现主要需要解决两个问题,哈希函数和冲突解决。
  • hash表具体是如何提高查找的速度?

13.0.0.5 HashCode的作用?可直接用hashcode判断两个对象是否相等?HashMap中是如何使用HashCode提高去重的逻辑?

  • HashCode的作用
    • 减少查找次数,提高程序效率
    • 例如查找是否存在重复值
      • h(k1)≠h(k2)则k1≠k2
      • 首先查看h(k2)输出值(内存地址),查看该内存地址是否存在值;
      • 如果无,则表示该值不存在重复值;
      • 如果有,则进行值比较,相同则表示该值已经存在散列列表中,如果不相同则再进行一个一个值比较;而无需一开始就一个一个值的比较,减少了查找次数
  • 可直接用hashcode判断两个对象是否相等?
    • 肯定是不可以的,因为不同的对象可能会生成相同的hashcode值。虽然不能根据hashcode值判断两个对象是否相等,但是可以直接根据hashcode值判断两个对象不等,如果两个对象的hashcode值不等,则必定是两个不同的对象。如果要判断两个对象是否真正相等,必须通过equals方法。
    • 也就是说对于两个对象,如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;
      • 如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;
      • 如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;
      • 如果两个对象的hashcode值相等,则equals方法得到的结果未知。
  • HashMap中是如何使用HashCode提高去重的逻辑?
    • 在Java中也一样,hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。
    • 为什么这么说呢?考虑一种情况,当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?(注意:集合中不允许重复的元素存在)
      • 也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。
      • 此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。下面这段代码是java.util.HashMap的中put方法的具体实现:
    • put方法是用来向HashMap中添加新的元素,从put方法的具体实现可知,会先调用hashCode方法得到该元素的hashCode值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将新的元素添加到HashMap中。从这里可以看出,hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。

13.0.0.6 Hashcode与equal区别?何时需用到hashcode?如何解决Hash冲突?当两个对象 hashcode 相同时如何获取值对象?

  • Hashcode与equal区别
    • equals()比较两个对象的地址值是否相等 ;hashCode()得到的是对象的存储位置,可能不同对象会得到相同值
    • 有两个对象,若equals()相等,则hashcode()一定相等;hashcode()不等,则equals()一定不相等;hashcode()相等,equals()可能相等、可能不等
    • 使用equals()比较两个对象是否相等效率较低,最快办法是先用hashCode()比较,如果hashCode()不相等,则这两个对象肯定不相等;如果hashCode()相等,此时再用equal()比较,如果equal()也相等,则这两个对象的确相等。
  • 什么时候需要用到hashcode
    • 同样用于鉴定2个对象是否相等的,java集合中其中 set不允许元素重复实现,那个这个不允许重复实现的方法,如果用 equal 去比较的话,如果存在1000个元素,你 new 一个新的元素出来,需要去调用1000次 equal 去逐个和他们比较是否是同一个对象,这样会大大降低效率。hashcode实际上是返回对象的存储地址,如果这个位置上没有元素,就把元素直接存储在上面,如果这个位置上已经存在元素,这个时候才去调用equal方法与新元素进行比较,相同的话就不存了,散列到其他地址上
    • 技术博客大总结
  • 如何解决Hash冲突
    • 开放定址法:常见的线性探测方式,在冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表
    • 链地址法:将有冲突数组位置生出链表
    • 建立公共溢出区:将哈希表分为基本表和溢出表两部分,和基本表发生冲突的元素一律填入溢出表
    • 再哈希法:构造多个不同的哈希函数,有冲突使用下一个哈希函数计算hash值
  • 当两个对象 hashcode 相同时如何获取值对象?

13.0.0.8 工作内存和主内存的关系?它们的作用分别是什么?

  • Java内存模型就是通过定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
  • 其中 ,主内存(Main Memory)是所有变量的存储位置,每条线程还有自己的工作内存,用于保存被该线程使用到的变量的主内存副本拷贝。为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中。
  • image

13.0.0.9 transient的作用是什么?transient使用场景?transient使用需要注意什么,底层是如何操作的?

  • transient的作用是什么
    • 我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
    • java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
  • transient使用场景
    • 然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
  • transient使用需要注意什么
    • 1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
    • 2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
    • 3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

13.0.1.0 Object有哪些公用方法?这些方法有哪些作用?wait(),notify(),notifyAll()为什么定义在Object类中?

  • Object有哪些公用方法?这些方法有哪些作用?
    • Object()
      • 默认构造方法
    • clone()
      • 创建并返回此对象的一个副本。
    • equals(Object obj)
      • 指示某个其他对象是否与此对象“相等”。
    • hashCode()
      • 返回该对象的哈希码值。
    • finalize()
      • 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
    • getClass()
      • 返回一个对象的运行时类。
    • notify()
      • 唤醒在此对象监视器上等待的单个线程。
    • notifyAll()
      • 唤醒在此对象监视器上等待的所有线程。
    • toString()
      • 返回该对象的字符串表示。
    • wait()
      • 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
    • wait(long timeout)
      • 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
    • wait(long timeout, int nanos)
      • 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
  • wait(),notify(),notifyAll()为什么定义在Object类中?
    • a、这些方法用于同步中
    • b、使用这些方法时必须要标识所属的同步的锁
    • c、锁可以是任意对象【或者说因为每个对象都有一把锁】,所以任意对象调用的方法一定是定义在Object类中

评论

留言