类加载器
类装载器ClassLaoder负责加载class文件,class文件开头有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则有Execution Engine决定虚拟机自带的类加载器
引导类加载器
这个类加载使用C/C++与亚伯实现的,嵌套在JVM内部,它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
并不继承自java.lang.ClassLoader,没有父加载器
加载扩展类和应用程序类加载器,并指定未他们的父类加载器,处于安全考虑,Bootstrap启动类加载器只加载包名未java、javax、sun等开头的类
扩展类加载器
Java语言编写
派生于ClassLoader类
父类加载器为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext,子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
应用类加载器
java语言编写
派生于ClassLoader类
父类加载器为扩展类加载器
它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库,改类加载器是程序默认的类加载器
自定义加载器
堆
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。堆逻辑上分为三部分:新生+养老+永久
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集、结束生命。新生区又分为两部分,伊甸区和幸存者区,所有的类都是在伊甸区被new出来的。幸村区有两个:0区和1区。当伊甸区的空间用完时,程序又创建对象,JVM的垃圾回收器将对伊甸区的进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,然后将将伊甸园区中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区.那如果1区也满了呢?
再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区内存清理。若养老区执行了Full GC之后依然无法进行对象的保存。就会产生OOM异常。
如果出现java.lang.OutofMemoryError:java heap space 异常,说明Java虚拟机的堆内存设置不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms,-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用),在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似
元空间和永久代之间最大的区别在于:
永久代使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存,因此默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入Java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制
Java虚拟机栈
栈管运行,堆管存储
栈也叫主内存,主管Java程序的运行,是线程创建时创建,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束改栈就over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配
栈存储什么?
栈帧主要保存三类数据:
本地变量:输入参数和输出参数以及方法内的变量(8中基本数据类型,对象的引用地址),部分结果并参与方法的调用和返回
栈操作:记录出栈、入栈的操作
栈帧数据:包括类文件、方法等等。
栈运行原理:
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256k~756K之间
,约等于1Mb左右,不存在垃圾回收问题
本地方法栈
方法区
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时的常量池、字段、和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同的虚拟机里头实现是不一样的,最典型的就是永久代和元空间。
寄存器
记录了方法之间的调用和执行情况,类似排班值日表,用来存储指向下一条指令的地址,也即将要执行的指令代码,它是当前线程所执行的字节码的行号指示器每个线程都有一个程序计数器,是线程私有的,就是一个指针,指到方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令如果执行的是一个Native方法,那这个计数器是空的,用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OOM) 错误。
双亲委派机制的好处:
防止类被重复加载
保护程序安全,防止核心API被随意篡改
GC四大算法:
引用计数法:
每次对对象赋值时要维护引用计数器,且计数器本身也有一定的消耗较难处理循环引用,JVM的实现一般不采用这种方式
复制算法
年轻代中使用的MinorGC这种GC算法采用的是复制算法(Coping)
HOtSpot把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例8:1:1,一般情况下新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当他的年龄增加到一定程度时,就会被移动到老年代中。因为年轻代中的对象基本都是朝生夕死(90%),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
标记清除
老年代一般是由标记清除或者标记清除与标记整理的混合实现,算法分成标记和清除两个阶段,先标记要回收的对象,然后统一回收这些对象,用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行
优点:不需要额外空间
缺点:两次扫描耗时严重,会产生内存碎片
标记压缩:
标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存或对象的引用地址。从效率上来说,标记/整理算法要低于复制算法,没有最好的收集算法,只有最合适的收集算法分代收集算法
年轻代的特点是区域相对老年代较小,存活率低这种情况复制算法的回收整理速度是最快的。老年代的特点是区域大,对象存活率高,这种情况复制算法明显变得不合适。一般是由标记清除或标记整理的混合实现