JVM
套用文章
https://www.cnblogs.com/zyrblog/p/9941723.html
https://www.cnblogs.com/zhangpan1244/p/6197832.html
https://www.jianshu.com/p/3ffc6a8c012e
https://blog.csdn.net/qq_41701956/article/details/80020103
JRE分为JVM与API,JRE为java的运行环境,JVM为JAVA虚拟机。为跨平台特性支撑。
JVM生命周期
**1、启动:**启动一个java程序时,一个JVM实例就产生了,任何一个拥有main方法的类都可以作为JVM实例运行的起点。
2、运行: main方法作为该程序初始线程的起点,任何其他的线程均由该线程启动。
**3、消亡:**当程序中的所有非守护线程都终止时,jvm才退出。
一个运行中的虚拟机有着清晰的任务:执行java程序。程序开始执行时才运行,程序结束时就停止。在同一台机器上运行三个程序,就会有三个运行中的java虚拟机。java虚拟机总是开始于一个main方法,且这个方法必须是**公有**、**返回void**、**只接受一个字符串数组**的。在程序执行时,必须指明main方法的类名。main方法是程序的起点,其执行的线程初始化为初始线程,程序中其他的线程都有其启动。
Java中的线程分为两种:守护线程(daemon)和普通线程(non-daemon)。任何线程都可以是守护线程(daemon)和普通线程(non-daemon)。它们几乎在每个方面都是相同的,唯一的区别在于虚拟机何时离开。
**普通线程:**虚拟机在它所有非守护线程已经离开后自动离开。
**守护线程:**是java虚拟机自己的线程,庇护负责垃圾回收的线程就是一个守护线程。只要java虚拟机中还有普通线程在执行,虚拟机就不会停止。如果有足够的权限,可以调用exit方法终止程序。
JVM内存区域划分
类装载器子系统
每一个Java虚拟机都有一个类装载器子系统,JVM的两种类装载器包括:启动类装载器和用户自定义类装载器,启动类装载器是JVM实现的一部分,用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类。负责加载程序中的类型(类和接口),并赋予唯一的名字。
执行引擎
每一个Java虚拟机都有一个执行引擎,负责执行被加载类中包含的指令;或者在执行字节码,或者在执行本地方法。
而其主要的执行技术有:解释(第一代JVM)、即时编译(第二代JVM)、自适应优化、芯片级直接执行。
**自适应优化:**接合第一代与第二代JVM的经验,采用二者合一的方式。开始对所有的代码进行解释执行的方式,并监视代码执行的情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
运行时数据区
1、新生代:新建的对象都由新生代分配内存。常常又被划分为Eden区和Survivor区。Eden空间不足时会把存活的对象转移到Survivor。新生代的大小可由-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。
2、旧生代:存放经过多次垃圾回收仍然存活的对象。
3、持久代:存放静态文件,如Java类、方法等;持久代在方法区,对垃圾回收没有显著影响。
线程共享
方法区(Method Area):
线程间共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据;OutOfMemoryError异常:当方法区无法满足内存的分配需求时。JVM用在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。来存放方法区。
**运行时常量池:**方法区的一部分,用于存放编译期生成的各种字面量与符号引用,如String类型常量就存放在常量池;OutOfMemoryError异常:当常量池无法再申请到内存时。
方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
堆(Heap):
被所有线程共享的一块内存区域,在虚拟机启动时创建用来存储对象实例,可以通过-Xmx和-Xms控制堆的大小;
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
OutOfMemoryError异常:当在堆中没有内存完成实例分配,且堆也无法再扩展时。java堆是垃圾收集器管理的主要区域。java堆还可以细分为:**新生代(New/Young)、**年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
旧生代/年老代(Old/Tenured)。年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
**持久代(Permanent):**在方法区,不属于Heap。用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。
堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。
Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
线程私有
虚拟机栈(JavaStack):
帧的组成:局部变量区(包括方法参数和局部变量,对于instance方法,首先还要保存this类型,其中方法参数按照声明顺序严格放置,局部变量可以任意放置),操作数栈,帧数据区(用来帮助支持常量池的解析,正常方法返回和异常处理)。)
虚拟机只会直接对Java Stack执行两种操作:以帧为单位压栈和出栈。每一个帧代表一个方法,java方法有两种返回方式,return和抛出异常,两种方式都会导致该方法对应的帧出栈和释放内存。
线程私有,生命周期与线程相同;存储方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。Java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,每一个方法被调用直至执行完成的过程,就对应着一个栈桢在虚拟机栈中从入栈到出栈的过程。
1、StackOverflowError异常:当线程请求的栈深度大于虚拟机所允许的深度
2、OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存
本地方法栈(Nativemethodstack):
与虚拟机栈相似,主要为虚拟机使用到的Native方法服务,在hotSpot虚拟机中直接把本地方法栈与虚拟机栈二合一。用于支持native方法的执行,存储了每个native方法调用的状态。保护native方法进入区域的地址。对于本地方法接口,实现JVM并不要求一定有他的支持,甚至可以完全没有。
区别于Java虚拟机栈的是,Java虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。也会有StackOverflowError异常和OutOfMemoryError异常。
程序计数器(ProgramCounter):
(不会出现OutOfMemoryError情况)
当前线程所执行的字节码的行号指示器。程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
如果线程正在执行的是Java方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址;
如果正在执行的是Native方法,则这个计数器值为空;
程序计数器记录的字节码指令地址,但是native本地方法大多是通过C实现并未编译成需要执行的字节码指令所以在计数器值当然为空。
Java代码的编译和执行过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pdu0ccG7-1604133922590)(C:\Users\nen\AppData\Roaming\Typora\typora-user-images\image-20201030173122907.png)]
其包括了三个重要机制
1、java源码编译机制(.java源代码文件->.class字节码文件)
2、类加载机制(classLoader)
3、类执行机制(JVM执行引擎)
java源码编译机制
java源代码是不能被机器是别的,需要先经过编译器编译成JVM可以执行的.class字节码文件,再由解释器解释运行。其过程可以理解为 .java文件通过java编译器生成.class文件在通过解释器的解释进行执行。
类加载机制(classLoader)
***java程序由多个独立的类文件组成。这些类文件并非一次性全部装入内存,而是依据程序逐步载入。***JVM的类加载是通过classLoader及其子类来完成的,类的层次关系和加载顺序可以如图所述:
1、Bootstrap Class Loader
jvm的根classLoader由C++实现
加载java的核心api:$JAVA_HOME中jre/lib/rt.jar中所有的class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。
jvm启动时及初始化此classloader
2、ExtensionClass Loader
加载java扩展api(lib/ext中的类)
3、AppClassLoader
加载classpath目录下定义的class
4、CustomClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomacat、jboss都会根据j2EE规范自行实现class Loader
1)双亲委派机制
jvm在加载类时默认采用的是双亲委派机制。就是某个特定的类加载器在接到加载类的请求时、首先将加载任务委托给父类加载器,依次递归。如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成加载任务时,才会由自己加载。
作用:避免重复加载,更加安全,因为如若不是双亲委派,而是自己编写的java.long.object的类,那就更加无法保证object的唯一性。所以使用双亲委派,即使自己编写了,但永远无法被加载。
2)破化双亲委派机制
双亲委派机制并不是一种强制性的约束模型。
JVM垃圾回收(GC)
原理:将内存中不再被引用的对象(也就是垃圾)进行回收,GC中用于回收的方法称为收集器。由于GC需要消耗一些资源和时间,java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
1、新生代的对象收集成为minor GC
2、对旧生代的对象收集称为Full GC
3、程序中主动调用System.gc()的GC为Full GC
java的垃圾回收是由后台线程GC执行的,自动运行无需显示调用。且即使主动调用了System.gc(),也只是会提醒系统进行垃圾回收,但系统不一定会回应。
JVM引用类型:
不同的对象引用类型,GC会采用不同的方法进行回收。
1、强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC才会被回收)
2、软引用:java提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
3、弱引用:在GC时一定会被回收
4、虚引用:用来得知对象是否被GC
如何判断一块内存空间是否符合回收标准:
1、对象赋予了空值,且之后再为被调用(obj = null)
2、对象赋予了新值,即重新分配了空间(obj = new obj();)
内存泄漏:
程序中保留着对永远不再使用的对象的引用。因此这些对象不会被GC回收,却一直占用着内存空间。也就是**1)、对象是可达的;2)、对象是无用的。**满足这两个条件即可判定为内存泄漏。
可达:创建一个对象,系统中存在引用指向该对象
**内存泄漏的原因:**1)全局集合;2)缓存;3)ClassLoader
内存调优:
**目的:**减少GC的频率尤其是Full GC的次数,过多的GC会占用许多系统资源影响吞吐量。特别要关注FUll GC,因为会对整个堆进行整理。
**手段:**JVM调优通过配置JVM的参数来提高垃圾回收的速度,合理分配堆内存各部分的比例。
1、旧生代空间不足
2、调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象
3、持久代(Pemanet Generation)空间不足
4、增大Perm Gen空间,避免太多静态对象
5、统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间
6、控制好新生代和旧生代的比例
7、System.gc()被显示调用
8、垃圾回收不要手动触发,尽量依靠JVM自身的机制