java虚拟机由4个部分组成,
- Class Loader类加载器,加载class文件到内存中
- Execution Engine解析class文件的字节码指令去执行
- Natvie Inerface 融合不同开发语言的原生库为java所用
- Runtime Date Area JVM内存空间结构模型
为什么使用反射?
java反射机制是在运行状态中,对于任意一个类,都是够知道这个类的属性和方法,对于任意一个类都能够调用它任意的方法和属性,这种动态获取信息的以及动态调用对象方法的功能称之为java反射。
类从编译到执行的过程
编译器将Robot.java源文件编译成Robot.class字节码文件
ClassLoader将字节码文件转换成Class<Robot>对象
什么是ClassLoder?
ClassLoder在java中有这非常重要的作用,它主要工作在class装载的加载阶段,其主要作用是从系统外部获取Class二进制数据流,它是java的核心组件。所有Class都是由ClassLoader进行加载的,ClassLoader复制将Class文件里的二进制数据流装载进系统然后交给Java虚拟机进行连接,初始化等操作。
ClasLoder的种类
- BootStrapClassLoder :C++编写 ,加载核心类库 java.*
- ExtClassLoader: java编写加载扩展库 javax.*
- AppClassLoader : java编写,加载程序所在目录
- 自定义ClassLoader: java编写定制化加载
ClassLoader的双亲委派机制
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException
),子加载器才会尝试自己去加载。
类的加载方式
- 隐式加载:new
- 显式加载:loadClass,forName,
Class.forName得到的class是已经初始化完成的(也就是初始化了类静态变量的值以及代码块代码)
ClassLoader.loadClass得到的是class还没有链接的(只是加载到内存中)
java的内存模型
线程私有: 程序计数器、虚拟机栈,本地方法栈。
线程共享的:mateSpace、java堆
- 程序计数器 1当前线程所执行的字节码行号指示器,2改变计数器的值来选取下一条需要执行的字节码指令,3和线程是一对一的关系即“线程私有”,4java方法计数如果是native方法则计数器值为undefined,5不会发生内存泄漏。
- java虚拟机栈 1java方法执行的内存模型,2包含多个栈帧。
- 本地方法栈 这部分主要与虚拟机用到的 Native 方法相关,一般情况下, Java 应用程序员并不需要关心这部分的内容。
- java堆是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。
- mateSpace 元空间。
JVM三大性能调优参数-Xms、-Xmx、-Xss的含义
-Xss:规定了每个线程虚拟机栈(堆栈)的大小。258k足够,影响并发线程数的大小。
-Xms:初始的java堆的大小,改线程刚创建出来的java堆的大小。如果超过到了java堆容量就会扩容,扩容至-Xmx大小。
-Xmx:java堆能扩容至最大大小。(一般Xms和Xmx设置一样,应该扩容时发生内存抖动影响性能)
java内存的堆和栈的区别
-内存分配策略
- 静态存储;在编译时确定每个数据目标在运行时的存储空间需求。
- 栈式存储:动态存储,数据区需求在编译时未知,运行时模块入口前确定。
- 堆式存储;编译时或运行时模块入口都无法确定,动态分配。
- 管理方式;栈自动释放,堆需要GC处理。
- 空间大小;栈比堆小,
- 碎片相关;栈产生的碎片远小于堆。
- 分配方式;栈支持静态分配和动态分配,而堆仅仅支持动态分配。
- 效率;栈的效率比堆高
不同JDK版本之间的intern()的区别---JDK6 VS JDK7+
String s = new String("a");
s.intern();
- jdk6 ; 当调用intern方法时,如果字符串常量池先前已创建出改字符串对象。则返回池中该字符串的引用,否则将此字符串对象添加到字符常量池中,并且返回改字符串对象的引用。
- jdk7+; 当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中改字符串的引用,否则将该字符串对象已经已经在与java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
GC垃圾回收机制
什么对象被判定为垃圾的标准?
当一个对象没有被任何对象引用时,对系统而言它就是垃圾,占据的内存就应该被释放,同时此对象应该被销毁。
判断对象有没有被引用有两种方法:
- 引用计数算法,通过判断对象的引用数量来决定对象是否可以被回收,每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1。优点:直接判断引用计数器为0的对象,执行效率高,程序执行受影响较小。确定是无法检测出循环引用的情况,导致内存泄漏。
- 可达性分析算法,通过判断对象的引用链是否可达,离散数学图论,程序把所有的引用关系看做一张图,从GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路劲,就被成为引用链,Reference Chain,当一个对象从GC Roots没有Reference Chain和引用链相连,从图论上来说就是从GC Roots到这个对象是不可达。这样就被认为这个对象是不可用,这样这个对象就被标记为垃圾了。
可用做为GC Root的对象,a.虚拟机栈中引用的对象(栈帧中的本地变量表)。b.方法区中的常量引用的对象。 c.方法区中的类静态属性引用的对象。d.本地方法栈中JNI(Native方法)的引用对象。e.活跃线程的引用对象。
谈谈你了解的垃圾回收算法
标记清除算法;
- 标记:从根集合进行扫描对存活的对象进行标记。
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存。
标记清楚算法由于不需要移动原本的对象位置,直接抽取清除垃圾对象,比较高效,但是内存碎片化比较严重。
复制算法;
分为对象面和空闲面,对象在对象面上创建,存活的对象被从对象面复制到空闲面,将对象面所有的对象内存清除。
标记整理算法;
- 标记:从根集合进行扫描对存活的对象进行标记。
- 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
分代收集算法;
垃圾回收算法的组合拳,按照对象生命周期的不同划分以采用不同的垃圾回收算法,目的提高JVM的回收效率。(java6,7堆内存分为年轻代、老年代和永久代这三个模块,jdk8版本永久代被去除,年轻代对象存活率低采用复制算法,老年代对象存活率高采用标记清楚算法或者标记整理算法)
分代收集算法的GC分为两种
Minor GC ;是在年轻代中工作,采用的是复式算法...
常见的垃圾收集器
年轻代常见的垃圾收集器
Serial收集器(-XX:+UseSerialGC,复制算法)
- 单线程收集,进行垃圾收集时,必须暂停所有的工作线程
- 简单高效,Client模式下默认的年轻代收集器
ParNew收集器(-XX:+UseParNewGC,复制算法)
- 多线程收集,其余的行为,特点和Serial收集一样
- 单核执行效率不如Serial,在多核执行才有优势
Pa(Ru):包含Running等待线程调度选中和Ready在线程池中等待线程调度和选中
- 比起关注用户线程停顿时间,更关注系统的吞吐量。(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
- 在多核执行才有优势,Server模式下默认年轻代收集器
老年代常见的垃圾收集器
Serial Old收集器(-XX:+UseSerialOldGC,复制算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)
- 多线程,吞吐量优先
- CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
- G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
- Garbage First 收集器的特点
- 并发和并行
- 分带收集
- 空间整合
- 可预测的停顿
- 将整个Java堆内存划分多个相等的region
- 年轻代和老年代不在物理隔离
GC相关的面试题QC
Object的finalize()方法的作用是否与C++的析函数作用相同?
- 与C++的析构函数不同,析构函数调用确定,而它的是不确定的
- 将未被引用的对象放置于F-Queue队列
- 方法执行随时可能被终止
- 给予对象最后一次重生的机会
Java中的强引用,软引用,弱引用,虚引用有什么用。
强引用(Strong Reference)
- 最普遍的引用:Object obj = new Object()
- 抛出OutOfMemoryError终止程序也不回被回收具有强引用的对象
- 通过将对象设置为null来弱化引用,使其被回收
软引用(Soft Reference)
- 对象处在有用但是非必须状态
- 只有当内存空间不足时,GC会回收改引用的对象的内存
- 可以用来实现内存敏感的高速缓存
String str = new String("abc");
SoftReference<String> softRef = new SoftReference<String>(str);
弱引用(Weak Reference)
- GC时被回收
- 被回收的概率也不大,因为GC线程优先级比较低
- 适用于引用偶尔被使用且不影响垃圾收集的对象
String str = new String("abc");
WeakReference<String> softRef = new WeakReference<String>(str);
虚引用(PhantomReference)
- 不会觉得对象的生命周期
- 任何时候都可能被垃圾回收器回收
- 跟踪对象被垃圾回收器回收的活动,起哨兵作用
- 必须和引用队列ReferenceQueue联合使用
String str = new String("abc");
ReferenceQueue<String> queue = new ReferenceQueue<String>(str);
PhantomReference ref = new PhantomReference(str ,queue)
引用队列(ReferenceQueue)
- 无实际存储结构;存储逻辑依赖于内部节点之间的关系来表达
- 存储关联的且被GC的软引用,弱引用以及虚引用
关于JDK版本的选择
最好选择JDK8,JDK11,因为这是Oracle长期支持的版本。
什么是进程和什么是线程
进程和线程的由来
- 串行:初期的计算机智能串行执行任务,并且需要长时间等待用户的输入
- 批处理:预先将用户的指令集中成清单,批量串行处理用户的指令,仍然无法并发执行
- 进程:进程独占内存空间,保存各自运行的状态,互相间不干扰且可以相互切换,为并发处理任务提供了可能。
- 线程:共享进程的内存资源,相互间切换更加快速,支持更细粒度的任务控制,使进程内的子任务得以并发执行。
- 线程不能看做独立应用,而进程可看做独立的应用
- 进程有独立的地址空间,互相不影响,线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程的程序健壮
- 进程的切换比线程的切换开销大
Java进程和线程的关系?
- Java对操作系统提供你的功能进行封装,包括进程的线程
- 运行一个程序会产生一个进程,进程包装至少一个线程
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆
- Java采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
Thread中的start和run方法的区别?
- 调用start()方法会创建一个新的子线程并启动
- run()方法是是Thread的一个普通方法的调用
Thread和Runnable是什么关系?
- Thread是实现了Runnable的类,使得run支持多线程
- 因类的单一继承原则,推荐多使用Runnable接口
如何给run()方法传参?
- 构造函数传参
- 成员变量传参
- 回调函数传参
如何实现处理线程的返回值
- 主线程等待法
- 使用Thread类的join()阻塞当前以等待子线程处理完毕
- 通过Callable接口实现:通过FutureTask 或者线程池获取
线程的六个状态
- 新建(New):创建后尚未启动的线程的状态,(新创建了一个线程对象,还没调用start方法)
- 运行(Runneble):包含Running等待线程调度选中和Ready在线程池中等待线程调度和选中
- 无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
- 没有设置Timeout参数的object.wait()方法。
- 没有设置Timeout参数的Thread.join()。
- LockSupport.part()方法。
- 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒
- Thread.sleep()方法。
- 设置了Timeout参数的object.wait()方法。
- 设置了Timeout参数的Thread.join()。
- LockSupport.parkNanos()方法。
- LockSupport.parkUntil()方法。
- 阻塞(Blocked):等待获取排他锁
- 结束(Terminated):已终止线程的状态,线程已经结束执行
sleep和wait的区别
基本差别
- sleep是Thread类的方法,wait()是object类中定义的方法
- sleep方法可以在任何地方使用,wait()方法只能在synchronize方法或者synchronized块中使用
最主要的本质区别
- Thread.sleep()只会让出CPU,不会导致锁行为的改变
- Object.wait()不仅让出CPU,还会释放占有的同步资源的锁
notify和notifyAll的区别
两个概念
- 锁池EntryList,当其他线程执行到有锁方法时,无法获取锁只能阻塞时,当前线程进入锁池
- 等待池WaitSet,当使用wait方法释放锁,进入等待池,等待其他线程唤醒或者等待期限结束。
notifyAll会让所有处于等待的线程全部进入锁池去竞争获取锁的机会,notify只会随机选取一个处于等待池的线程进入锁池去竞争获取锁的机会
--个人理解当所有线程需要锁才能执行下,notify只会随机唤醒一个线程去抢锁执行,其他线程没有被唤醒根本不会去抢锁。notifyAll会唤醒所有的线程去抢锁,抢到锁的执行,没有抢到锁的在锁池,一旦抢到锁依然会执行。
yield的概念
当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能忽视这个暗示。(就算出让CPU也不会释放锁)
如何中断线程
调用interrupt(),通知线程应该被中断了
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InerruptException异常
- 如果线程处于正常活动状态,那么线程将改线程的中断标识设置true,被设置中断标志的线程将继续正常运行不受影响。
需要被调用的线程配合中断
- 在正常运行的任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
- 如果线程处于正常的活动状态,那么会将该线程的中断标识设置为true,被设置中断标志的线程将继续正常运行,不受影响。
synchronized
互斥锁的特效
- 互斥性:及在同一时间只允许一个线程持有某个对象的对象锁,通过这个特效来实现多线协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问,互斥性也称之为操作的原子性。
- 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于最后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能在本地缓存的某个副本上继续操作,从而引起不一致。
根据获取锁的分类:获取对象锁和获取类锁
获取对象锁
- 同步代码块(synchronized(this),synchronized(类的实例),锁的是小括号()中的实例对象)。
- 同步非静态方法,synchronized method,锁是当前对象的实例对象。
获取类锁
- 同步代码块(synchronized(类.class)锁的是小括号()中的Class对象)。
- 同步非静态方法,synchronized static method,锁是当前对象的类对象,Class对象。
对象锁和类锁的总结
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象的同步方法的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步方法的线程会被阻塞;反之亦然;
- 同一个类的不同对象的对象锁互不干扰;
- 类锁由于也是一直特殊的对象锁,因此和上诉1,2,3,4一致,而由于一个类只有一把对象锁,所有同一个类的不同对象使用类锁,将会是同步的;
- 类锁和对象锁互不干扰;
synchronized底层实现原理
这个概念性的东西太多了,还有JVM指令,容我先缓缓神...o(╥﹏╥)o