JVM虚拟机

1:JVM生命周期

1:启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
2:运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
3:消失。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。

Java中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。当然,你也可以把自己的程序设置为守护线程。包含main()方法的初始线程不是守护线程。

只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。

2. JVM体系结构

1类装载器(ClassLoader)(用来装载.class文件)
2 执行引擎(执行字节码,或者执行本地方法)
3 运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)
JVM虚拟机

3. JVM运行时数据区

Java对象实例存放在堆中;常量存放在方法区的常量池;虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据放在方法区;方法区和堆是线程共享的。栈是线程私有的,存放该方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。除了程序技术器不会发生内存溢出,其它都会发生内存溢出。当在堆中没有内存完成实例分配,且堆也无法再扩展时。当方法区无法满足内存的分配需求时,OutOfMemoryError异常
一个Java程序对应一个JVM,一个方法(线程)对应一个Java栈。

JVM虚拟机

程序计数器(Program Counter Register)
当前线程所执行的字节码的行号指示器(代码控制,多线程切换,生命周期)

java虚拟机栈(VM Stack)
栈帧组成。栈帧包含方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈(Native Method Stack)
非java语言实现的

用来存储对象实例和数组。可以通过-Xmx和-Xms控制堆的大小。
java堆还可以细分为:新生代(New/Young)、旧生代/年老代(Old/Tenured)。持久代(Permanent)在方法区,不属于Heap。jdk1.8将方法区移到堆外,叫做元空间。被加载的类作为元数据加载到底层操作系统的本地内存区。
  java堆是垃圾收集器管理的主要区域。
方法区(堆内)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

4:JVM垃圾回收

GC的基本原理:将内存中不再被引用的对象进行回收,GC中用于回收的方法称为收集器。垃圾:不再被引用的对象。

对新生代的对象的收集称为minor GC;
对旧生代的对象的收集称为Full GC;
程序中主动调用System.gc()的GC为Full GC

如何判断可回收对象
1、引用计数算法
每当一个地方引用它时,计数器+1;引用失效时,计数器-1;计数值=0——不可能再被引用。

2、可达性分析算法:
向图,树图,把一系列“GC Roots”作为起始点,从节点向下搜索,路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即不可达时,则证明此对象时不可用的。

注:在Java中可作为GCRoots的对象:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象;
2)方法区中类静态属性引用的对象;
3)方法区中常量引用的对象;
4)本地方法栈中JNI引用的对象。

内存泄漏

内存泄露的原因:1)全局集合;2)缓存;3)ClassLoader
检查List,map等集合对象是否有使用完后,未清除的问题。List、Map等集合对象会始终存有对对象的引用,使得对象不能被GC回收
程序中保留着对永远不再使用的对象的引用。因此这些对象不会被GC回收,却一直占用内存空间却毫无用处。即:1)对象是可达的;2)对象是无用的。满足这两个条件即可判定为内存泄漏。
内存溢出
检查代码有否有死循环或者递归调用
检查是否有大循环重复产生新对象实体
检查数据库查询中,是否有一次获取全部数据的查询。一般来说,如果一次取十万条记录,就可能引起内存溢出。

垃圾回收算法
分代收集算法中堆空间被分为新生代和老年代。因为新生代中对象的存活率比较低,所以一般采用复制算法,老年代的存活率一般比较高,一般使用”标记-清理”或者”标记-整理”算法进行回收。

最后来讲一下垃圾回收流程。
1、新建的对象,大部分存储在Eden中
2、当Eden内存不够,就进行Minor GC释放掉不活跃对象;然后将部分活跃对象复制到Survivor中(如Survivor1),同时清空Eden区
3、当Eden区再次满了,将Survivor1中不能清空的对象存放到另一个Survivor中(如Survivor2),同时将Eden区中的不能清空的对象,复制到Survivor1,同时清空Eden区
4、重复多次(默认15次):Survivor中没有被清理的对象就会复制到老年区(Old)
5、当Old达到一定比例,则会触发Major GC释放老年代
6、当Old区满了,则触发一个一次完整的垃圾回收(Full GC)
7、如果内存还是不够,JVM会抛出内存不足,发生oom,内存泄漏。

5. 内存调优

调优目的:减少GC的频率尤其是Full GC的次数,过多的GC会占用很多系统资源影响吞吐量。特别要关注Full GC,因为它会对整个堆进行整理。

主要手段:JVM调优主要通过配置JVM的参数来提高垃圾回收的速度,合理分配堆内存各部分的比例。

导致Full GC的几种情况和调优策略:

旧生代空间不足
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间不要创建过大的对象及数组避免直接在旧生代创建对象
持久代(Pemanet Generation)空间不足
增大Perm Gen空间,避免太多静态对象
统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间
控制好新生代和旧生代的比例
System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制

堆内存比例不良设置会导致什么后果:

1)新生代设置过小

一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC

2)新生代设置过大

一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加

一般说来新生代占整个堆1/3比较合适

3)Survivor设置过小

导致对象从eden直接到达旧生代,降低了在新生代的存活时间

4)Survivor设置过大

导致eden过小,增加了GC频率

另外,通过**-XX:MaxTenuringThreshold=n来控制新生代存活时间**,尽量让对象在新生代被回收

JVM提供两种较为简单的GC策略的设置方式:

1)吞吐量优先

JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置

2)暂停时间优先

JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置

JVM常见配置

堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小

垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

参考:https://blog.****.net/hequan199411/article/details/81915884