JVM
说一下垃圾回收机制?
Java 语言中一个显著的特点就是引入了垃圾回收机制,在编写程序的时候不再需要考虑内存管理。垃圾回收机制可以有效的防止内存泄露,提高内存的内存率。
垃圾回收器通常是作为一个单独的低级线程运行,不可预知的情况下对堆中已经死亡的或者长时间没有使用的对象进行清理和回收。
回收机制的算法有:标记清除算法、复制算法、标记压缩算法等等。
描述一下垃圾回收的流程?
首先有三个代,新生代、老年代、永久代。
在新生代有三个区域:一个Eden区和两个Survivor区。当一个实例被创建了,首先会被存储Eden 区中。
具体过程是这样的:
- 一个对象实例化时,先去看Eden区有没有足够的空间。
- 如果有,不进行垃圾回收,对象直接在Eden区存储。
- 如果Eden区内存已满,会进行一次minor gc。
- 然后再进行判断Eden区中的内存是否足够。
- 如果不足,则去看存活区的内存是否足够。
- 如果内存足够,把Eden区部分活跃对象保存在存活区,然后把对象保存在Eden区。
- 如果内存不足,查询老年代的内存是否足够。
- 如果老年代内存足够,将部分存活区的活跃对象存入老年代。然后把Eden区的活跃对象放入存活区,对象依旧保存在Eden区。
- 如果老年代内存不足,会进行一次full gc,之后老年代会再进行判断 内存是否足够,如果足够 还是那些步骤。
- 如果不足,会抛出OutOfMemoryError(内存溢出异常)。
解释一下JVM的内存模型?
Java内存模型决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,定义了线程和主内存之间的抽象关系。
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(并不真实存在),本地内存中存储的是在主内存中共享变量的副本。
有两条规定:
- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
- 线程的工作内存是私有的,其他线程无法访问,线程变量值的传递通过主内存来完成。
GC回收的是堆内存还是栈内存?
主要管理的是堆内存。
什么时候新生代会转换为老年代?
- Eden区满时,进行Minor GC时。
- 对象体积太大, 新生代无法容纳时。
- 虚拟机对每个对象定义了一个对象年龄(Age)计数器。当年龄增加到一定的临界值时,就会晋升到老年代中。
- 如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,包括比这个年龄大的对象就都可以直接进入老年代。
创建占大内存的对象分配到哪一代?
如果新创建的对象占用内存很大,则直接分配到老年代
新生代2个Survivor区的好处?
解决了内存碎片化问题。整个过程中,永远有一个Survivor区是空的,另一个非空的Survivor区是无碎片的。
遇到过OOM怎么解决?
我们可以修改虚拟机的参数,获取Heap Dump的文件,后缀名是.hprof。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:\jvm
之后可以使用JDK自带的一个工具jvisualvm来进行排查和定位。
Java基础
int和Integer的区别?
- int是基本数据类型,Integer是它的包装类。
- Integer保存的是对象的引用,int保存的变量值。
- Integer默认是null,int默认是0。
- Integer变量必须实例化后才能使用,而int变量不需要。
Java的基本数据类型和大小?
单位:字节
boolean(1) = byte(1) < short(2) = char(2) < int(4) = float(4) < long(8) = double(8)
Java类冲突怎么解决(jar包冲突)?
- 使用mvn: dependency tree查看冲突的jar
- 然后在pom文件里边 使用exclusion标签排除掉这些冲突的jar包
接口和抽象类的区别?
- 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
- 类可以实现很多个接口,但是只能继承一个抽象类。
- Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
- Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。
多线程
多线程的创建方式
- 继承Thread类
- 实现Runnable接口
- 直接使用线程池
实现Runnable接口这种方式更受欢迎,已经继承别的类的情况下只能实现接口。
Sleep(0)表示什么?
触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。
线程有三个状态,就绪态,运行态,等待态。Sleep(n)方法是让当前线程在n秒内不会参与CPU竞争。线程进入等待队列,n秒之后再次进入就绪队列。
Sleep(0)是让线程直接进入就绪状态。
Sleep与Wait区别?
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对象调用wait方法导致本线程放弃对象锁,进入等待池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入锁池准备抢夺对象锁。
Thread.Join方法
Thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
Lock锁与synchronized的区别?
- Lock能完成synchronized的所有功能。
- Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。
- Lock可以知道是不是已经获取到锁,而synchronized无法知道。
线程不安全的问题在多线程怎么解决?
可以使用synchronized、lock、volatile来实现同步。
谈谈你对volatile的理解?
volatile是轻量级的synchronized,比它的执行成本更低,因为它不会引起线程的上下文切换,它保证了共享变量的可见性,可见性的意思是当一个线程修改一个变量时,另外一个线程能读到这个修改的值。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。还有就是它通过添加内存屏障的方式禁止指令的重排序。
如何实现主线程等待子线程执行完后再继续执行?
- 我们可以使用join方法,在主线程内部调用子线程.join方法。
- CountDownLatch实现
这是一个属于JUC的工具类,从1.5开始。主要用到方法是countDown() 和 await()。
- await()方法阻塞当前线程,直到计数器等于0。
- countDown()方法将计数器减一。
思路:我们可以在创建CountDownLatch对象,然后将此对象通过构造参数传递给子线程,在开启子线程后主线程调用await()方法阻塞主线程,子线程调用countDown()方法计数器减一。
简单说一下synchronize
synchronize是java中的关键字,可以用来修饰实例方法、静态方法、还有代码块;主要有三种作用:可以确保原子性、可见性、有序性。
- 原子性就是能够保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等该线程处理完数据后才能进行。
- 可见性就是当一个线程在修改共享数据时,其他线程能够看到。
- 有序性就是,被synchronize锁住后的线程相当于单线程,在单线程环境jvm的重排序是不会改变程序运行结果的,可以防止重排序对多线程的影响。
synchronried的底层原理
synchronized的底层原理是跟monitor有关,也就是视图器锁,每个对象都有一个关联的monitor,当Synchronize获得monitor对象的所有权后会进行两个指令:加锁指令跟减锁指令。
monitor里面有个计数器,初始值是从0开始的。如果一个线程想要获取monitor的所有权,就看看它的计数器是不是0,如果是0的话,那么就说明没人获取锁,那么它就可以获取锁了,然后将计数器+1,也就是执行monitorenter加锁指令;monitorexit减锁指令是跟在程序执行结束和异常里的,如果不是0的话,就会陷入一个堵塞等待的过程,直到为0等待结束。
有几种线程池?并且详细描述一下线程池的实现过程
- newFixedThreadPool创建一个指定大小的线程池。每当提交一个任务就创建一个线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务