关于java:面试

38次阅读

共计 13979 个字符,预计需要花费 35 分钟才能阅读完成。

反射的概述

Java 反射的原理:java 类的执行须要经验以下过程,

编译:.java 文件编译后生成.class 字节码文件
加载:类加载器负责依据一个类的全限定名来读取此类的二进制字节流到 JVM 外部,并存储在运行时内存区的办法区,而后将其转换为一个与指标类型对应的 java.lang.Class 对象实例
连贯:细分三步
验证:格局(class 文件标准)语义(final 类是否有子类)操作
筹备:动态变量赋初值和内存空间,final 润饰的内存空间间接赋原值,此处不是用户指定的初值。
解析:符号援用转化为间接援用,调配地址
初始化: 有父类先初始化父类,而后初始化本人;将 static 润饰代码执行一遍,如果是动态变量,则用用户指定值笼罩原有初值;如果是代码块,则执行一遍操作。

Java 的反射就是利用下面第二步加载到 jvm 中的.class 文件来进行操作的。.class 文件中蕴含 java 类的所有信息,当你不晓得某个类具体信息时,能够应用反射获取 class,而后进行各种操作。

Java 反射就是在运行状态中,对于任意一个类,都可能晓得这个类的所有属性和办法;对于任意一个对象,都可能调用它的任意办法和属性;并且能扭转它的属性。总结说:反射就是把 java 类中的各种成分映射成一个个的 Java 对象,并且能够进行操作。

线程的创立形式? 你喜爱哪种? 为什么

形式一:继承 Thread 类

形式二:实现 Runnable 接口

形式三:实现 Callable 接口 –JDK5.0 新增

@PathVariable @RequestParam 和 @RequestBody 的区别

@RequestParam

1. 用于将申请参数区数据映射到性能解决办法的参数上,承受的参数是来自 requestHeader 中,即申请头,通常用于 GET 申请, 也能够用于 post,delete 等申请。
2. 用来解决 Content-Type: 为 application/x-www-form-urlencoded 编码的内容。
3.@RequestParam 有三个属性:
(1)value:申请参数名(必须配置)
(2)required:是否必须,默认 true,即申请中必须蕴含该参数,如果没有蕴含,将会抛出异样(可选配置)
(3)defaultValue:默认值,如果设置了该值,required 将主动设为 false,无论你是否配置了 required,配置了什么值,都是 false(可选配置)

@RequestBody

1.@RequestBody 映射申请到办法体上,承受的参数是来自 requestBody 中,即申请体。
2. 用来解决非 Content-Type: 为 application/x-www-form-urlencoded 编码的内容。例如:application/json、application/xml 等类型的数据。
3.@RequestBody 注解罕用于接管 json 格局的数据,并将其转换成对应的数据类型。

@PathVariable

1.@PathVariable 能够将 URL 中占位符参数绑定到控制器解决办法的入参中, 获取申请门路中的变量作为参数

  1. 带占位符的 URL 是 Spring3.0 新增的性能

为什么 HashMap 线程不平安?(jdk7 版本)

HashMap 会进行 resize 操作,在 resize 操作的时候会造成线程不平安。上面将举两个可能呈现线程不平安的中央。

1、put 的时候导致的多线程数据不统一。这个问题比拟好设想,比方有两个线程 A 和 B,首先 A 心愿插入一个 key-value 对到 HashMap 中,首先计算记录所要落到的桶的索引坐标,而后获取到该桶外面的链表头结点,此时线程 A 的工夫片用完了,而此时线程 B 被调度得以执行,和线程 A 一样执行,只不过线程 B 胜利将记录插到了桶外面,假如线程 A 插入的记录计算出来的桶索引和线程 B 要插入的记录计算出来的桶索引是一样的,那么当线程 B 胜利插入之后,线程 A 再次被调度运行时,它仍然持有过期的链表头然而它对此无所不知,以至于它认为它应该这样做,如此一来就笼罩了线程 B 插入的记录,这样线程 B 插入的记录就凭空隐没了,造成了数据不统一的行为。

上面的代码是 resize 的核心内容:

这是 jdk7 的实现形式,jdk8 不是这样的。

