关于JVM
深入理解Java类加载器(ClassLoader) https://blog.csdn.net/javazejian/article/details/73413292 优秀参考文章 《深入理解Java虚拟机》https://blog.csdn.net/zhang_jiayuan/article/details/82083163#%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8 |
堆: 存储对象 , 新生代,老年代及永久代(存放类和元数据Meta)-->java8已经移除 因为会产生OOM异常 被"元空间"取代(它使用的是本地内存)
java栈:每个方法执行时创建栈帧(局部变量表<编译时期的基本数据类型,对象引用等>,操作数栈,动态链接,返回地址) 动态存储(数据目标在运行时刻内存分配是暂时的,存储的都是局部变量,临时变量,对象引用) 也就是OS在建立线程时为这个线程建立的存储区域,该区域有先进后出的特性 栈是暂时的 数据具有生命周期 属于临时存储 栈中共享数据与两个对象引用指向一个对象不同,他是由编译器完成 利于节省空间
程序计数器:
本地方法栈:
方法区:存储JVM加载的类信息,常量,静态变量(包括静态成员变量)static/final,运行时常量池,即时编译器编译后的代码等数据-->堆的逻辑部分
1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;(CPU存放计算数据的地方)
1. 栈:存放基本类型(会包含这个基本类型的具体值) 的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放的常量池中),局部变量【注意:(方法中的局部变量使用final修饰后,放在堆中,而不是栈中)】
2.堆:存放使用new创建的对象(对象本身)及数组,全局变量,非静态成员变量(锁和同步,垃圾回收机制)
https://blog.csdn.net/lin542405822/article/details/80338256
堆区: 堆这块区域是JVM中最大的,应用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 gc 主要的回收区,一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:新生代、年老代、永久代
|
第三行 创建了两个对象 一个常量池存储的 String对象 一个是存在堆中的String("xyz")对象 以及一个引用 在栈中c引用
我们先来记住两条黄金法则:
1.引用类型总是被分配到“堆”上。
2.值类型总是分配到它声明的地方:
a.作为引用类型的成员变量分配到“堆”上
b.作为方法的局部变量时分配到“栈”上
参考理解(含代码例子) https://www.cnblogs.com/Mind-Hacker/archive/2013/07/24/3210223.html
gc是针对堆进行垃圾回收的,栈是线程创建的时候就固定了的,它跟线程是绑定的。 对象 ,类信息 ,常量等都是保存在堆中。gc有很多中,但1.7以后基本上都是用的G1
参考: 扒一扒JVM的垃圾回收机制,下次面试你准备好了吗
-----------------------------
参考 JVM性能调优
还有一个问题是,垃圾回收动作何时执行?
另一个问题是,何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出
满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。
除直接调用System.gc外,触发Full GC执行的情况有如下四种。
1. 老生代空间不足
老生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2. Permanet Generation(永久代)空间满
PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space .为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
3. CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
4. 统计得到的Minor GC晋升到老生代的平均大小大于老生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
大部分的GC采用了分代收集算法
三种新生代回收器
三种老生代回收器
CMS收集器 目标为获取最短停顿时间 (良好响应速度) 标记清除算法 优点为并发标记和低停顿 步骤: 初始标记(Stop the World) 并发标记 重新标记(Stop the World) 并发清除 总体来说,CMS收集器的内存回收过程就是与用户线程一起并发执行的。 缺点:浮动垃圾无法处理 可能会concurrent mode failure 导致另一次 full GC产生; 产生空间碎片 ; CPU资源敏感 默认启动线程数为 (CPU数+3)/4 因此 CPU不足4时 影响很大
|
G1收集器 G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:
在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,[横跨整个堆内存]它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合(通过rememberset来避免全表扫描)。 虽然G1看起来有很多优点,实际上CMS还是主流。 详细补充 参考 JVM中的G1垃圾回收器 步骤: 初始标记(GCroots直接关联到的对象(Stop the World) ) 并发标记(可达性分析) 最终标记(修正程序运行时变动部分) 筛选回收(排序 根据停顿时间制定回收计划)
与GC相关的常用参数 除了上面提及的一些参数,下面补充一些和GC相关的常用参数:
参考 http://www.importnew.com/26383.html |
*首先了解类加载机制 https://blog.csdn.net/qq_38182963/article/details/78660779
Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,(准备)转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载器的任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM中,
然后转换为一个与目标类对应的java.lang.Class对象实例。
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这动作的代码模块成为“类加载器”。
类和类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。这句话可以表达的更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这个两个类就必定不相等。
先看父类类加载器是否有相应类加载器 由上到下查询 双亲委派方式的作用:保证JDK核心类的优先加载;(保证类的唯一)
所有的代码都在java.lang.ClassLoader中的loadClass方法之中
逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass方法, 如父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass方法进行加载。
如何打破双亲委派模型?
参考2 : https://www.cnblogs.com/wxd0108/p/6681618.html
1.第一次破坏
由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。
2.第二次破坏
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美。
如果基础类又要调用回用户的代码,那该么办?
一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,
它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
3.第三次破坏
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
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)否则,类加载器失败。
*Tomcat 加载器为何违背双亲委派模型?
如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。我们再看第二个问题,我们想我们要怎么实现jsp文件的热修改(楼主起的名字),jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*、/server/*、/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
*如果tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类
可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。