JVM(一)
First And MOST Important
JVM内存模型

1.本地方法栈
线程方法内部私有,为native方法提供服务。
2.程序计数寄存器
线程方法内部私有,存放执行指令的偏移量和行号指示器等。
3.虚拟机栈
线程私有,描述Java方法执行的内存区域。
- 局部变量表:存放方法参数和局部变量的区域。
- 操作数栈:用来存放操作数。
- 动态链接/当前类常量池引用:一个在常量池中对当前方法的引用。
- 方法返回值
4.堆区
堆分为新生代和老年代——新对象在新生代创建,后续进入老年代[超大对象直接创建在老年代,当然前提是有足够大的空间咯]
新生代=1个Eden+2个Survivor区
JVM里的有几种classloader,为什么会有多种?
参考
从开发角度看,类加载器分为:
1.启动类加载器(Bootstrap ClassLoader)---VM层
负责将放置在JAVA_HOME/lib目录中或-Xbootclasspath所指定路径中并且虚拟机能识别的类库加载到JVM中。
启动类加载器在VM中,无法在Java程序直接使用。
2.扩展类加载器(Extension ClassLoader):
由sun.misc.Launcher#ExtClassLoader实现,它负责加载<JAVA_HOME>/lib/ext目录中或系统变量--java.ext.dirs所指定的路径中的类库。
开发者可以直接使用扩展类加载器
3.应用程序类加载器(Application ClassLoader):
这个类加载器由sum.misc.Launcher#AppClassLoader来实现。它负责加载用户类路径上所指定的类库。
开发者可以直接使用应用程序类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
什么是双亲委派机制,有何优点。

loadClass的逻辑顺序[双亲委派]
1.调用findLoadedClass,检查是否已经加载过指定类了
2.如果parent:ClassLoader存在,则循环调用到最原始的parent;如果parent为null,则使用bootstrap class loader加载[返回值可能为null]
3.如果上一步为加载到类,则调用findClass[如果未被子类覆盖,则会抛出classNotFoundException]
4.如果入参resolve为true,则ClassLoader将被按照Java™规范中的Execution描述进行链接[如果类未被链接]
双亲委派模型很好地解决了各个类加载器对基础类的加载问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。
什么情况下需要破坏双亲委派模型[Tomcat破坏了]
参考:
1.如果基础类又要调用用户的代码,那该怎么办呢,如JNDI服务,它的代码由启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能"认识"这些代码,为了解决这个困境,Java设计团队引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
2.追求程序的动态性时可能会导致破坏双亲委派模型,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
如OSGi,OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。
垃圾收集算法?
1.标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的主要不足有两个:一是效率问题,标记和清除效率都不高,二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活这的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
3.标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费50%的空间就要使用额外的空间进行分配担保(Handle Promotion当空间不够时,需要依赖其他内存),以应对被使用的内存中所有对象都100%存活的极端情况对于“标记-整理”算法,标记过程仍与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有的存活对象都向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法
当前的商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把堆划分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收
Minor GC 和Full GC有什么区别?
从年轻代空间(Eden和Survivor区)回收内存称为Minor GC。
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,所以 Eden 和 Survivor 区不存在内存碎片,写指针总是停留在所使用内存池的顶部。
执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
Full GC 是清理整个堆空间—包括年轻代和永久代。
GC流程

参考资料
《码出高效》
https://blog.csdn.net/zhangcanyan/article/details/78993959
https://www.jianshu.com/p/166c5360a40b
https://blog.csdn.net/qq_38182963/article/details/78660779