// 这个办法的性能是将原来的记录从新计算在新桶的地位,而后迁徙过来。void transfer(Entry[] newTable, boolean rehash) {  
        int newCapacity = newTable.length;  
        for (Entry<K,V> e : table) {while(null != e) {  
                Entry<K,V> next = e.next;           
                if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);  
                }  
                int i = indexFor(e.hash, newCapacity);   
                e.next = newTable[i];  
                newTable[i] = e;  
                e = next;  
            } 
        }  
    }  

为什么 HashMap 是不平安的?(jdk8 版本)

依据下面 JDK1.7 呈现的问题,在 JDK1.8 中曾经失去了很好的解决,如果你去浏览 1.8 的源码会发现找不到 transfer 函数,因为 JDK1.8 间接在 resize 函数中实现了数据迁徙。另外说一句,JDK1.8 在进行元素插入时应用的是尾插法。

为什么说 JDK1.8 会呈现数据笼罩的状况喃,咱们来看一下上面这段 JDK1.8 中的 put 操作代码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有 hash 碰撞则直接插入元素
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

其中第六行代码是判断是否呈现 hash 碰撞,假如两个线程 A、B 都在进行 put 操作,并且 hash 函数计算出的插入下标是雷同的,当线程 A 执行完第六行代码后因为工夫片耗尽导致被挂起,而线程 B 失去工夫片后在该下标处插入了元素,实现了失常的插入,而后线程 A 取得工夫片,因为之前曾经进行了 hash 碰撞的判断,所有此时不会再进行判断,而是间接进行插入,这就导致了线程 B 插入的数据被线程 A 笼罩了,从而线程不平安。

总结: HashMap 的线程不平安次要体现在上面两个方面:

1. 在 JDK1.7 中,当并发执行扩容操作时会造成环形链和数据失落的状况。

2. 在 JDK1.8 中,在并发执行 put 操作时会产生数据笼罩的状况。

单点登录业务实现原理

实现步骤:
1. 用户输出用户名和明码之后点击登录按钮开始进行登录操作.
2.JT-WEB 向 JT-SSO 发送申请, 实现数据校验
3. 当 JT-SSO 获取数据信息之后, 实现用户的校验, 如果校验通过则将用户信息转化为 json. 并且动静生成 UUID. 将数据保留到 redis 中. 并且返回值 uuid.
如果校验不存在时, 间接返回 ” 不存在 ” 即可.
4.JT-SSO 将数据返回给 JT-WEB 服务器.
5. 如果登录胜利, 则将用户 UUID 保留到客户端的 cookie 中.

索引是什么?有什么作用以及优缺点?以及常见的几种索引

1、索引是对数据库表中一或多个列的值进行排序的构造,是帮忙 MySQL 高效获取数据的数据结构

2、索引就是放慢检索表中数据的办法。数据库的索引相似于书籍的索引。在书籍中,索引容许用户不用翻阅残缺个书就能迅速地找到所须要的信息。在数据库中,索引也容许数据库程序迅速地找到表中的数据,而不用扫描整个数据库。

MySQL 数据库几个根本的索引类型:一般索引、惟一索引、主键索引、全文索引

1、索引放慢数据库的检索速度
2、索引升高了插入、删除、批改等保护工作的速度
3、惟一索引能够确保每一行数据的唯一性
4、通过应用索引,能够在查问的过程中应用优化暗藏器,进步零碎的性能、索引须要占物理和数据空间

主键索引 : 数据列不容许反复,不容许为 NULL,一个表只能有一个主键。
惟一索引 : 数据列不容许反复,容许为 NULL 值,一个表容许多个列创立惟一索引。
一般索引 : 根本的索引类型,没有唯一性的限度,容许为 NULL 值。
全文索引:是目前搜索引擎应用的一种关键技术。

为什么 redis 这么快

  1. Redis 是齐全基于内存的数据库
  2. 解决网络申请应用的是单线程,防止了不必要的上下文切换和锁的竞争保护。
  3. 应用了 I / O 多路复用模型。

齐全基于内存

为什么要用齐全呢。因为像 mysql 这样的传统关系型数据库是存储在硬盘的,那么硬盘的性能和瓶颈将会影响到数据库。

