Java核心技术36讲
2019-01-17 14:16:42 0 举报仅支持查看
AI智能生成
Java核心36讲笔记
Java
模板推荐
作者其他创作
大纲/内容
Exception和Error有什么区别
Throwable<br>
Exception<br>
可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。
不检查异常就是所谓的运行时异常,不会在编译期强制要求。
Error
正常情况下,不大可能出现的情况,绝大部分的Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。
强引用、软引用、弱引用、幻象引用有什么区别
强引用(Strong Reference)
常见的普通对象引用,只要还有强引用指向一个对象,就表明对象还活着,垃圾收集器不会碰这种对象。
软引用SoftReference<br>
当JVM认为内存不足时,就会试图回收软引用指向的对象。确保在OOM之前清理软引用指向的对象。
弱引用WeakReference
被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用PhantomReference
最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
谈谈 Java 反射机制,动态代理是基于什么原理
反射机制
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
动态代理
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制。比如用来包装 RPC 调用、面向切面的编程(AOP)。实现动态代理的方式很多,JDK自身提供的动态代理就是利用反射机制实现的。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似 ASM、cglib(基于 ASM)、Javassist 等。
对比Vector、ArrayList、LinkedList有何区别
底层实现方式
ArrayList内部用数组来实现;LinkedList内部采用双向链表实现;Vector内部用数组实现。
读写机制
ArrayList在执行插入元素是超过当前数组预定义的最大值时,数组需要扩容,扩容过程需要调用底层System.arraycopy()方法进行大量的数组复制操作;在删除元素时并不会减少数组的容量(如果需要缩小数组容量,可以调用trimToSize()方法);在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。<br>LinkedList在插入元素时,须创建一个新的Entry对象,并更新相应元素的前后元素的引用;在查找元素时,需遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除即可。<br>Vector与ArrayList仅在插入元素时容量扩充机制不一致。Vector 在扩容时会提高 1 倍,而 ArrayList则是增加 50%。<br>
读写效率
ArrayList对元素的插入和删除都会引起数组的内存分配空间动态发生变化。因此,对其进行插入和删除速度较慢,但检索速度很快。LinkedList由于基于链表方式存放数据,插入和删除元素的速度较快,但是检索速度较慢。
线程安全性
ArrayList、LinkedList为非线程安全;Vector是基于synchronized实现的线程安全的ArrayList。<br>
如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?
Java 提供了不同层面的线程安全支持,除了Hashtable等同步容器,还提供了同步包装器,如 Collections.synchronizedMap),粗粒度锁,高并发情况下,性能比较低下。利用并发包提供的线程安全容器类,如ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ArrayBlockingQueue、SynchronousQueue等<br>
jdk7中ConcurrentHashMap基于lock实现锁分段技术。首先将Map存放的数据进行分段(Segment),里面则是 HashEntry 的数组, 内部使用 volatile 的 value字段保证可见性,和 HashMap 类似,哈希相同的条目也是以链表形式存放。然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。
JDK1.8的ConcurrentHashMap实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap
Java有几种文件拷贝方式?哪一种最高效?
利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个 FileOutputStream,完成写入工作
利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现。总体上来说,NIO transferTo/From 的方式更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。
谈谈你知道的设计模式?
创建型模式
是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。
结构型模式
是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。
行为型模式
是从类或对象之间交互、职责划分等角度总结的模式,比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。
Spring中使用的设计模式
BeanFactory和ApplicationContext使用工厂模式
在 Bean 的创建中,Spring 也为不同 scope 定义的对象,提供了单例和原型等模式实现。
AOP 领域则是使用了代理模式、装饰器模式、适配器模式等
各种事件监听器,是观察者模式的典型应用。
JdbcTemplate 等则是应用了模板模式。
synchronized底层如何实现?什么是锁的升级、降级?
synchronized代码块是由一对monitorentor/monitorexit指令实现的,Monitor 对象是同步的基本实现单元。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。synchronized使用的锁对象是存储在Java对象头Mark Word中
为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状<br>态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。当没有竞争的时候默认使用偏向锁,JVM使用CAS在对象头Mark Word部分设置线程ID。表示这个对象偏向当前线程,如果另外的线程试图锁定已经被偏向过的对象,JVM就会撤销偏向锁,并切换到轻量级锁。轻量级锁依赖CAS修改Mark Word试图获取锁,如果重试成功就使用轻量级锁,否则进一步升级为重量级锁。
什么情况下Java程序会产生死锁?如何定位、修复?
在多线程场景中,两个或者多个线程之间,互相持有对方需要的锁。而永久处于阻塞状态。定位死锁的工具有:jstack、JConsole等工具。定位步骤:1、使用jps、top等命令确定进程ID 2、使用jstack获取线程的栈信息 3、找到处于BLOCKED 状态的线程以及确定代码。
并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?
阻塞队列,由于 LinkedBlockingQueue 实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是 Integer.MAX_VALUE。
非阻塞队列,ConcurrentLinkedQueue 是 Queue 的一个安全实现。Queue 中元素按 FIFO 原则进行排序。采用 CAS操作,来保证元素的一致性。
AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用CAS操作?
AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作。其原子性操作是基于CAS技术实现的。CAS是一种无锁算法(乐观锁),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则要么进行重试,要么返回成功或者失败的结果。
有哪些方法可以在运行时动态生成一个Java类?
直接用 ProcessBuilder 之类启动 javac进程,并指定生成的文件作为输入,进行编译。最后,再利用类加载器在运行时加载
使用 Java Compiler API,这是 JDK 提供的标准 API,里面提供了与 javac 对等的编译器功能
通常我们可以利用 Java 字节码操纵工具和类库来实现,比如ASM、Javassist、cglib等。<br>
如何监控和诊断JVM堆内和堆外内存使用?
可以使用综合性的图形化工具;如 JConsole、VisualVM,命令行工具可以使用jstat 和 jmap 等工具;也可以使用 jmap 等提供的命令,生成堆转储Heap Dump)文件,然后利用 jhat 或 Eclipse MAT 等堆转储分析工具进行详细分析;通过GC日志也包含着丰富的信息。
GC 年代方式划分,堆内存分为:
新生代
新生代是大部分对象创建和销毁的区域,在通常的 Java 应用中,绝大部分对象生命周期都是很短暂的,其内部又分为 Eden 区域,作为对象初始分配的区域;两个 Survivor,有时候也叫 from、to 区域,被用来放置从 Minor GC 中保留下来的对象。JVM 会随意选取一个 Survivor 区域作为“to”, 然后会在 GC 过程中进行区域间拷贝, 也就是将 Eden 中存活下来的对象和 from 区域的对象,拷贝到这个“to”区域。这种设计主要是为了防止内存的碎片化,并进一步清理无用对象。
老年代
放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。 如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。
永久代
这部分就是早期 Hotspot JVM 的方法区实现方式了,储存 Java 类元数据、常量池、Intern 字符串缓存,在 JDK 8 之后就不存在永久代这块儿了。
JVM堆内存大参数控制:-Xmx 最大堆内存,最小堆内存-Xms,老年代和新生代的比例-XX:NewRatio 默认是2, -XX:NewSize 新生代大小,Eden 和 Survivor 的大小是按照比例设置-XX:SurvivorRatio 默认是8
谈谈你的GC调优思路?
通常关注三个方面,内存占用(footprint)、延时(latency)和吞吐量(throughput) ,大多数情况下调优会侧重于其中一个或者两个方面的目标。
理解应用需求和问题,确定调优目标。 如果出现性能抖动,评估可接受的响应时间和业务量;通过 jstat 等工具、开启GC日志查看 GC 等相关状态,确定真的有 GC 调优的必要;选择的 GC 类型是否符合我们的应用特征,如果是,具体问题表现在哪里,是Minor GC过长还是其他异常停顿情况;如果不是考虑切换到什么类型,比如CMS和G1;通过分析确定具体调整的参数或者软硬件配置;验证是否达到目标。如果没达到重复完成分析、调整、验证这个过程。
Java程序运行在Docker等容器环境有哪些新问题?
如果未配置合适的 JVM 堆和元数据区、直接内存等参数,Java 就有可能试图使用超过容器限制的内存,最终被容器 OOM kill,或者自身发生 OOM。
错误判断了可获取的 CPU 资源,例如,Docker 限制了 CPU 的核数,JVM 就可能设置不合适的 GC 并行线程数等。
后台服务出现明显“变慢”,谈谈你的诊断思路?
怎么找到最耗费 CPU 的 Java 线程,简要介绍步骤
利用 top 命令获取相应 pid,“-H”代表 thread 模式,你可以配合 grep 命令更精准定位。
然后转换成为 16 进制。printf "%x" your_pid
最后利用 jstack 获取的线程栈,对比相应的 ID 即可。
当然,还有更加通用的诊断方向,利用 vmstat 之类,查看上下文切换的数量
利用 free 之类查看内存使用。
利用 iostat 等命令有助于判断磁盘的健康状况。
利用 JMC、JConsole 等工具进行运行时监控。
利用各种工具,在运行时进行堆转储分析,或者获取各种角度的统计数据(如jstat -gcutil 分析 GC、内存分带等)。
GC 日志等手段,诊断 Full GC、Minor GC,或者引用堆积等。
谈谈Spring Bean的生命周期和作用域?
生命周期
实例化 Bean 对象。
设置 Bean 属性。
如果我们通过各种 Aware 接口声明了依赖关系,则会注入 Bean 对容器基础设施层面的依赖。具体包括 BeanNameAware、BeanFactoryAware 和 ApplicationContextAware,分别会注入 Bean ID、Bean Factory 或者 ApplicationContext。
调用 BeanPostProcessor 的前置初始化方法 postProcessBeforeInitialization。
如果实现了 InitializingBean 接口,则会调用 afterPropertiesSet 方法。
调用 Bean 自身定义的 init 方法。
调用 BeanPostProcessor 的后置初始化方法 postProcessAfterInitialization。
Spring Bean 的销毁过程会依次调用 DisposableBean 的 destroy 方法和 Bean 自身定制的 destroy 方法。
作用域
Singleton,这是 Spring 的默认作用域,也就是为每个 IOC 容器创建唯一的一个 Bean 实例。
Prototype,针对每个 getBean 请求,容器都会单独创建一个 Bean 实例。
Request,为每个 HTTP 请求创建单独的 Bean 实例。
Session,很显然 Bean 实例的作用域是 Session 范围。
GlobalSession,用于 Portlet 容器,因为每个 Portlet 有单独的 Session,GlobalSession 提供一个全局性的 HTTP Session。
谈谈常用的分布式ID的设计方案?Snowflake是否受冬令时切换影响?
谈谈 final、finally、 finalize 有什么不同<br>
final
final修饰的class代表不可以继承扩展、final修饰的方法代表不可以被重写、final修饰的变量代表不可以被改变。
finally
finally是Java保证重点代码一定要被执行的一种机制
finalize
finalize是Object的一个方法。保证对象在被垃圾收集前完成特定的资源回收
String、StringBuffer、StringBuilder有什么区别
声明成为 final class,所有属性都是final的。由于不可变性,所有的拼接、裁剪等操作,都会产生一个新的String对象。
为了解决String拼接、裁剪等操作产生的太多的中间对象而提供的一个类。append 或者 add 方法,把字符串添加到已有序列的末尾指定的位置。并且保证了线程安全。也带来了额外的性能开销。<br>
和StringBuffer没有本质上的区别。只是去掉了线程安全。减小开销。
int和Integer有什么区别
Integer是int的包装类,引入了自动装箱和自动拆箱功能,在调用valueOf的时候会利用一个缓存机制来提升性能,这个值默认缓存是 -128 到 127 之间。
对比Hashtable、HashMap、TreeMap有什么不同
元素特性
HashTable中的key、value都不能为null;HashMap中的key、value可以为null,TreeMap中当未实现 Comparator 接口时,key 不可以为null;当实现 Comparator 接口时,若未对null情况进行判断,则key不可以为null,反之亦然。
顺序特性
HashTable、HashMap具有无序特性。TreeMap是利用红黑树来实现的(树中的每个节点的值,都会大于或等于它的左子树种的所有节点的值,并且小于或等于它的右子树中的所有节点的值),实现了SortMap接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择TreeMap来进行,默认为升序排序方式(深度优先搜索),可自定义实现Comparator接口实现排序方式。
初始化与增长方式
初始化时:HashTable在不指定容量的情况下的默认容量为11,且不要求底层数组的容量一定要为2的整数次幂;HashMap默认容量为16,且要求容量一定为2的整数次幂。扩容时:Hashtable将容量变为原来的2倍加1;HashMap扩容将容量变为原来的2倍。<br>
线程安全性
HashTable其方法函数都是同步的因此保证了线程安全性。也正因为如此,在多线程运行环境下效率不是很好。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会进入阻塞状态。在新版本中已被废弃,不推荐使用。<br>HashMap不支持线程的同步,多线程环境下会导致CPU打满,size不一致等问题。如果需要同步(1)可以用 Collections的synchronizedMap方法;(2)使用ConcurrentHashMap类,相较于HashTable锁住的是对象整体, ConcurrentHashMap基于lock实现锁分段技术
一段话HashMap
HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。
Java提供了哪些IO方式? NIO如何实现多路复用?
BIO
交互方式是同步、阻塞的方式,线程在读、写动作完成之前会一直阻塞在哪里。优点:代码简单、直观;缺点:io效率和扩展性存在局限性
NIO
提供了Channel,Selector、Buffer等新的抽象,可以构建多路复用、同步非阻塞IO程序,提供了更接近操作系统底层的高性能数据操作方式。
Selector,是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。
AIO
异步非阻塞IO方式,异步IO操作基于事件和回调机制,应用操作后直接返回,不会阻塞在哪里,当后台处理完成,操作系统会通知相应的线程进行后续操作。
谈谈接口和抽象类有什么区别?
接口
接口是对行为的抽象,他是抽象方法的集合,不能实例化,不能包含非常量成员,任何字段都隐含着public static final的意义;没有非静态方法实现;支持多重继承,
抽象类
抽象类是不能被实例化的类,其目的主要是代码重用。可以有多个抽象方法,也可以没有,除了不能实例化其他和普通类没太大区别。
synchronized和ReentrantLock有什么区别呢?
synchronized
可重入锁。Java内置的同步机制,提供了互斥的语义和可见性,当一个线程获取当前锁时,其他试图获取的线程职能等待或者阻塞在那里,持有或者释放锁都是隐式进行的。
ReentrantLock
可重入锁,显示的获取或者释放锁,尝试非阻塞的获取锁,可以指定是公平锁还是非公平锁,提供Condition(条件)唤醒线程,可中断的获取锁和超时获取锁。
一个线程两次调用start()方法会出现什么情况?
Java的线程是不允许启动两次的,第二次调用必然会报IllegalThreadStateException异常。这是一个运行时异常。线程状态分别有:新建(NEW)、就绪(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING、计时等待(TIMED_WAIT)、终止(TERMINATED)
Java并发包提供了哪些并发工具类?
并发包也就是 java.util.concurrent 及其子包
提供了比 synchronized 更加高级的各种同步结构,包括CountDownLatch、CyclicBarrier、Semaphore,可以实现更加丰富的多线程操作。
各种线程安全的容器,比如最常见的ConcurrentHashMap、有序的ConcunrrentSkipListMap,实现线程安全的动态数组CopyOnWriteArrayList等。
各种并发队列实现,如各种BlockedQueue实现,比较典型的ArrayBlockingQueue、SynchorousQueue、PriorityBlockingQueue
强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等
Java并发类库提供的线程池有哪几种? 分别有什么特点?
newCachedThreadPool
用来处理大量短时间工作任务的线程池 特点:1、试图缓存线程并重用,无缓存线程可用时就会创建新的工作线程;2、如果闲置线程超过60秒就会终止并移出缓存;3、长时间闲置时不会消耗系统资源,内部使用SynchronousQueue作为工作队列。
newFixedThreadPool(int nThreads)
重用指定数目nThreads的线程,使用的是无界队列,任何时候最多有nThreads个线程是活动的。如果任务数量超过活动队列数目,就会在工作队列中等待空闲线程出现。如果有工作线程退出,就会有新的工作线程被创建,补足指定数目nThreads的线程。
newSingleThreadExecutor
工作线程数被闲置为1,操作一个无界工作的队列,能保证所有任务都是被顺序执行的。最多只有一个工作任务是活动的。
newSingleThreadScheduledExecutor 和 newScheduledThreadPool
可以进行定期或者周期性工作调度,区别在于是单一工作线程还是多个工作线程
newWorkStealingPool
Java8提供的线程池,内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处顺序。
请介绍类加载过程,什么是双亲委派模型?
加载过程分为三个主要步骤:加载、链接、初始化
加载阶段,它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
链接阶段
验证
这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合Java 虚拟机规范的,否则就被认为是 VerifyError。防止了恶意信息或者不合规的信息危害 JVM 的运行。有可能触发更多 class 的加载
准备
创建类或接口中的静态变量,变量(仅包括类变量不包括实例变量)所使用的内存都将在方法区中分配,并初始化静态变量的初始值(数据类型的零值)。
解析
将常量池中的符号引用(symbolic reference)替换为直接引用。
初始化阶段
执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,父类型的初始化逻辑优先于当前类型的逻辑。
双亲委派模型,简单说就是当类加载器试图加载某个类型的时候,除非父加载器找不到相应类型否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。
启动类加载器(Bootstrap Class-Loader),加载 jre/lib 下面的 jar 文件,如 rt.jar。
扩展类加载器(Extension or Ext Class-Loader),负责加载我们放到 jre/lib/ext/ 目录下面的 jar包。
应用类加载器(Application or App Class-Loader),就是加载我们最熟悉的 classpath 的内容。
谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError?
程序计数器
在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。
Java 虚拟机栈
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。在一个时间点,对应的只会有一个活动的栈帧,叫做当前帧,如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈。栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。
堆
它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享。堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。
方法区
也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息、常量、静态变量、字段、方法代码等。方法区称为永久代(Permanent Generation)JDK 8 中将永久代移除,同时增加了元数据区(Metaspace)。
运行时常量池
这是方法区的一部分。 Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用。
本地方法栈
它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。
Java常见的垃圾收集器有哪些?
垃圾收集器
Serial GC
收集工作是单线程的,进行垃圾收集过程中会进入“Stop-The-World”状态。优点是:精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。开启JVM参数是:-XX:+UseSerialGC
ParNew GC<br>
新生代 GC 的实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作 。开启JVM参数是:-XX:+UseConcMarkSweepGC -XX:+UseParNewGC<br>
Parrallel GC
早期JDK8默认GC选择,也被称作是吞吐量优先的 GC。 它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。开启JVM参数为:-XX:+UseParallelGC;还可以直接设置暂停时间或吞吐量等目标,JVM会自适应的调整:-XX:MaxGCPauseMillis=value //暂停时间 -XX:GCTimeRatio=N // GC 时间和用户时间比例 = 1 / (N+1)<br>
CMS<br>
基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。
G1 GC
兼顾吞吐量和停顿时间的 GC 实现,JDK 9默认选项,G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。G1 GC 仍然存在着年代的概念,但是其内存结构是一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。
哪些内存可以被释放
主要就是两个方面,1、最主要部分就是对象实例,都是存储在堆上的;还有就是方法区中的元数据等信息,卸载该 Java 类似乎是很合理的。对象收集采用可达性分析算法,其原理简单来说,就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和 GC Roots 之间不可达,也就是不存在引链条,那么即可认为是可回收对象。 JVM 会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,作为 GC Roots。2、方法区无用元数据的回收比较复杂,一般来说初始化类加载器加载的类型是不会进行类卸载的;而普通的类型的卸载,往往是要求相应自定义类加载器本身被回收,所以大量使用动态类型的场合,需要防止元数据区(或者早期的永代)不会 OOM。
常见的算法
复制(Copying)算法
新生代GC基本都是采用此算法,将活着的对象复制到 to 区域,拷贝过程中将对象顺序放置,可以避免内存碎片化,代价就是复制要提前预留内存空间,有一定的浪费;
标记 - 清除(Mark-Sweep)算法
首先进行标记工作,标识出所有要回收的对象,然后进行清除。 除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,不适合特别大的堆;否则,一旦出现 Full GC,暂停时间可能回无法接受。
标记 - 整理(Mark-Compact)
类似于标记 - 清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。
垃圾收集过程
年轻代:1、Java 应用不断创建对象,通常都是分配在 Eden 区域,当其空间占用达到一定阈值时,触发 minor GC。 仍然被引用的对象存活下来,被复制到Survivor区域,没有引用的对象则被回收。2、第二, 经过一次 Minor GC,Eden 就会空闲下来,直到再次达到Minor GC触发条件,这时候,另外一个 Survivor 区域则会成为 to 区域,Eden 区域的存活对象和 From 区域对象,都会被复制到to 区域,并且存活的年龄计数会被加 1。3、直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升Promotion)过程,超过阈值的对象会被晋升老年代。阈值参数:-XX:MaxTenuringThreshold=<N>
老年代:老年代 GC,具体取决于选择的 GC 选项,对应不同的算法。通常我们把老年代 GC 叫作 Major GC,将对整个堆进行的清理叫作 Full GC。
Java内存模型中的happen-before是什么?
线程内执行的每个操作,都保证 happen-before 后面的操作,这就保证了基本的程序顺序规则,这是开发者在书写程序时的基本约定。
对于 volatile 变量,对它的写操作,保证 happen-before 在随后对该变量的读取操作。
对于一个锁的解锁操作,保证 happen-before 加锁操作。
对象构建完成,保证 happen-before 于 finalizer 的开始动作。
甚至是类似线程内部操作的完成,保证 happen-before 其他 Thread.join() 的线程等。
这些 happen-before 关系是存在着传递性的,如果满足 a happen-before b 和 b happen-before c,那么 a happen-before c 也成立。
happen-before,而不是简单说前后,是因为它不仅仅是对执行时间的保证,也包括对内存读、写操作顺序的保证。仅仅是时钟顺序上的先后,并不能保证线程交互的可见性。
谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?
事务隔离级别
读未提交(Read uncommitted),就是一个事务能够看到其他事务尚未提交的修改,这是最低的隔离水平,允许脏读出现。
读已提交(Read committed),事务能够看到的数据都是其他事务已经提交的修改,也就是保证不会看到任何中间性状态,当然脏读也不会出现。读已提交仍然是比较低级别的隔离,并不保证再次读取时能够获取同样的数据,也就是允许其他事务并发修改数据,允许不可重复读和幻象读(Phantom Read)出现。
可重复读(Repeatable reads),保证同一个事务中多次读取的数据是一致的,这是 MySQL InnoDB 引擎的默认隔离级别,但是和一些其他数据库实现不同的是,可以简单认为 MySQL 在可重复读级别不会出现幻象读。
串行化(Serializable),并发事务之间是串行化的,通常意味着读取需要获取共享读锁,更新需要获取排他写锁,如果 SQL 使用 WHERE 语句,还会获取区间锁(MySQL 以 GAP 锁形式实现,可重复读级别中默认也会使用),这是最高的隔离级别。
操作共享数据时,“悲观锁”即认为数据出现冲突的可能性更大,对应到MySQL 数据库中, 悲观锁一般就是利用类似 SELECT … FOR UPDATE这样的语句,对数据加锁。
“乐观锁”则是认为大部分情况不会出现冲突,进而决定是否采取排他性措施,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。
对比Java标准NIO类库,你知道Netty是如何实现更高性能的吗?
更加优雅的 Reactor 模式实现、灵活的线程模型、利用 EventLoop 等创新性的机制,可以非常高效地管理成百上千的 Channel。
充分利用了 Java 的 Zero-Copy 机制,并且从多种角度,“斤斤计较”般的降低内存分配和回收的开销。例如,使用池化的 Direct Buffer 等技术,在提高 IO 性能的同时,减少了对象的创建和销毁;利用反射等技术直接操纵 SelectionKey,使用数组而不是 Java 容器等。
使用更多本地代码。例如,直接利用 JNI 调用 Open SSL 等方式,获得比 Java 内建 SSL 引擎更好的性能。
在通信协议、序列化等其他角度的优化。
收藏
立即使用
Collect
Get Started
Collect
Get Started
Collect
Get Started
Collect
Get Started
评论
0 条评论
下一页