JVM是可运行java代码的假想计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收、堆和一个存储方法域。JVM是运行在操做系统之上的,它与硬件没有直接的交互。java
咱们都知道Java代码源文件,经过编译器可以产生相应的.Class字节码文件,而字节码文件又经过Java虚拟机中的解释器,编译成特定机器上的机器码。c++
① Java源文件 ——> 编译器 ——> 字节码文件 ② 字节码文件 ——> JVM ——> 机器码程序员
每种平台的解释器是不一样的,可是虚拟机是相同的,这也就是java为何可以跨平台的缘由了。当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序 启动就会存在多个虚拟机实例。 程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。算法
什么是类的加载? 类的加载是指将类的字节码文件数据读入到内存中,将其放在运行时数据区的方法区内,而后在堆区建立一个java.lang.Class对象,用来封装类在方法区内的数据结构。 类的加载的最终产品是位于堆区内中的Class对象,Class对象封装了类在方法区内的数据结构,而且向java程序员提供了访问方法区内的数据结构的接口。json
类加载器包括:浏览器
String str = new String("HelloWorld"); System.out.println(str.getClass().getClassLoader()); //控制台打印null
扩展类加载器(ExtClassLoader)服务器
——是java代码实现的,用来加载java安装目录下 jre/lib/ext 目录中的可执行jar包。数据结构
应用程序类加载器(AppClassLoader)多线程
——是java代码实现的,用来加载用户编写的代码。咱们新建一个类,获取其类加载器就是AppClassLoaderjvm
public class MyClassLoaderTest { public static void main(String[] args) { String str = new String("HelloWorld"); // 打印null System.out.println(str.getClass().getClassLoader()); // 打印sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(MyClassLoaderTest.class.getClassLoader()); // 打印sun.misc.Launcher$ExtClassLoader@4554617c System.out.println(MyClassLoaderTest.class.getClassLoader().getParent()); // 打印null System.out.println(MyClassLoaderTest.class.getClassLoader().getParent().getParent()); } }
由上述代码可见: AppClassLoader extend ExtClassLoader extend BootstrapClassLoader
为了防止用户自定义类与jdk自带的类冲突,jdk内有双亲委派机制和沙箱机制。
上述过程当中,咱们认识到了类加载器之间的继承关系。当java在加载类的时候,由AppClassLoader
委派其父类ExtClassLoader
进行加载,ExtClassLoader
会再次委派其父类BootStrapClassLoader
进行加载, 若是BootStrapClassLoader
找到该类那么加载该类返回该类的Class对象,可是,若是此时BootStrapClassLoader
没有找到该类, 那么就须要ExtClassLoader
自身进行加载,若是ExtClassLoader
找到该类那么加载该类返回该类的Class对象, 可是,若是ExtClassLoader
也没有找到该类,那么就要由AppClassLoader
进行加载。 若是最后AppClassLoader
也没有找到该类,那么就会抛出 ClassNotFoundException
。
(类加载器没有向下寻找,没有getChild只有getParent)
若是你本身定义了一个与jdk自带类名包名一致的类,那么java也不会去加载该类。
JVM内存区域主要分为
生命周期
方法区和堆是全部线程共享的内存区域;而java栈、本地方法栈和程序计数器是运行时线程私有的内存区域。
方法区 主要存放静态变量,常量,Class类模板(接口定义,构造函数),运行时常量池。
java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。又被称做为运行时数据区。
程序计数器(Program Counter Register),是一块比较小的内存空间,它的做用能够看作是当前线程所执行的字节码的行号指示器。每一个线程都有一个私有的,能够理解为它是一个指针,指向方法字节码地址,用来标记下一个要执行的方法字节码地址。
JVM栈(JVM Stacks),与程序计数器同样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同,线程结束栈内存也就释放了,对于栈来讲不存在来及回收的问题。主要保存八大基本数据类型的变量、对象的引用变量以及实例方法。虚拟机栈描述的是Java方法执行的内存模型:每一个方法被执行的时候都会同时建立一个栈帧(Stack Frame)用于存储局部变量表、操做栈、动态连接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈(Native Method Stacks),与c/c++交互的一块区域,本地方法栈(Native Method Stacks)与虚拟机栈所发挥的做用是很是类似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
Sun JDK监控和故障处理命令有 jps
、 jstat
、 jmap
、 jhat
、 jstack
、 jinfo
经常使用调优工具分为两类
设定堆内存大小 -Xmx
:堆内存最大限制。
设定新生代大小。 新生代不宜过小,不然会有大量对象涌入老年代 -XX:NewSize
:新生代大小 -XX:NewRatio
新生代和老生代占比 -XX:SurvivorRatio
:伊甸园空间和幸存者空间的占比
设定垃圾回收器 年轻代用 -XX:+UseParNewGC
年老代用-XX:+UseConcMarkSweepGC
递归操做,程序没有出口会一直进行压栈操做
栈的深度不够了
逻辑上分为
物理上分为
新生区 、 养老区、 永久区
又将新生区分为了三个区
新new的对象都放在伊甸园区,存活率2%,其余对象都被垃圾回收器回收 没有被垃圾回收幸存下来的对象将会保存到幸存者区 当伊甸园区内存不足时,会进行轻量级(minor GC)垃圾回收,将幸存者from区和伊甸园区的还在用的对象移动到幸存者to区, 而后清空幸存者from区和伊甸园区,幸存者from区清空以后会交换from区和to区,保证to区始终是空的。注意from区向to区移动以前会判断对象的年龄, 若是大于15,直接移动到养老区。年龄计数的原理:垃圾回收器回收一次,幸存活一次加一岁。 若是养老区的内存也不够用了,就会触动重量级GC(full GC)将养老区和新生区全量级回收垃圾对象。若是FullGC以后养老区的内存仍是不够用,那么会引起OOM。
若是程序一开始就new了一个比伊甸园区大的对象,伊甸园区没有足够的空间存放应该如何存放呢?此时会将对象存放到养老区,若是养老区也不够存储,那么会引起OOM。
对象优先分配在Eden区,若是Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指须要大量连续内存空间的对象)。这样作的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每一个对象定义了一个年龄计数器,若是对象通过了1次Minor GC那么对象会进入Survivor区,以后每通过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
动态判断对象的年龄。若是Survivor区中相同年龄的全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象能够直接进入老年代。 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,若是这个值大于老年区的剩余值大小则进行一次Full GC,若是小于检查HandlePromotionFailure设置,若是true则只进行Monitor GC,若是false则进行Full GC。
在java8中,永久代已经移除了,被“元数据”(元空间)的区域所取代。元空间的本质和永久代相似,元空间与永久代的最大区别在于: **元空间并不在虚拟机中,而是使用本地内存。**所以,默认状况下,元空间的大小仅受本地内存限制。 类的源数据放入本定内存中,字符串和类的静态变量放到java堆中,这样能够加载多少类的元数据就再也不由MaxPermSize控制,而由系统的实际可用空间来控制。
引用计数法 在java中,引用和对象是有关联的。若是要操做对象则必须用引用进行。所以,一个简单的方法就是经过引用计数来判断一个对象是否能够回收。简单来讲,即一个对象若是没有任何与之关联的引用,即他们的引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
可达性分析 为了解决引用计数法的循环引用问题,java使用了可达性分析的方法。经过一系列的“GC roots”对象做为起点搜索,若是在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
注意 不可达并不等价于可回收对象,不可达对象变为可回收对象至少要通过两次标记过程。两次标记后还是可回收对象,则将面临回收。
最基础的垃圾回收算法,分为两个阶段:标记和清楚。标记阶段是标记出来全部要回收的对象,清楚阶段回收被标记的对象所占的空间。
该算法的缺点: 内存碎片化严重,垃圾清理完成后,形成不少内存空间不连续。后续可能发生大对象不能找到可利用的问题。
MajorGC使用该算法
MinorGC使用该算法
缺点: 这种算法虽然实现简单,内存效率高,不易产生碎片,可是最大的问题是能够用内存被压缩到了本来的一半。且存活对象增多的话,copying算法的效率也大大下降。
6.1. 新生代与复制算法
目前大部分的JVM的GC对于新生代都采起了copying方法,由于新生代中每次垃圾回收都要回收大部分对象, 即要复制的操做比较少,但一般并非按照1:1来划分新生代。通常将新生代划分为一块较大的Eden空间和两个比较小的Surviror空间(FromSpace,ToSpace),每次使用Eden空间和其中的一块Surivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
6.2 老年代与标记复制算法 而老年代由于每次只回收少许的对象,所以采用Mark-Compact算法。
JAVA虚拟机提到过的处于方法区的永生带,它用来存储class类,常量、方法描述等。对永生代的回收主要包括废弃常量和无用的类
对象的内存分配主要在新生代的EdenSpace和SurvivorSpace的FormSpace(Survivor目前存放对象的那一块),少数状况会直接分配到老生代。
当新生代的EdenSpace和FromSpace空间不足时就会发生一次GC,进行GC后,EdenSpace和FromSpace区的存活对象会被移动到ToSpace,而后将EdenSpace和FromSpace进行清理。
若是ToSpace没法足够存储某个对象,则将这个对象存储到老生代。
进行GC后,使用的即是EdenSpace和ToSpace了,如此反复循环。
当对象在Survivor区躲过一次GC后,其年龄就会+1。默认状况下年龄达到15的对象就会移动到老生代中
在Java中最多见的就是强引用,把一个对象赋值给一个引用变量,这个引用变量就是一个强引用。 当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即便该对象之后永远都不会被用到,JVM也不会回收。所以强引用是形成Java内存泄漏主要缘由之一。
软引用须要使用SoftReference类来实现,对于只有软引用的对象来讲,当系统内存足够时他不会被回收,当系统内存足够用时,它不会被回收,当系统内存不足时它会被回收。软引用一般用在对内存敏感的程序中。
弱引用须要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来讲,只要垃圾回收机制一运行,无论JVM的内存空间足够,总会回收该对象占用的内存。
**虚引用须要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。**虚引用的主要做用是跟踪对象被垃圾回收的状态。