单线程

须要留神的是,这里的单线程指的是,Redis 解决网络申请的时候只有一个线程,而不是整个 Redis 服务是单线程的。

I/ O 多路复用模型

  • 传统多过程并发模型: 每监听到一个 Socket 连贯就会调配一个线程解决
  • 多路复用模型: 单个线程,通过记录跟踪每一个 Socket 连贯的 I / O 的状态来同时治理多个 I / O 流。
    这里的 I / O 指的是 网络 I /O,多路指的是 多个网络连接 ,复用指的是 复用一个线程

联合 Redis:

  1. 在 Redis 中的 I/ O 多路复用程序 会监听多个客户端连贯的 Socket
  2. 每当有客户端通过 Socket 流向 Redis 发送申请进行操作时,I/ O 多路复用程序会将其放入一个 队列 中。
  3. 同时 I / O 多路复用程序会 同步 有序、每次传送一个工作给处理器解决。
  4. I/ O 多路复用程序会在上一个申请处理完毕后再持续分派下一个工作。(同步)

怎么判断对象曾经死亡?

援用计数法

给对象中增加一个计数器, 每当一个中央援用他, 则计数器加 1, 当援用生效, 计数器减 1, 计数器为 0 的对象就是不可能在被应用的。
这个办法实现简略, 效率高, 然而目前支流并没有抉择这个算法来治理内存, 次要起因是无奈解决对象之间互相循环援用的问题。

可达性算法

通过一系列 ”GC Roots” 对象作为终点,开始向下搜寻节点所走过的路称为援用链,当一个对象到 ”GC Roots” 没有任何援用链相连,则证实该对象是不可达的。

可作为 GC Roots 的对象包含上面几种

  • 虚拟机栈 (栈帧中的本地变量表) 中援用的对象
  • 本地办法栈 (Native 办法) 中援用的对象
  • 办法区中类动态属性援用的对象
  • 办法区中常量援用的对象

援用

强援用
咱们经常 new 进去的对象都是强援用,只有强援用存在垃圾回收器永远不会回收,哪怕内存不足。

软援用
应用 SoftReference 润饰的对象被称为软援用,如果内存足够则垃圾回收器不会回收,如果内存不足就会回收这写对象的援用。

弱援用
应用 WeakReference 润饰的对象被称为弱援用,只有产生垃圾回收,不论以后内存是否足够,都会回收它的内存。不过因为垃圾回收器是一个优先级很低的线程, 因而不肯定会很快发现那些只具备弱援用的对象。

虚援用
应用 PhantomReference 润饰的对象被称为虚援用,虚援用并不会决定对象的生命周期。如果一个对象仅持有虚援用,那么就和没有任何援用一样,在任何时候都会被垃圾回收。惟一的作用就是用队列承受对象行将死亡的告诉。

如何判断一个类是一个无用的类

  • 该类的所有实例都曾经被回收,堆中不存在该类的任何实列
  • 加载该类的 ClassLoader 曾经被回收
  • 该类对应的 java.lang.Class 对象没在任何中央被援用,无奈在任何中央通过反色和拜访该类的办法

虚拟机能够对满足上诉 3 个条件的无用类进行回收,并不是和对象一样不应用了就会必然被回收

类加载器

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其余类加载器均由 Java 实现且全副继承自 java.lang.ClassLoader:

BootstrapClassLoader(启动类加载器)
最顶层的加载类,由 C ++ 实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者或被 -Xbootclasspath参数指定的门路中的所有类。

ExtensionClassLoader(扩大类加载器)
次要负责加载目录 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 零碎变量所指定的门路下的 jar 包。

AppClassLoader(应用程序类加载器)
面向咱们用户的加载器,负责加载以后利用 classpath 下的所有 jar 包和类。

双亲委派模型

零碎中的 ClassLoder 在协同工作的时候会默认应用 双亲委派机制。在类加载的时候,零碎会先判断以后类是否被加载过。曾经被加载的类会间接返回,否则才会尝试加载。加载的时候,首先会把申请委派给该父类加载器的 loadClass() 解决,因而所有的申请最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无奈解决时,才由本人来解决。当父类加载器为 null 时,会应用启动类加载器 BootstrapClassLoader 作为父类加载器。

