android面试---java 深入源码

tech2024-05-14  83

1、哪些情况下的对象会被垃圾回收机制处理掉?

哪些情况下的对象会被垃圾回收机制处理掉

1.所有实例都没有活动线程访问。

2.没有被其他任何实例访问的循环引用实例。 3.Java 中有不同的引用类型。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。

要判断怎样的对象是没用的对象

1.采用标记计数的方法: 给内存中的对象给打上标记,对象被引用一次,计数就加 1,引用被释放了,计数就减一,当这个计数为 0 的时候,这个对象就可以被回收了。当然,这也就引发了一个问题:循环引用的对象是无法被识别出来并且被回收的。所以就有了第二种方法:

2.采用根搜索算法:从一个根出发,搜索所有的可达对象,这样剩下的那些对象就是需要被回收的

2、什么是强引用、软引用、弱引用以及虚引用?

整体结构

java 提供了 4 中引用类型,在垃圾回收的时候,都有自己的各自特点。

 

为什么要区分这么多引用呢,其实这和 Java GC 有密切关系。

强引用(默认支持模式)

把一个对象赋给一个引用变量,这个引用变量就是一个强引用。强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着当内存不足的时候,jvm 开始垃圾回收,对于强引用的对象,就算出现 OOM 也不会回收该对象的。

因此,强引用是造成 java 内存泄露的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示的将引用赋值为 nullGC 就会回收这个对象了。

 

软引用(SoftReference)

软引用是一种相对强化引用弱化了一些引用,需要使用 java.lang.SoftReference 类来实现。对于只有软引用的对象来说,当系统内存充足时,不会被回收;当系统内存不足时,会被回收;软引用适合用于缓存,当内存不足的时候把它删除掉,使用的时候再加载进来

 

弱引用

弱引用需要用 java.lang.WeakReference 类来实现,它比软引用的生存期更短。

* 如果一个对象只是被弱引用引用者,那么只要发生 GC,不管内存空间是否足够,都会回收该对象。

弱引用适合解决某些地方的内存泄漏的问题ThreadLocal 静态内部类 ThreadLocalMap 中的 Entiry 中的 key 就是一个虚引用;

 

WeakHashMap 的键是弱键”,也就是键的引用是一个弱引用。

 

结果:map 为空了。

理论上我们只是把引用变量 key 变成 null 了,"wekHashMap"字符串应该被 Map key 引用啊,不应该被 GC 回收啊,

但是因为 key 是弱引用,GC 回收的时候就忽略了这个引用,把对象当成垃圾收回了。

虚引用

虚引用需要 java. langref.PhantomReference 类来实现。顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象仅被虛引用持有,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

它不能单独使用也不能通过它访问对象,虚引用必须和引用队列( Reference queue) 联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 fi nalize 以后,做某些事情的机制。PhantomReference get 方法总是返回 null,因此无法访问对应的引用对象。

使用它的意义在于说明一个对象已经进入 finalization阶段,可以被回收,用来实现比 f inalization 机制更灵活的回收操作

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理;

虚引用用来管理堆外内存

 

ReferenceQueue 引用队列

对象在被回收之前要被引用队列保存一下。GC 之前对象不放在队列中,GC 之后才对象放入队列中。【通过开启线程监听该引用队列的变化情况】就可以在对象被回收时采取相应的动作。

由于虚引用的唯一目的就是能在这个对象被垃圾收集器回收时能收到系统通知,因而创建虚引用时必须要关联一个引用队列,而软引用和弱引用则不是必须的。

这里所谓的收到系统通知其实还是通过开启线程监听该引用队列的变化情况来实现的。

这里还需要强调的是,对于软引用和弱引用,当执行第一次垃圾回收时,就会将软引用或弱引用对象添加到其关联的引用队列中,然后其 finalize 函数才会被执行(如果没复写则不会被执行);而对于虚引用,如果被引用对象没有复写 finalize 方法,则是在第一垃圾回收将该类销毁之后,才会将虚拟引用对象添加到引用队列,

如果被引用对象复写了 finalize 方法,则是当执行完第二次垃圾回收之后,才会将虚引用对象添加到其关联的引用队列

一个对象的 finalize()方法只会被调用一次,而且 finalize()被调用不意味着 gc 会立即回收该对象,所以有可能调用 finalize()后,

该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用 finalize(),产生问题,所以,推荐不要使用 finalize()方法

 

