1 JVM运行机制概述

img JVM运行机制

  • 类加载机制: 类加载过程由类加载器来完成,即由ClassLoader及其子类实现,有隐式加载显式加载两种方式。隐式加载是指在使用new等方式创建对象时会隐式调用类加载器把对应的类加载到JVM中;显式加载是指通过直接调用Class.forName()把对应的类加载到JVM中。
  • 内存模型(运行时数据区):共享区【方法区、堆】、私有区【虚拟机栈、本地方法栈、程序计数器】、直接内存(不受JVM GC管理)。其中程序计数器是唯一不会出现OOM的内存区。
  • 执行引擎:即时编译器、垃圾收集器(按代回收算法:新生代-复制算法(Minor GC),老年代-标记整理算法(Major GC / Full GC))

2 类加载机制

类加载过程由类加载器来完成,即由ClassLoader及其子类实现,有隐式加载显式加载两种方式。隐式加载是指在使用new等方式创建对象时会隐式调用类加载器把对应的类加载到JVM中;显式加载是指通过直接调用Class.forName()把对应的类加载到JVM中。

参考链接:jvm之java类加载机制和类加载器(ClassLoader)的详解

2.1 类加载过程

  • 装载:查找和导入Class文件;

  • 链接:把类的二进制数据合并到JRE中;

    校验:检查载入Class文件数据的正确性;

    准备:给类的静态变量分配存储空间;

    解析:将符号引用转成直接引用;

  • 初始化:对类的静态变量,静态代码块执行初始化操作

2.2 类加载器

类的加载是动态的,它不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(如基类)完全加载到JVM中,至于其他类,则在需要时才加载。类分为3类:核心类扩展类自定义类。针对这3种类有3种类型的加载器:Bootstrip ClassLoaderExtension ClassLoaderApplication ClassLoader

  • 启动类加载器(Bootstrap ClassLoader):负责加载 Java 的核心类( $JAVA_HOME 中 jre/lib/rt.jar 里所有的class),由C++实现的,不是 java.lang.ClassLoader 的子类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到该类加载器的引用,所以不允许直接通过引用进行操作。
  • 扩展类加载器(Extension ClassLoader):负责加载JRE的扩展目录,lib/ext 或者由 java.ext.dirs 系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
  • 应用类加载器(Application ClassLoader):也被称为系统类加载器(System ClassLoader),负责在JVM启动时加载来自Java 命令的 -classpath 选项、java.class.path 系统属性,或者 CLASSPATH 环境变量所指定的 JAR 包和类路径。程序可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父类加载器。由Java语言实现,父类加载器为ExtClassLoader。
  • 自定义类加载器(User ClassLoader):必须继承 java.lang.ClassLoader。
public class Main {
	public static void main(String[] args) {
		ClassLoader appClassLoader=Main.class.getClassLoader(); //获取AppClassLoader
		System.out.println(appClassLoader);
		ClassLoader extClassLoader=appClassLoader.getParent(); //获取ExtClassLoader
		System.out.println(extClassLoader);
		ClassLoader bootClassLoader=extClassLoader.getParent(); //获取BootClassLoader
		System.out.println(bootClassLoader);
	}
}
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
null

双亲委派机制

img 双亲委派模型

注意:上图是委派关系,不是继承关系。

双亲委派机制工作原理:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

2.3 反射机制

反射机制能够实现在运行时对类进行装载,增加了程序的灵活性。反射机制提供的功能主要有:获取类获取父类获取接口获取成员变量获取构造器获取成员方法运行时创建对象运行时调用方法

class Demo{
	int x;
	String s;
	Demo(){}
	Demo(int x,String s){
		this.x=x;
		this.s=s;
	}
	public void show() {
		System.out.println(x+","+s);
	}
	public String show(int x,String s) {
		System.out.println(x+","+s);
		return "success";
	}
}

(1)获取 Class 类

Class c=Class.forName("Demo");
Class c=Demo.class;
Class c=demo.getClass();