类加载器之间的 ” 父子 ” 关系不是通过继承来体现的,是由 ” 优先级 ” 来决定的。

如果咱们不想突破双亲委派模型,就重写 ClassLoader 类中的 findClass() 办法即可,无奈被父类加载器加载的类最终会通过这个办法被加载。然而如果想突破双亲委派机制模型则须要重写 loadClass() 办法。

应用线程池的益处

  • 升高资源耗费。通过反复利用曾经创立的线程升高创立线程和销毁线程带来的耗费。
  • 进步效应速度。当工作达到时,工作能够不须要期待线程创立就能立刻执行。
  • 进步线程的可管理性。如果无限度的创立线程,不仅会耗费系统资源,还会升高零碎的稳定性,应用线程能够进行对立的调配,调优和监控。

Executor 框架

Executor 框架是在 JDK1.5 之引进的,通过 Executor 来启动线程比应用 Thread 的 start 办法更好,除了更易治理和效率更好之外,最要害的一点是:有助于避免 this 逃逸。

this 逃逸是指在构造函数返回之前其余线程就持有该对象的援用. 调用尚未结构齐全的对象的办法可能引发令人纳闷的谬误。

Executor 框架不仅包含了线程池的治理,还提供了线程工厂,队列以及回绝策略。

ThreadPoolExecutor

ThreadPoolExcutor 是 Executor 框架最外围的类

ThreadPoolExecutor 最外围的三个参数

  • corePoolSize:外围线程数定义了最小能够同时运行的线程数量。
  • maximumPoolSize:当队列中的工作达到队列容量的时候,以后能够同时运行的线程数量变为最大线程数。
  • workQueue:当新的工作来的时候会判断以后运行的线程数量是否达到外围线程数量,如果达到,则会被寄存到队列当中。

ThreadPoolExecutor 其余常见参数:

  1. keepAliveTime: 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的工作提交,外围线程外的线程不会立刻销毁,而是会期待,直到期待的工夫超过了 keepAliveTime 才会被回收销毁;
  2. unit: keepAliveTime 参数的工夫单位。
  3. threadFactory: executor 创立新线程的时候会用到。
  4. handler: 饱和策略。

ThreadPoolExecutor 饱和策略定义:

如果以后同时运行的线程数量达到最大线程数量并且队列也曾经被放满了工作时,ThreadPoolTaskExecutor 定义一些策略:

ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException 来回绝新工作的解决。
ThreadPoolExecutor.CallerRunsPolicy:调用执行本人的线程运行工作,也就是间接在调用 execute 办法的线程中运行 (run) 被回绝的工作,如果执行程序已敞开,则会抛弃该工作。因而这种策略会升高对于新工作提交速度,影响程序的整体性能。如果您的应用程序能够接受此提早并且你要求任何一个工作申请都要被执行的话,你能够抉择这个策略。
ThreadPoolExecutor.DiscardPolicy:不解决新工作,间接抛弃掉。
ThreadPoolExecutor.DiscardOldestPolicy:此策略将抛弃最早的未解决的工作申请。

Executors 返回线程池对象的弊病如下:

  • FixedThreadPool 和 SingleThreadExecutor:容许申请的队列长度为 Integer.MAX_VALUE, 可能沉积大量的申请,从而导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool:容许创立的线程数量为 Integer.MAX_VALUE,可能会创立大量线程,从而导致 OOM。

创立线程池的几种形式

形式一:通过 ThreadPoolExecutor 构造函数实现(举荐)

形式二:通过 Executor 的框架的工具类 Executors 来创立三种类型的 ThreadPoolExecutor:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

FixedThreadPool

FixedThreadPool 被称为可重用固定线程数量的线程池。

  1. 如果以后运行的线程数小于 corePoolSize,如果再来新工作的话,就创立新的线程来执行工作;
  2. 以后运行的线程数等于 corePoolSize 后,如果再来新工作的话,会将工作退出 LinkedBlockingQueue;
  3. 线程池中的线程执行完 手头的工作后,会在循环中重复从 LinkedBlockingQueue 中获取工作来执行;

为什么不举荐应用 FixedThreadPool?