软引用:SoftReference 的应用场景

假如有一个应用需要读取大量的本地图片每次读取图片都从硬盘读取会影响性能。

一次全部加载到内存中,又可能造成内存溢出。

此时,可以使用软引用解决问题;使用一个 HashMap 保存图片的路径和响应图片对象关联的软引用之间的映射关系,内存不足时,jvm 会自动回收这些缓存图片对象所占用的空间,可以避免 OOM

3、什么是依赖注入?能说几个依赖注入的库么?你使用过哪些?

依赖注入,是 IOC 的一个方面,是个通常的概念,它有多种解释。这概念是说你不用创建对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述哪些组件需要哪些服务,之后一个容器(IOC 容器)负责把他们组装起来。

构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。

Setter 方法注入:Setter 方法注入是容器通过调用无参构造器或无参 static 工厂方法实例化 bean 之后,调用该 bean 的 setter 方法,即实现了基于 setter 的依赖注入。

Spring 框架支持以下五种 bean 的作用域:

singleton : bean 在每个 Spring ioc 容器中只有一个实例。

prototype:一个 bean 的定义可以有多个实例。

request:每次 http 请求都会创建一个 bean,该作用域仅在基于 web 的 Spring ApplicationContext 情形下有效。

session:在一个 HTTP Session 中,一个 bean 定义对应一个实例。该作用域仅在基于 web 的 Spring ApplicationContext 情形下有效。

global-session:在一个全局的 HTTP Session 中,一个 bean 定义对应一个实例。

该作用域仅在基于 web 的 Spring ApplicationContext 情形下有效。

缺省的 Spring bean 的作用域是 Singleton.

1,Spring 容器从 XML 文件中读取 bean 的定义,并实例化 bean。

2,Spring 根据 bean 的定义填充所有的属性。

3,如果 bean 实现了 BeanNameAware 接口,Spring 传递 bean 的

ID 到 setBeanName 方法。

4,如果 Bean 实现了 BeanFactoryAware 接口, Spring 传递 beanfactory 给 setBeanFactory 方法。

5,如果有任何与 bean 相关联的 BeanPostProcessors,Spring 会在 postProcesserBeforeInitialization()方法内调用它们。

6,如果 bean 实现 IntializingBean 了,调用它的 afterPropertySet 方法,如果 bean 声明了初始化方法,调用此初始化方法。

7,如果有 BeanPostProcessors 和 bean 关联,这些 bean 的 postProcessAfterInitialization() 方法将被调用。

8,如果 bean 实现了 DisposableBean,它将调用 destroy()方法。

4、关键字 synchronized 的作用是什么?

确保线程互斥地访问同步代码 保证共享变量的修改能够及时可见 有效解决重排序问题 用法: 修饰普通方法 修饰静态方法 指定对象,修饰代码块 &...

5、什么是 ThreadPoolExecutor

 

定义

编辑

线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。

