进程与线程的区别

进程是资源分配的最小单位,线程是程序执行的最小单位。进程是一个动态的概念,就是一个程序执行过程的总称包括独立的内存,线程是依赖于进程的,一个进程内的线程是共用内存等资源的。

进程的创建、销毁、切换、通讯等操作开销都比线程高。

说说线程的状态有哪些

创建、就绪、运行、等待、阻塞等待、结束。创建好进入创建状态,调用start进入就绪状态等待CPU调度,调度了就进入运行状态,如果被调用sleep,wait等方法就会进入超时等待,到时间或者调用notify回到就绪状态,无参的wait方法进入等待状态,调用notify进入就绪状态。如果在等待锁等进入阻塞状态。结束就进入结束状态。其中sleep和wait的主要区别是wait会释放锁,sleep不会释放锁,但是平时使用表明sleep也会让出CPU资源。

说说jvm内存结构

jvm内存结构分为方法区、虚拟机栈、本地方法栈、程序计数器、堆。方法去主要是存放类信息,常量这些数据,线程共享的。虚拟机栈和本地方法栈保存的是运行时数据比如局部变量、操作数栈等,这部分是线程私有的。

程序计数器主要是记录字节码执行的位置,执行到哪,这里也是线程私有的。堆主要是保存对象实例的区域,这里是最大的一块内存区域也是垃圾回收的重点区域,它是线程共享的。

说说对象创建过程

步骤顺序为类加载、内存的分配,零值初始化、对象头设置、执行init方法初始化对象。先在方法区检查是否已经有类信息,没有则先进行类加载

类加载完后就可以确定对象会占用多少内存,所以接下来就进行内存的分配。内存分配我完成后就需要进行零值的初始化,将对应的字段设置成相应的零值,保证不初始化也能读取到零值。

之后进行对象头的设置比如指向类信息、哈希码、年代信息、锁状态等信息。到这里对象就产生了,只是还需要根据程序的意愿调用init函数进行字段的赋值初始化。就完成了对象创建。

说说垃圾回收算法和垃圾回收器

jvm是通过可达性分析的手段识别垃圾的,选取一些变量作为GC root(比如局部变量、方法区的静态变量常量等)开始向下搜索遍历所有引用链,当一个对象没有一个GC root的引用链相连的话说明该对象是垃圾。

垃圾回收算法三种,复制算法,先将内存分为两部分,先用一部分,用完后将还存活的对象复制到两外一部分,之后回收该部分所有内存。这个算法的优点是效率高,在存活对象比较少的情况效率就很高。

标记-清除算法,这个算法先标记不需要回收的对象,之后将需要回收的进行回收,这个算***导致空间碎片化严重效率也不高,但是回收对象比较少的时候比较有效。

标记-整理算法,这个算法先标记不需要回收得对象,然后将其统一的移动到一边,之后把另一边全部清除,这个算法是对标记清除算法的改进,解决内存碎片问题。

因为三种垃圾回收算法都是在合适的场景才能发挥最大的功效,所以JVM进行了分代回收的思路,存活久的放老年代,朝生幕死的放新生代。不同的年代区域使用不同的回收算法

serial收集器,在新生代大多是朝生幕死的对象使用复制算法,将内存分三部分,两个幸存区和eden区每次GC将存活对象复制到另外一块没使用得幸存区后全部清清除两个幸存区交替使用,还有空间担保的机制,让老年代区空间担保存活对象能全部复制到幸存区,放不了就进入担保区,老年代可回收的对象比较少使用标记整理算法。这个回收器简单而高效,但是是单线程的不能充分利用CPU资源,而且标记和回收的过程都是停顿的有可能导致停顿过久。

parNew是serial的多线程版本,其他基本都一样只是在做标记和清除的时候使用了多线程。

CMS收集器,这个回收器是以获取最短停顿时间为目的的回收器,它将垃圾回收分四个步骤,1、初始标记,这部分是停顿其他线程的,但是只是对GC root进行标记,很快的,2、并发标记,这部分是根据第一步的GC root进行标记进一步标记的过程,这步和用户线程一起工作的,虽然耗时但是用户线程可以继续工作,3、重新标记,标记第二步过程中用户再次产生的垃圾,这部分是和停顿的,但是因为第一第二步已经标记了大量的垃圾所以这步也很快。4、并发清除,和用户线程一起运行的,使用标记清除的算法进行清理垃圾。