FixedThreadPool 应用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响:

  1. 当线程池中的线程数达到 corePoolSize 后,新工作将在无界队列中期待,因而线程池中的线程数不会超过 corePoolSize;
  2. 因为应用无界队列时 maximumPoolSize 将是一个有效参数,因为不可能存在工作队列满的状况。所以,通过创立 FixedThreadPool 的源码能够看出创立的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
  3. 因为 1 和 2,应用无界队列时 keepAliveTime 将是一个有效参数;
  4. 运行中的 FixedThreadPool(未执行 shutdown() 或 shutdownNow())不会回绝工作,在工作比拟多的时候会导致 OOM(内存溢出)。

SingleThreadExecutor

SingleThreadExecutor 是一个只有一个线程的线程池。

  1. 如果以后运行的线程数少于 corePoolSize,则创立一个新的线程执行工作;
  2. 以后线程池中有一个运行的线程后,将工作退出 LinkedBlockingQueue
  3. 线程执行完以后的工作后,会在循环中重复从 LinkedBlockingQueue 中获取工作来执行;

为什么不举荐应用 SingleThreadExecutor?

SingleThreadExecutor 应用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 应用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 雷同。说简略点就是可能会导致 OOM,

CachedThreadPool

CachedThreadPool 是一个会依据须要创立新线程数量的线程池。

  1. 首先执行 SynchronousQueue.offer(Runnable task) 提交工作到工作队列。如果以后 maximumPool 中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与闲暇线程执行的 poll 操作配对胜利,主线程把工作交给闲暇线程执行,execute() 办法执行实现,否则执行上面的步骤 2;
  2. 当初始 maximumPool 为空,或者 maximumPool 中没有闲暇线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种状况下,步骤 1 将失败,此时 CachedThreadPool 会创立新线程执行工作,execute 办法执行实现;

为什么不举荐应用 CachedThreadPool?

CachedThreadPool 容许创立的线程数量为 Integer.MAX_VALUE,可能会创立大量线程,从而导致 OOM。

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 次要用来在给定的提早后运行工作,或者定期执行工作。

运行时数据区域

线程公有的:

  • 程序计数器
  • 虚拟机栈
  • 本地办法栈

线程共享的:

  • 办法区
  • 间接内存

程序计数器

程序计数器是一块较小的内存空间,能够看作是以后线程所执行的字节码的行号指示器。字节码解释器工作时通过扭转这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异样解决、线程复原等性能都须要依赖这个计数器来实现。

另外,为了线程切换后能复原到正确的执行地位,每条线程都须要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程公有”的内存。

作用:

  1. 字节码解释器通过改变程序计数器来顺次读取指令,从而实现代码的流程管制,如:程序执行、抉择、循环、异样解决。
  2. 在多线程的状况下,程序计数器用于记录以后线程执行的地位,从而当线程被切换回来的时候可能晓得该线程上次运行到哪儿了。

留神:程序计数器是惟一一个不会呈现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创立而创立,随着线程的完结而死亡。

Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈也是线程公有的,它的生命周期和线程雷同,形容的是 Java 办法执行的内存模型,每次办法调用的数据都是通过栈传递的。

Java 内存能够毛糙的辨别为堆内存(Heap)和栈内存 (Stack), 其中栈就是当初说的虚拟机栈,或者说是虚拟机栈中局部变量表局部。(实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都领有:局部变量表、操作数栈、动静链接、办法进口信息。)

局部变量表次要寄存了编译期可知的各种数据类型 (boolean、byte、char、short、int、float、long、double)、 对象援用(reference 类型,它不同于对象自身,可能是一个指向对象起始地址的援用指针,也可能是指向一个代表对象的句柄或其余与此对象相干的地位)。

Java 虚拟机栈会呈现两种谬误:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不容许动静扩大,那么当线程申请栈的深度超过以后 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 谬误。
  • OutOfMemoryError: 若 Java 虚拟机堆中没有闲暇内存,并且垃圾回收器也无奈提供更多内存的话。就会抛出 OutOfMemoryError 谬误。

Java 虚拟机栈也是线程公有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创立而创立,随着线程的死亡而死亡。