为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展钩子 (hook)。但是,强烈建议程序员使用较为方便的 Executors 工厂方法 Executors.newCachedThreadPool() (无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和 Executors.newSingleThreadExecutor()(单个后台线程),它们均为大多数使用场景预定义了设置。否则,在手动配置和调整此类时,使用以下指导:

核心和最大池大小

ThreadPoolExecutor 将根据 corePoolSize(参见 getCorePoolSize())和

maximumPoolSize(参见 getMaximumPoolSize())设置的边界自动调整池大小。当新任务在方法 execute(java.lang.Runnable) 中提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。如果设置的 corePoolSize maximumPoolSize 相同,则创建了固定大小的线程池。如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。在大多数情况下,核心和最大池大小仅基于构造来设置,不过也可以使用 setCorePoolSize(int) setMaximumPoolSize(int) 进行动态更改。

ThreadPoolExecutor 设置

编辑

ThreadPoolExecutor 按需构造

默认情况下,即使核心线程最初只是在新任务到达时才创建和启动的,也可以使用方法 prestartCoreThread() prestartAllCoreThreads() 对其进行动态重写。如果构造带有非空队列的池,则可能希望预先启动线程。

ThreadPoolExecutor 创建新线程

使用 ThreadFactory 创建新线程。如果没有另外说明,则在同一个 ThreadGroup 中一律使用 Executors.defaultThreadFactory() 创建线程,并且这些线程具有相同的

NORM_PRIORITY 优先级和非守护进程状态。通过提供不同的 ThreadFactory,可以改变线程的名称、线程组、优先级、守护进程状态,等等。如果从 newThread 返回 null

ThreadFactory 未能创建线程,则执行程序将继续运行,但不能执行任何任务。

ThreadPoolExecutor 保持活动时间

如果池中当前有多于 corePoolSize 的线程,则这些多出的线程在空闲时间超过

keepAliveTime 时将会终止(参见 getKeepAliveTime(java.util.concurrent.TimeUnit))。这提供了当池处于非活动状态时减少资源消耗的方法。如果池后来变得更为活动,则可以创建新的线程。也可以使用方法 setKeepAliveTime(long, java.util.concurrent.TimeUnit) 动态地更改此参数。使用 Long.MAX_VALUE TimeUnit.NANOSECONDS 的值在关闭前有效地从以前的终止状态禁用空闲线程。默认情况下,保持活动策略只在有多于

corePoolSizeThreads 的线程时应用。但是只要 keepAliveTime 值非 0

allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。

ThreadPoolExecutor 排队

编辑

所有 BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:

如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。

如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。

如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

排队有三种通用策略:

直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过

corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

被拒绝的任务

Executor 已经关闭,并且 Executor 将有限边界用于最大线程和工作队列容量,且已经饱和时,在方法 execute(java.lang.Runnable) 中提交的新任务将被拒绝。在以上两种情况下,execute 方法都将调用其 RejectedExecutionHandler

RejectedExecutionHandler.rejectedExecution(java.lang.Runnable,

java.util.concurrent.ThreadPoolExecutor) 方法。下面提供了四种预定义的处理程序策略:

在默认的 ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时

RejectedExecutionException

ThreadPoolExecutor.CallerRunsPolicy 中,线程调用运行该任务的 execute 本身。

此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

ThreadPoolExecutor.DiscardPolicy 中,不能执行的任务将被删除。

ThreadPoolExecutor.DiscardOldestPolicy 中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。定义和使用其他种类的 RejectedExecutionHandler 类也是可能的,但这样做需要非常小心,尤其是当策略仅用于特定容量或排队策略时。

ThreadPoolExecutor 使用方法

编辑钩子 (hook) 方法

此类提供 protected 可重写的 beforeExecute(java.lang.Thread, java.lang.Runnable) afterExecute(java.lang.Runnable, java.lang.Throwable) 方法,这两种方法分别在执行

每个任务之前和之后调用。它们可用于操纵执行环境;例如,重新初始化 ThreadLocal、搜集统计信息或添加日志条目。此外,还可以重写方法 terminated() 来执行 Executor 完全终止后需要完成的所有特殊处理。

如果钩子 (hook) 或回调方法抛出异常,则内部辅助线程将依次失败并突然终止。

ThreadPoolExecutor 队列维护

编辑

方法 getQueue() 允许出于监控和调试目的而访问工作队列。强烈反对出于其他任何目的而使用此方法。remove(java.lang.Runnable) purge() 这两种方法可用于在取消大量已排队任务时帮助进行存储回收。

终止

程序 AND 不再引用的池没有剩余线程会自动 shutdown。如果希望确保回收取消引用

的池(即使用户忘记调用 shutdown()),则必须安排未使用的线程最终终止:设置适当保持活动时间,使用 0 核心线程的下边界和/或设置 allowCoreThreadTimeOut(boolean)

扩展示例。此类的大多数扩展可以重写一个或多个受保护的钩子 (hook) 方法。例如,

下面是一个添加了简单的暂停/恢复功能的子类:

class PausableThreadPoolExecutor extends ThreadPoolExecutor { private boolean

isPaused; private ReentrantLock pauseLock = new ReentrantLock(); private Condition unpaused = pauseLock.newCondition(); public PausableThreadPoolExecutor(...) { super(...); } protected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r); pauseLock.lock(); try { while (isPaused) unpaused.await(); } catch(InterruptedException ie) { t.interrupt(); } finally { pauseLock.unlock(); } } public void pause() { pauseLock.lock(); try { isPaused = true; } finally { pauseLock.unlock(); } } public void resume()

{ pauseLock.lock(); try { isPaused = false; unpaused.signalAll(); } finally

{ pauseLock.unlock(); } } }

6、线程池如何定义合适的线程

一、线程池如何配置合理线程数

CPU 密集型:

定义:CPU 密集型的意思就是该任务需要大量运算,而没有阻塞,CPU 一直全速运行。

CPU 密集型任务只有在真正的多核 CPU 上才可能得到加速(通过多线程)。

CPU 密集型任务配置尽可能少的线程数。

CPU 密集型线程数配置公式:(CPU 核数+1)个线程的线程池

IO 密集型:

定义:IO 密集型,即该任务需要大量的 IO,即大量的阻塞。

在单线程上运行 IO 密集型任务会导致浪费大量的 CPU 运算能力浪费在等待。

所以 IO 密集型任务中使用多线程可以大大的加速程序运行,即使在单核 CPU 上,这种加速主要利用了被浪费掉的阻塞时间。

第一种配置方式:由于 IO 密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程。

配置公式:CPU 核数 * 2

第二种配置方式:

IO 密集型时,大部分线程都阻塞,故需要多配置线程数。

配置公式:CPU 核数 / (1 – 阻塞系数)0.8~0.9 之间)比如:8 核 / (1 – 0.9) = 80 个线程数二、自定义线程池

 

三、实际使用JDK内置的哪个线程池?

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors 返回的线程池对象的弊端如下:

FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为

Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为

Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

 

.

 

8、utf-8 编码中的中文占几个字节;int 型几个字节?

utf-8 的编码规则:

如果一个字节,最高位为 0,表示这是一个 ASCII 字符(00~7F)如果一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数

一个 utf8 数字占 1 个字节

一个 utf8 英文字母占 1 个字节

少数是汉字每个占用 3 个字节,多数占用 4 个字节。

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

代理模式

代理模式(Proxy Pattern),又叫委托模式,是指为其他对象提供一种代理,以

控制对这个对象的访问,属于结构型设计模式目的是为了保护目标对象或增强目标对象

 

抽象角色-Subject:可以是抽象类,也可以是接口

真实角色/委托类-RealSubject:业务逻辑的具体执行者

代理角色/代理类-Proxy:把所有抽象主题角色定义的方法限制委托给具体主题角色实现,并且在具体主题角色处理完毕前后做预处理和善后处理隐藏委托类,在一定程度上实现了解耦合,同时提高了安全性,符合开闭原则(扩展开放、修改封闭)静态代理

显示声明代理对象,在编译期就生成了代理类由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行之前,代理类的类文件就已经被创建了实现:一个公共接口、一个委托类、一个代理类优点:简单、实用、效率高缺点:需要实现接口,造成代码冗余只能对固定接口的实现类实现代理,灵活性较差动态代理

在程序运行时通过反射机制动态创建代理类优点:只需将被委托类作为参数传入即可,使用灵活;服务内容只需写在 invoke 方法中,减少了代码冗余

缺点:效率较低

基于 JDK 实现:通过 JDK 提供的工具方法 Proxy.newProxyInstance 动态构建全新的代理类(继承 Proxy 类,并持有 InvocationHandler 接口引用)字节码文件并实例化对象返回。由 Java 内部的反射机制来实例化代理对象,并代理的调用委托类方法

基于 CGlib 实现:基于继承被代理类生成代理子类,不用实现接口,只需被代理类为非 final 类,底层借用 ASM 字节码技术实现基于 AspectJ 实现:修改目标类的字节,织入代理的字节,在程序编译的时候,插入动态代理的字节码,不会生成全新的 class 基于 Instrumentation 实现:修改目标类的字节码,类装载的时候动态拦截,插入动态代理的字节码,不会生成全新的 class,基于 JavaAgent

SpringAOP 的实现基础

ASM:Automatic Storage Management 自动存储管理

AOP 三剑客:

APT:Annotation Processing Tool 注解处理器

AspectJ:面向切面的框架

Javassit:开源的分析、编辑和创建 Java 字节码的类库

 

10、Java 的异常体系

 

11、谈谈你对解析与分派的认识。

解析