CMS回收器可以回收算法线程数但是还是会出现占用一部分用户CPU资源,而且由于清除是和用户线程一起的所以会出现清除阶段新产生的垃圾浮动垃圾。因为用标记清除算法同样也有内存碎片问题。对应内存碎片问题设置整理机制,FGC前先整理一次内存保证可用内存连续。

G1收集器,这个回收期前三个步骤和CMS是一样的,只是第四个步骤的时候,G1也是需要停顿的但是停顿是可以预测的,就是说JVM提供一个参数给用户设置可接受的停顿时间,G1根据用户的接受时间尽可能的回收更多的垃圾。

G1先是将内存分为一个一个的区域,区域大小是用户可以设置的,回收的时候是将存活的对象统一移动到一些区域,然后将其他区域全部回收。G1会通过用户设置的停顿时间进行筛选回收价值最大的区域。G1也还保留着分代的概念,但是不同年代的区域是可以不连续的,而且新生代和老年代都使用G1回收器,对于分代算法的一些规则比如长期存活的对象进入老年代的这些都一样。

说说知道哪些GC种类

young GC 新生代内存使用率达到一定的条件后触发,比较频繁

old GC 老年代内存使用了达到一定条件后触发,相对young GC妹那么频繁

full GC 有很多情况会发生,总的来说就是空间满了没内存分配了就会进行一次full GC比如没有连续空间分给大对象,担保失败等 ,也可以进行手动调用system.gc。

说说类加载机制

分为加载、验证、准备、解析、初始化五个阶段。

加载阶段是更具类名加载类信息到方法区并生成一个对应的class对象。

验证阶段主要是对class文件进行验证,格式验证字节码验证等保证class的正确性。

准备阶段是将静态变量等在方法区分配内存并赋值相应的0值。特殊的final修饰的字段这个阶段将赋值最终值。

解析阶段是将符号应用变成直接引用的过程。

初始化阶段是执行初始化函数进行内存初始化比如static代码块,static变量的初始化等。

说说双亲委派模型、何时被破坏

一个类的相同的类由类全限定名和类加载器共同决定,所以即使同一个类不同的类加载器加载也会别jvm识别为不同的类。jvm提供三种类加载,启动类加载器、扩展类加载器、应用程序类加载器,这三个加载器分别负责不同路劲的类的加载。

jdk里面的一些类比如object类,如果我们自定义一个类名然后加载,内存里面岂不是由很多个object类,就混乱了。

为了避免这种情况,jvm提出双亲委派模型的建议,既接到一个类加载的申请先委派给父类加载器进行加载,父类无法加载再自己加载,这样无论是哪个加载器加载object最后都会委派给启动类加载器加载,得到一样的object.

双亲委派模型只是一个约定是可以被破坏的,说两个比较经典的破坏场景,比如JDBC这种SPI,基础类只提供了接口,实现类需要用户自己引入相应的驱动,然后虚拟机自动的扫包去寻找实现类,扫包寻找实现类这个方法所在的类的是启动类加载器加载的,所以这个实现类应该也是要被启动类加载器加载的,但是启动类加载器是不可能加载这个实现类的,因为启动类加载器的加载目录(/lib)里面不可能有这个实现类的,但是为了实现SPI做了一个破坏双亲委派模型的机制,也就是线程上下文加载器默认是应用程序类加载器,使启动类加载器可以委派应用程序类加载器去加载实现类。

还有一个模块化热部署的场景,多模块时每个模块有自己的类加载器,模块之间引用时,方一个模块收到一个类加载的请求后先是去请求启动类加载器再到扩展类加载器,之后就到引用的其他平行模块最后才到自己,可见前两步是符合双亲委派模型的,但是后就不会符合了因为不同模块的类加载器之间是平行的没有父子关系,虽然不符合但是这机制无疑是实现动态化、热部署等特性的最佳方式。