Java 栈中保留的次要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用完结后,都会有一个栈帧被弹出。

Java 办法有两种返回形式:

  1. return 语句。
  2. 抛出异样。

不论哪种返回形式都会导致栈帧被弹出。

本地办法栈

和虚拟机栈所施展的作用十分类似,区别是:虚拟机栈为虚拟机执行 Java 办法(也就是字节码)服务,而本地办法栈则为虚拟机应用到的 Native 办法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地办法被执行的时候,在本地办法栈也会创立一个栈帧,用于寄存该本地办法的局部变量表、操作数栈、动静链接、进口信息。

办法执行结束后相应的栈帧也会出栈并开释内存空间,也会呈现 StackOverFlowError 和 OutOfMemoryError 两种谬误。

Java 虚拟机所治理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创立。此内存区域的惟一目标就是寄存对象实例,简直所有的对象实例以及数组都在这里分配内存。

Java 世界中“简直”所有的对象都在堆中调配,然而,随着 JIT 编译期的倒退与逃逸剖析技术逐步成熟,栈上调配、标量替换优化技术将会导致一些奥妙的变动,所有的对象都调配到堆上也慢慢变得不那么“相对”了。从 jdk 1.7 开始曾经默认开启逃逸剖析,如果某些办法中的对象援用没有被返回或者未被里面应用(也就是未逃逸进来),那么对象能够间接在栈上分配内存。

Java 堆是垃圾收集器治理的次要区域,因而也被称作 GC 堆(Garbage Collected Heap). 从垃圾回收的角度,因为当初收集器根本都采纳分代垃圾收集算法,所以 Java 堆还能够细分为:新生代和老年代:再粗疏一点有:Eden 空间、From Survivor、To Survivor 空间等。 进一步划分的目标是更好地回收内存,或者更快地分配内存。

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常被分为上面三局部:

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永生代(Permanent Generation)

JDK 8 版本之后办法区(HotSpot 的永恒代)被彻底移除了(JDK1.7 就曾经开始了),取而代之是元空间,元空间应用的是间接内存。

办法区

办法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、动态变量、即时编译器编译后的代码等数据。尽管 Java 虚拟机标准把办法区形容为堆的一个逻辑局部,然而它却有一个别名叫做 Non-Heap(非堆),目标应该是与 Java 堆辨别开来。

办法区和永恒代的关系

《Java 虚拟机标准》只是规定了有办法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上办法区的实现必定是不同的了。办法区和永恒代的关系很像 Java 中接口和类的关系,类实现了接口,而永恒代就是 HotSpot 虚拟机对虚拟机标准中办法区的一种实现形式。 也就是说,永恒代是 HotSpot 的概念,办法区是 Java 虚拟机标准中的定义,是一种标准,而永恒代是一种实现,一个是规范一个是实现,其余的虚拟机实现并没有永恒代这一说法。

为什么要将永恒代 (PermGen) 替换为元空间 (MetaSpace) 呢?
  1. 整个永恒代有一个 JVM 自身设置固定大小下限,无奈进行调整,而元空间应用的是间接内存,受本机可用内存的限度,尽管元空间仍旧可能溢出,然而比原来呈现的几率会更小。
  2. 元空间外面寄存的是类的元数据,这样加载多少类的元数据就不禁 MaxPermSize 管制了, 而由零碎的理论可用空间来管制,这样能加载的类就更多了。
  3. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 素来没有一个叫永恒代的货色, 合并之后就没有必要额定的设置这么一个永恒代的中央了。

乐观锁的两种实现形式

乐观锁个别会应用版本号机制和 cas 算法实现

版本号机制

个别是在数据表中加上一个数据版本号 version 字段,示意数据被批改的次数,当数据被批改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若方才读取到的 version 值为以后数据库中的 version 值相等时才更新,否则重试更新操作,直到更新胜利。

cas 算法

compare and swap(比拟与替换),是一种有名的 无锁算法 。无锁编程,即不应用锁的状况下实现多线程之间的变量同步,也就是在没有线程被阻塞的状况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法 波及到三个操作数

  • 须要读写的内存值 V
  • 进行比拟的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A 时,CAS 通过原子形式用新值 B 来更新 V 的值,否则不会执行任何操作(比拟和替换是一个原子操作)。个别状况下是一个 自旋操作 ,即 一直的重试