Java 中方法调用的目标方法在 Class 文件里面都是常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。(关于符号引用与直接引用,详见【深入理解 JVM】:Class 类文件结构这种解析的前提是:方法在程序真正运行之前就有一个可以确定的调用版本,并且这个方法的调用版本在运行期是不可改变的,即“编译期可知,运行期不可变”,这类目标的方法的调用称为解析(Resolve)。

只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合条件的有静态方法(invokestatic 指令)、私有方法、实例构造方法、父类方法(这 3 个是 invokespecial 指令),它们在类加载的的解析阶段就会将符号引用解析为该方法的直接引用。

关于解析的具体过程详见【深入理解 JVM】:类加载机制

分派解析调用一定是个静态的过程,在编译期就完全确定,在类加载的解析阶段就将涉及的符号引用全部转变为可以确定的直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动态的。分派是多态性的体现,

Java 虚拟机底层提供了我们开发中“重载”(Overload)“和重写”(Override)的底层实现。其中重载属于静态分派,而重写则是动态分派的过程。

静态分派静态分派只会涉及重载(Oveload),而重载是在编译期间确定的,那么静态分派自然是一个静态的过程(因为还没有涉及到 Java 虚拟机)。静态分派的最直接的解释是在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。因此在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪个重载版本。

下面的代码演示了静态分派(重载)的过程:

 

 

首先弄清楚什么是静态类型和实际类型,在语句Human man = new Man();中,

Human 称为变量的静态类型(Static Type)或者外观类型(Apparent Type),Man 称为变量的实际类型(Actual Type)。静态类型和实际类型在程序中都可以发生变化,但是静态类型的变化仅仅发生在使用时,变量本身的静态类型不会改变,最终的静态类型在编译期是可知的;而实际类型变化的结果在运行期才可以确定,编译时并不知道一个对象的实际类型是什么。例如:

 

在该实例代码中,main()方法的两次调用 sayHello(),在方法接收者已经确定是对象 sr 的前提下,使用哪个重载版本就完全取决于传入参数的数量和数据类型。代码中使用了两个静态类型相同而实际类型不同的变量,但是 Javac 编译期在重载时是通过参数的静态类型而不是实际类型作为判定依据的,man 和 woman 的静态类型都是 Human。静态类型在编译期可知,因此在编译阶段,编译期根据 man 和 woman 的静态类型为 Human 的事实,选择 sayHello(Human)作为调用目标,这就是方法重载的本质。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派是还没有涉及到虚拟机,由编译期执行。虽然编译器能够在编译阶段确定方法的版本,但是很多情况下重载的版本不是唯一的,在这种模糊的情况下,编译器会选择一个更合适的版本。

动态分派动态分派的一个最直接的例子是重写(Override)。对于重写,我们已经很熟悉

了,那么 Java 虚拟机是如何在程序运行期间确定方法的执行版本的呢?

解释这个现象,就不得不涉及 Java 虚拟机的 invokevirtual 指令了,这个指令的解析过程有助于我们更深刻理解重写的本质。该指令的具体解析过程如下:

找到操作数栈栈顶的第一个元素所指向的对象的实际类型,记为 C

如果在类型 C 中找到与常量中描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回非法访问异常

如果在类型 C 中没有找到,则按照继承关系从下到上依次对 C 的各个父类进行第

2 步的搜索和验证过程

如果始终没有找到合适的方法,则抛出抽象方法错误的异常

从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者称为接受者)的实际类型,所以当调用 invokevirtual 指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。

动态分派的实例代码如下:

 

虚拟机动态分派的实现上面的叙述已经把虚拟机重写与重载的本质讲清楚了,那么 Java 虚拟机是如何做到这点的呢?

由于动态分派是非常频繁的操作,实际实现中不可能真正如此实现。Java 虚拟机

是通过“稳定优化”的手段——在方法区中建立一个虚方法表(Virtual Method

Table),通过使用方法表的索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址(由于 Java 虚拟机自己建立并维护的方法表,所以没有必要使用符号引用,那不是跟自己过不去嘛),如果子类没有覆盖父类的方法,那么子类的虚方法表里面的地址入口与父类是一致的;如果重写父类的方法,那么子类的方法表的地址将会替换为子类实现版本的地址。

方法表是在类加载的连接阶段(验证、准备、解析)进行初始化,准备了子类的初始化值后,虚拟机会把该类的虚方法表也进行初始化。

12、修改对象 A 的 equals 方法的签名,那么使用 HashMap 存放这个对象实例的时候,会调用哪个 equals 方法?

会调用对象的 equals 方法,如果对象的 equals 方法没有被重写,equals 方法和== 都是比较栈内局部变量表中指向堆内存地址值是否相等。

13、Java 中实现多态的机制是什么?

多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。

这样就可以不用修改源程序,就可以让引用变量绑定到各种不同的类实现上。