(2)获取父类和接口

Class c=String.class;

Class superClass=c.getSuperclass(); //Object
Class[] interfaces=c.getInterfaces(); //Serializable, Comparable<String>, CharSequence

(3)获取类的属性、构造器、方法

Class c=Class.forName("Demo");
    		
//获取属性
Field[] fields1=c.getFields(); //获取所有public属性
Field[] fields2=c.getDeclaredFields(); //获取所有属性
Field field=c.getDeclaredField("x"); //获取指定属性
		
//获取构造器
Constructor[] constructors=c.getDeclaredConstructors(); //获取所有构造器
Constructor constructor1=c.getDeclaredConstructor(null); //获取无参构造器
Constructor constructor2=c.getDeclaredConstructor(int.class,String.class); //获取指定参数的构造器
		
//获取方法
Method[] methods1=c.getMethods(); //获取所有public方法
Method[] methods2=c.getDeclaredMethods(); //获取所有方法
Method method=c.getDeclaredMethod("show",int.class,String.class); //获取所有指定方法

(4)创建实例

Class c=Class.forName("Demo");

Demo demo1=(Demo)c.newInstance();
demo1.show();
		
Constructor constructor=c.getDeclaredConstructor(int.class,String.class); //获取指定参数的构造器
Demo demo2=(Demo) constructor.newInstance(20,"abc");
demo2.show();
0,null
20,abc

(5)调用方法

c=Class.forName("test.Demo");
Method method=c.getDeclaredMethod("show",int.class,String.class); //获取show()方法
String s=(String)method.invoke(c.newInstance(),10,"edf"); //调用show()方法
System.out.println(s); //打印show()方法返回值
10,edf
success

注意:若待调用的方法是静态方法,invoke(Object obj, Object... args) 方法的第一个参数为 null。

3 内存模型