乐观锁的毛病

1.ABA 问题
如果一个变量 V 首次读取的时候是 A 值,并且在筹备赋值的时候查看到它依然是 A 值,那咱们就能阐明它的值没有被其余线程批改过了吗?很显著是不能的,因为在这段时间它的值可能被改为其余值,而后又改回 A,那 CAS 操作就会误认为它素来没有被批改过。这个问题被称为 CAS 操作的 “ABA” 问题。

JDK 1.5 当前的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 办法就是首先查看以后援用是否等于预期援用,并且以后标记是否等于预期标记,如果全副相等,则以原子形式将该援用和该标记的值设置为给定的更新值。

2. 循环工夫长开销大

自旋 CAS(也就是不胜利就始终循环执行直到胜利)如果长时间不胜利,会给 CPU 带来十分大的执行开销。 如果 JVM 能反对处理器提供的 pause 指令那么效率会有肯定的晋升,pause 指令有两个作用,第一它能够提早流水线执行指令(de-pipeline), 使 CPU 不会耗费过多的执行资源,提早的工夫取决于具体实现的版本,在一些处理器上延迟时间是零。第二它能够防止在退出循环的时候因内存程序抵触(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而进步 CPU 的执行效率。

3. 只能保障一个共享变量的原子操作
CAS 只对单个共享变量无效,当操作波及跨多个共享变量时 CAS 有效。然而从 JDK 1.5 开始,提供了 AtomicReference 类来保障援用对象之间的原子性,你能够把多个变量放在一个对象里来进行 CAS 操作. 所以咱们能够应用锁或者利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作。

CAS 与 synchronized 的应用状况

简略来说 CAS 实用于写比拟少的状况下,synchronized 实用于写比拟多的状况下

MyISAM 和 InnoDB 的区别

  • 是否反对行级锁:MyISAM 只有表级锁,而 InnoDB 反对行级锁和表级锁,默认为行级锁。
  • 是否反对事物和解体后的平安复原:MyISAM 强调的是性能,,每次查问具备原子性,其执行速度比 InnoDB 类型更快,然而不提供事务反对。然而 InnoDB 提供事物反对,内部键等高级数据库问题。
  • 是否反对外键:MyISAM 不反对,而 InnoDB 反对
  • 是否反对 MVCC:仅 InnoDB 反对。应答高并发事务, MVCC 比单纯的加锁更高效;MVCC 只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作;MVCC 能够应用 乐观 (optimistic) 锁 和 乐观 (pessimistic) 锁来实现; 各数据库中 MVCC 实现并不对立。

并发事务带来的问题

  • 脏读(Dirty read): 当一个事务正在拜访数据并且对数据进行了批改,而这种批改还没有提交到数据库中,这时另外一个事务也拜访了这个数据,而后应用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,根据“脏数据”所做的操作可能是不正确的。
  • 失落批改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也拜访了该数据,那么在第一个事务中批改了这个数据后,第二个事务也批改了这个数据。这样第一个事务内的批改后果就被失落,因而称为失落批改。例如:事务 1 读取某表中的数据 A =20,事务 2 也读取 A =20,事务 1 批改 A =A-1,事务 2 也批改 A =A-1,最终后果 A =19,事务 1 的批改被失落。
  • 不可反复读(Unrepeatableread): 指在一个事务内屡次读同一数据。在这个事务还没有完结时,另一个事务也拜访该数据。那么,在第一个事务中的两次读数据之间,因为第二个事务的批改导致第一个事务两次读取的数据可能不太一样。这就产生了在一个事务内两次读到的数据是不一样的状况,因而称为不可反复读。
  • 幻读(Phantom read): 幻读与不可反复读相似。它产生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查问中,第一个事务(T1)就会发现多了一些本来不存在的记录,就如同产生了幻觉一样,所以称为幻读。

不可反复读和幻读区别:

不可反复读的重点是批改比方屡次读取一条记录发现其中某些列的值被批改,幻读的重点在于新增或者删除比方屡次读取一条记录发现记录增多或缩小了。

正文完
 0