Java 实现多态有三个必要条件:继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。

14、如何将一个 Java 对象序列化到文件里?

ObjectOutputStream.writeObject()负责将指定的流写入,

ObjectInputStream.readObject()从指定流读取序列化数据。

 

15、说说你对 Java 反射的理解

在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能称为 java 语言的反射机制。

反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只对系统应用开放,这里就可以利用 java 的反射机制通过反射来获取所需的私有成员或是方法。

获取类的 Class 对象实例 Classclz=Class.forName("com.zhenai.api.Apple");根据 Class 对象实例获取 Constructor 对象 `Constructor appConstructor =clz.getConstructor();使用 Constructor 对象的newInstance方法获取反射类对象 `Object appleObj = appConstructor.newInstance();获取方法的 Method 对象 `Method setPriceMethod = clz.getMethod("setPrice",int.class);利用 invoke 方法调用方法 setPriceMethod.invoke(appleObj, 14);通过 getFields()可以获取 Class 类的属性,但无法获取私有属性,而 getDeclaredFields()可以获取到包括私有属性在内的所有属性。带有 Declared 修饰的方法可以反射到私有的方法,没有 Declared 修饰的只能用来反射公有的方法,其

例如 AnnotationFieldConstructor 也是如此。

16、说说你对 Java 注解的理解

注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@

 

使用时@TestAnnotation 来引用,要使注解能正常工作,还需要使用元注解,它是

可以注解到注解上的注解。元标签有@Retention @Documented @Target

@Inherited @Repeatable 五种

@Retention 说明注解的存活时间,取值有 RetentionPolicy.SOURCE 注解只在源码

阶段保留,在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候,并不会被加载到 JVM 中。RetentionPolicy.RUNTIME 可以留到程序运

行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

@Documented 注解中的元素包含到 javadoc 中去

@Target 限定注解的应用场景,ElementType.FIELD 给属性进行注解;

ElementType.LOCAL_VARIABLE 可以给局部变量进行注解;ElementType.METHOD

可以给方法进行注解;ElementType.PACKAGE 可以给一个包进行注解

ElementType.TYPE 可以给一个类型进行注解,如类、接口、枚举

@Inherited 若一个超类被@Inherited 注解过的注解进行注解,它的子类没有被任何注解应用的话,该子类就可继承超类的注解;

注解的作用:

提供信息给编译器:编译器可利用注解来探测错误和警告信息编译阶段:软件工具可以利用注解信息来生成代码、html 文档或做其它相应处理;运行阶段:程序运行时可利用注解提取代码

注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了某个注解,再通过 getAnnotation()方法获取 Annotation 对象

17、JVM 的回收算法是怎样的引用计数法

概念

引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。

首先需要声明,至少主流的 Java 虚拟机里面都没有选用引用计数算法来管理内存。

什么是引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。那为什么主流的 Java 虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题

根搜索算法

概念

根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链(即 GC Roots 到对象不可达)时,则证明此对象是不可用的。

那么问题又来了,如何选取 GCRoots 对象呢?在 Java 语言中,可以作为 GCRoots 的对象包括下面几种:

(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

(2). 方法区中的类静态属性引用的对象。

(3). 方法区中常量引用的对象。

(4). 本地方法栈中 JNI(Native 方法)引用的对象。

标记清除算法

概念

该算法有两个阶段。

标记阶段:找到所有可访问的对象,做个标记清除阶段:遍历堆,把未被标记的对象回收

应用场景

该算法一般应用于老年代,因为老年代的对象生命周期比较长。

优缺点

标记清除算法的优点和缺点

1. 优点

是可以解决循环引用的问题必要时才回收(内存不足时)

2. 缺点:

回收时,应用需要挂起,也就是 stop the world标记和清除的效率不高,尤其是要扫描的对象比较多的时候会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到),

复制算法

概念

如果 jvm 使用了 coping 算法,一开始就会将可用内存分为两块,from 域和 to 域, 每次只是使用 from 域,to 域则空闲着。当 from 域内存不够了,开始执行 GC 操作,这个时候,会把 from 域存活的对象拷贝到 to ,然后直接把 from 域进行内存清理。

应用场景

coping 算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用 coping 算法进行拷贝时效率比较高。jvm Heap 内存划分为新生代与老年代,又将新生代划分为 Eden(伊甸园) 2 Survivor Space(幸存者区) ,然后在 Eden

–>Survivor Space 以及 From Survivor Space To Survivor Space 之间实行 Copying 算法。

不过 jvm 在应用 coping 算法时,并不是把内存按照 1:1 来划分的,这样太浪费内存空间了。一般的 jvm 都是 8:1。也即是说,Eden :From :To 区域的比例是

始终有 90%的空间是可以用来创建对象的,而剩下的 10%用来存放回收后存活的对象。

Eden 区满的时候,会触发第一次 young gc,把还活着的对象拷贝到 Survivor From 区;当 Eden 区再次触发 young gc 的时候,会扫描 Eden 区和 From 区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到 To 区域,并将 Eden From 区域清空。当后续 Eden 又发生 young gc 的时候,会对 Eden To 区域进行垃圾回收,存活的对象复制到

From 区域,并将 Eden To 区域清空。

可见部分对象会在 From To 区域中复制来复制去,如此交换 15 (JVM 参数

MaxTenuringThreshold 决定,这个参数默认是 15),最终如果还是存活,就存入到老年代

注意: 万一存活对象数量比较多,那么 To 域的内存可能不够存放,这个时候会借助老年代的空间。

优缺点

优点:在存活对象不多的情况下,性能高,能解决内存碎片和 java 垃圾回收算法之-标记清除 中导致的引用更新问题。

缺点: 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,coping 的性能会变得很差。

标记压缩算法

标记清除算法和标记压缩算法非常相同,但是标记压缩算法在标记清除算法之上解决内存碎片化

概念

压缩算法简单介绍

任意顺序 : 即不考虑原先对象的排列顺序,也不考虑对象之间的引用关系,随意移动对象;

线性顺序 : 考虑对象的引用关系,例如 a 对象引用了 b 对象,则尽可能将 a b 移动到一块;

滑动顺序 : 按照对象原来在堆中的顺序滑动到堆的一端。

优缺点

优点:解决内存碎片问题,缺点压缩阶段,由于移动了可用对象,需要去更新引用。

分代算法

概述

这种算法,根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。可以用抓重点的思路来理解这个算法。

新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。

新生代

在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;可以参看我之前写的 java 垃圾回收算法之-coping 复制

老年代而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须标记-清除-压缩算法进行回收。参看 java 垃圾回收算法之-标记_清除压缩

新创建的对象被分配在新生代,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。

老年代区存放 Young Survivor 满后触发 minor GC 后仍然存活的对象,当 Eden 区满后会将存活的对象放入 Survivor 区域,如果 Survivor 区存不下这些对象,GC 收集器就会将这些对象直接存放到 Old 区中,如果 Survivor 区中的对象足够老,也直接存放到 Old 区中。如果 Old 区满了,将会触发 Full GC 回收整个堆内存。

18、Art 虚拟机与 jvm 的区别在哪里

Dalvik 和标准 Java 虚拟机(JVM)的首要差别

Dalvik 基于寄存器,而 JVM 基于栈。基于寄存器的虚拟机对于更大的程序来fb 说,在它们编译的时候,花费的时间更短。 JVM字节码中,局部变量会被放入局部变量表中,继而被压入堆栈供操作码进行运算,当然JVM也可以只使用堆栈而不显式地将局部变量存入变量表中。Dalvik字节码中,局部变量会被赋给

65536个可用的寄存器中的任何一个,Dalvik指令直接操作这些寄存器,而不是访问堆栈中的元素。

Dalvik 和 Java 字节码的区别

VM字节码由.class文件组成,每个文件一个class。JVM在运行的时候为每一个类装载字节码。相反的,Dalvik程序只包含一个.dex文件,这个文件包含了程序中所有的类。Java编译器创建了JVM字节码之后,Dalvik的dx编译器删

除.class文件,重新把它们编译成Dalvik字节码,然后把它们写进一个.dex

文件中。这个过程包括翻译、重构、解释程序的基本元素(常量池、类定义、数据段)。常量池描述了所有的常量,包括引用、方法名、数值常量等。类定义包括了访问标志、类名等基本信息。数据段中包含各种被VM执行的函数代码以及类和函数的相关信息(例如DVM所需要的寄存器数量、局部变量表、操作数堆栈大小),还有实例变量。

Dalvik 和 Java 运行环境的区别

Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

Dalvik虚拟机在android2.2之后使用JIT (Just-In-Time)技术,与传统JVM 的JIT并不完全相同,

Dalvik虚拟机有自己的 bytecode,并非使用 Java bytecode。

还有以下几点:

1、Dalvik主要是完成对象生命周期管理,堆栈管理,线程管理,安全和异常管理,以及垃圾回收等等重要功能。 2、Dalvik负责进程隔离和线程管理,每一个Android应用在底层都会对应一个独立的Dalvik虚拟机实例,其代码在虚拟机的解释下得以执行。

不同于Java虚拟机运行java字节码,Dalvik虚拟机运行的是其专有的文件格式Dex。dex文件格式可以减少整体文件尺寸,提高I/O操作的类查找速度。odex是为了在运行过程中进一步提高性能,对dex文件的进一步优化。所有的Android应用的线程都对应一个Linux线程,虚拟机因而可以更多的依赖操作系统的线程调度和管理机制。有一个特殊的虚拟机进程Zygote,他是虚拟机实例的孵化器。它在系统启动的时候就会产生,它会完成虚拟机的初始化、库的加载、预制类库和初始化的操作。如果系统需要一个新的虚拟机实例,它会迅速复制自身,以最快的速度提供给系统。对于一些只读的系统库,所有虚拟机实例都和Zygote共享一块内存区域。

19、说说你对依赖注入的理解

DI—Dependency Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定(由容器动态的将某个依赖关系注入到组件之中)。

依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

理解 DI 的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

.                谁依赖于谁:当然是应用程序依赖于 IoC 容器;

.                为什么需要依赖:应用程序需要 IoC 容器来提供对象需要的外部资源;

.                谁注入谁:很明显是 IoC 容器注入应用程序某个对象,应用程序依赖的对象;

.            注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

IoC 的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过 DI(Dependency Injection,依赖注入)来实现的。比如对象 A 需要操作数据库,以前我们总是要在 A 中自己编写代码来获得一个

Connection 对象,告诉依赖注入框架,A 中需要一个 Connection,至于这个

Connection 怎么构造,何时构造,A 不需要知道。

在系统运行时,依赖注入框架会在适当的时候制造一个 Connection,然后像打针一样,注射到 A 当中,这样就完成了对各个对象之间关系的控制。A 需要依赖 Connection 才能正常运行,而这个 Connection 是由依赖注入框架注入到 A 中的,依赖注入的名字就这么来的。

20、说一下泛型原理,并举例说明

泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。Java 泛型是在 Java1.5 以后出现的,为保持对以前版本的兼容,使用了擦除的方法实现

泛型。擦除是指在一定程度无视类型参数 T,直接从 T 所在的类开始向上 T 的父类去擦除,如调用泛型方法,传入类型参数T进入方法内部,若没在声明时做类似public T methodName(T extends Father t){}Java 就进行了向上类型的擦除,直接把参数

t 当做 Object 类来处理,而不是传进去的 T。即在有泛型的任何类和方法内部,它都无法知道自己的泛型参数,擦除和转型都是在边界上发生,即传进去的参在进入类或方法时被擦除掉,但传出来的时候又被转成了我们设置的 T。在泛型类或方法内,任何涉及到具体类型(即擦除后的类型的子类)操作都不能进行,如 new T(),或者 T.play()play 为某子类的方法而不是擦除后的类的方法)

21、String 为什么要设计成不可变的?

字符串常量池需要String不可变

因为 String 设计成不可变,当创建一个 String 对象时,若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。如果字符串变量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象。

String对象可以缓存hashCode。

字符串的不可变性保证了 hash 码的唯一性,因此可以缓存 String hashCode,这

样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较 hashCode,提高了比较性能;

安全性

String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反射机制所需的

Strign 参数等,若 String 可变,将会引起各种安全隐患。

22、Object 类的 equal 和 hashCode 方法重写,为什么?

toString()----------------------输出对象的地址 重写后输出对象的值

对象.equals(对象)---------------比较两个对象的内存地址 可以被重写,重写后比较两个对象的属性(总是会重写 hashcode()方法,因为实例 ab 通过 equals 表现为相等,但是他们存在 hashmap 中的 hashcode 依然是不一样的)、

object 类其他的比较重要的方法: getClass():得到类对象(强调类的代码信息,类的对象更强调类代码信息的值)

         hashcode() :唯一区分对象的(对象地址字符串)   clone()    线程中:notify() ,

notifyAll() , wait()

最新回复(0)