img 内存模型

  • 方法区:用于存储被JVM加载的类信息常量静态变量、即时编译后的代码;HotSpot VM把GC分代收集扩展至方法区,使用堆的永久代实现方法区(永久代的内存回收主要针对常量池的回收类的卸载)。在jdk1.8以前,方法区也叫永久代,主要存放 Class 和 Meta(元数据),GC不会在主程序运行时对永久代进行清理,随着加载的Class的增多而膨胀,最终抛出OOM;jdk1.8中,永久代被元空间取代,元空间并不在虚拟机中,而是使用本地内存,因此,元空间的大小仅受本地内存限制。类的元数据放入本地内存中,字符串池和类的静态变量放入堆中。
  • 堆:分为新生代(Eden区:from Survivor:to Survivor=8:1:1)、老年代。新生代占堆内存1/3,由Minior GC回收;老年代占堆内存(2/3)由Major GC(或 Full GC)回收。
  • 虚拟机栈:每个方法在执行的同时都会创建一个栈帧(Stack Frame)【局部变量表、操作数栈、动态链接、方法出口】。栈帧随着方法的调用而创建,随着方法的结束而销毁,因此不需要GC。
  • 本地方法栈:虚拟机栈为Java方法服务,本地方法栈为native方法服务
  • 程序计数器:当前线程所执行的字节码的行号指示器(唯一没有OOM的区域

堆和栈的比较:

  1. 内存结构:堆空间被划分为新生代、老年代,新生代被又被划分为Eden区、Survivor区;栈空间没有这种划分,有虚拟机栈和本地方法栈两类,在方法调用时,会产生栈帧,栈帧中主要保存局部变量表、操作数栈、动态链接、方法出口。
  2. 空间大小:堆空间较大,栈空间较小。
  3. 存储信息:堆主要用于存储对象;栈主要存储对象引用、基本类型数据、方法调用。
  4. 共享性:堆是线程共享的,栈是线程私有的,每个线程有自己独立的栈空间。
  5. 运行速度:堆和栈都位于通用RAM中,但栈处理速度较快,仅次于寄存器。
  6. 垃圾回收:堆由GC回收,栈不需要GC回收;方法调用结束时,自动销毁栈帧。
  7. 抛出异常:堆满时,抛出 OutOfMemoryError 异常;栈满时,抛出 StackOverFlowError 异常。

4 垃圾回收算法

4.1 判断是否为垃圾

  • 引用计数法:引用和对象相关联,对象的引用数为0即可回收
  • 可达性分析:为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索,2次被标记为不可达的对象,即可回收。

4.2 垃圾回收算法

  • 复制算法:将内存分为大小相等的两块,每次只使用其中一块,垃圾回收时,将存活的对象复制到另一块内存中,并清空已使用的内存
  • 标记清除算法:标记需要回收的对象,并清除
  • 标记整理算法:标记存活对象,将其移相内存一端,清除端边界外的对象
  • 分代收集算法:新生代-复制算法(Minior GC)老年代-标记整理算法(Major GC / Full GC)

JDK1.6 中 Sun HotSpot 虚拟机垃圾收集器如下(其中,连线表示可以配套使用):

img 垃圾收集器

5 引用类型

  • 强引用:最常见,如:将对象赋给一个引用变量,它处于可达状态,不会被GC 回收。强引用是造成内存泄漏的主要原因之一
  • 软引用:使用SoftReference类实现,当系统内存足够时不会被回收,当系统内存不够时会被回收
  • 弱引用:使用WeakReference类实现,只要GC一运行,就会被回收
  • 虚引用:使用PhantomReference类实现,不能单独使用,必须和引用队列联合使用,用于跟踪对象被垃圾回收的状态

6 JVM参数调优

JVM参数调优主要是针对堆的参数进行调优。在Java程序界面右击,依次选择【Run As】 ->【Run Configurations】->【Arguments】->【VM arguments】,然后填入要传入的调优参数,如“-Xms512m -Xmx1024m -XX:+PrintGCDetails”,再点击【Run】即可。如下图所示:

img JVM参数调优步骤

程序输出结果如下:

Heap
 PSYoungGen      total 153088K, used 7895K [0x00000000eab00000, 0x00000000f5580000, 0x0000000100000000)
  eden space 131584K, 6% used [0x00000000eab00000,0x00000000eb2b5dc8,0x00000000f2b80000)
  from space 21504K,  0% used [0x00000000f4080000,0x00000000f4080000,0x00000000f5580000)
  to   space 21504K,  0% used [0x00000000f2b80000,0x00000000f2b80000,0x00000000f4080000)
 ParOldGen       total 349696K, used 0K [0x00000000c0000000, 0x00000000d5580000, 0x00000000eab00000)
  object space 349696K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000d5580000)
  Metaspace      used 2670K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 294K,  capacity 386K,  committed 512K,  reserved 1048576K

堆设置

  • -Xms:初始堆大小,默认为物理内存的1/64
  • -Xmx:最大堆大小,默认为物理内存的1/4
  • -XX:NewSize=n:设置新生代大小
  • -XX:NewRatio=n:设置新生代和老年代的比值。默认为2,表示新生代:老年代=1:2,新生代占堆内存的1/3
  • -XX:SurvivorRatio=n:年轻代中Eden区与一个Survivor区的比值。默认为8,表示Eden:Survivor=8:2(Survivor区有2个)
  • -XX:PermSize=n:设置永久代初始空间
  • -XX:MaxPermSize=n:设置永久代最大空间

收集器设置

  • -XX:+UseSerialGC:设置串行收集器
  • -XX:+UseParallelGC:设置并行收集器
  • -XX:+UseParalledlOldGC:设置并行年老代收集器
  • -XX:+UseConcMarkSweepGC:设置并发收集器

垃圾回收统计信息

  • -XX:+PrintGC:输出GC处理日志
  • -XX:+PrintGCDetails:输出详细的GC处理日志
  • -XX:+PrintGCTimeStamps
  • -Xloggc:filename

并行收集器设置

  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的收集线程数
  • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
  • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

  • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
  • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

​ 声明:本文转自JVM详解