Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java 最主要的特性之一。
Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行,它的存在也是Java可以跨平台的关键。
深入了解JVM有助于提高平台期解决问题的能力,优化并提高系统性能。
Java编译器
JVM可以运行字节码文件,因为它有一个编译器,可以把Java文件编译成一个字节码文件.(java源代码通过java的编译器生成.class文件)
Java的编译器分为两种: 静态编译器和动态编译器.
静态编译器: 通过javac的方式,可以把Java源代码作为输入,并将其编译成字节码文件.
动态编译器: 通过JIT的方式,动态的将一种编程语言编译成另一种语言,生成更好更有效的序列指令.
JVM是一种规范,基于这种规范不同公司就做了具体的实现,主流JVM产品:
Oracle公司收购了BEA公司和Sun公司, 就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM .
Oracle公司宣布JDK8会完成这两款虚拟机的整合工作, 使之优势互补.
HotSpot(热点代码探测技术)是Sun JDK和Open JDK中所带的默认虚拟机, 也是目前使用范围最广的Java虚拟机 .
不管是现在仍在广泛使用的JDK8,还是使用比例较多的JDK版本中,默认的虚拟机都是HotSpot. 从服务器、桌面到移动端、嵌入式都有应用.
在HotSpot中,将class文件翻译成机器码执行时提供了两种方式,分别是:找出热点代码进行编译执行 和 全部代码次次解释执行.
通常情况两种方式是会同时存在的
其中:
当一个程序启动,伴随的就是一个jvm实例的诞生,当这个程序关闭退出,这个jvm实例就随之消亡。
类从被加载到虚拟机,内存开始到卸载出内存为止,整个声明周期的阶段包括7个阶段:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)。其中验证、准备、解析3个部分统称为链接(Linking)。
类加载:指的是类的生命周期中加载、连接、初始化三个阶段。就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口.
加载:类的加载方式比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取.
连接: 连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析.
验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格
式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行.
准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值.
jvm默认的初值是这样的:
1. 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0.
2. 引用类型的默认值为null.
3. 常量的默认值为我们程序中设定的值.
解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。 例如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,通过内存地址就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而内存地址就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址.
连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化.
初始化:如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有以下四种情况:
1. 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法
2. 通过反射方式执行以上三种行为
3. 初始化子类的时候,会触发父类的初始化
4. 作为程序入口直接运行时(也就是直接调用main方法).
除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化.
另外,初始化有三个阶段:
1. 操作系统级别的初始化:
确保只有一个JVM启动.
初始化一些模块和系统属性.
2. JVM级别的初始化:
初始化一些命令行选项.
初始化垃圾收集器,栈,本地存储,日至流记录器等.
初始化lib库,线程库等.
创建main线程,并与系统线程相关联.
3. Java级别的初始化:
初始化Java级别的线程.
初始化类加载器,编译器,解释器,处理器.
初始化并加载Java类和系统类等.
卸载:执行shutdown,停止全部组建,全部线程,停止全部IO,关闭JVM.
Java能够发展到如今,很大程度上取决于JVM(Java虚拟机),而内存管理又是JVM中的一个重要命题。
JVM是如何管理各种资源的,这就需要有一套优秀的管理方案,能够了解JVM的内存结构,也可以让你编写高性能的应用程序。
JVM系统的三大主要部分: 类加载系统,运行时数据区,执行引擎
类加载系统: ClassLoader System,负责把类加载进内存
运行时数据区: Runtime Data Area, 负责存储数据信息
执行引擎: Execute Engine,负责调用对象执行业务
分为两部分: 线程共享的内存区域 和线程独占的内存区域,
线程共享的内存区域 包括堆,方法区。
线程独占的内存区域 包括虚拟机栈,本地方法栈,程序计数器。
也就是说,当你在写程序时,需要判断当前数据读写的是存在于哪类内存区域。
如果存在的是线程共享的内存区域,那么就要考虑是否存在线程安全问题,
如果存在线程独占的内存区域,那么就可以打消这种顾虑。
堆内存被分为两部分: 年轻代(young)和老年代(old).
年轻代分为伊甸园区(eden)和幸存区(survivor),如上图幸存区又被分为两个区域(from和to).
一般来讲新创建的对象,都会被分配在年轻代,并放在伊甸园区(eden). 如果伊甸园区内存不足时会启动GC回收,还是没有被回收的就会放入幸存区的from区,后面GC时可能也会把from里的内容回收一部分,from没有被回收的就会放进to里并被记录这个对象的年龄,如果多次未被回收的对象(年龄大的)就会被直接放入老年代了.所以老年代存的一般都是GC多次都没被回收的对象.当然,如果对象比较大伊甸园区内存不足的话也可以被直接分配到老年代(tenured).
堆区 :
1.虚拟机启动时创建,用来存储对象实例,被所有线程共享.
2.堆内存heap分为新生代(包括伊甸园eden区和幸存区survivor)和老年代(tenured)
3.也是垃圾回收器(GC)主要的工作区域
4.此时,如果你对堆内存又了一定了解,想做一些堆内存的优化的话,你需要调整以下参数来调整堆内存的大小:
-Xmx设置堆的最大空间大小。
-Xms设置堆的最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:NewRatio 新生代和老年代的比值,值为4 则表示新生代:比老年代1:4
-XX:SurivorRatio 表示Survivor和eden的比值,值为8表示两个survivor:eden=2:8.
-Xss:设置每个线程的堆栈大小(值越大那么容纳的线程数就越少能处理的吞吐量就越少)。
方法区:
1.存储已被jvm加载了的数据(类的信息,常量,静态变量)
2.存储运行时常量池(加载类文件的常量池,动态生成的常量)
3.随着jdk版本的不同,实现方式也会有所改变
4.如果你想优化方法区那么可以参考调整以下参数:
-XX:MetaspaceSize 设置元数据区最小空间。 (JDK8)
-XX:MaxMetaspaceSize 设置元数据区最大空间。(JDK8)
程序计数器 :
1. 线程启动时创建,并设置为线程私有
2. 用于记录当前正在执行的虚拟机字节码指令的地址
3. Java虚拟机规范里唯一一个没有内存溢出的区域,只做计算
栈:
1.也叫栈帧stack,有一个先进后出的特点,分为栈顶和栈底,进栈的过程叫进栈/压栈,出栈的过程叫出栈/弹栈
2.用于存储栈帧(stack frame)对象,保存方法里的局部变量,操作数栈,执行运行时常量池的引用等
3.调用方法时会创建一个新的栈帧,方法执行完毕,栈帧出栈
4.出现多次方法的递归调用时会出现栈内存溢出
5. 虚拟机栈帧参数调整:
-Xss128k: 设置每个线程的堆栈大小,原来是256K,JDK5后默认1M
本地方法栈:
1.存放虚拟机使用到的native方法
2.用于存储本地方法执行时的一些变量信息,操作数信息,结果信息等
当程序执行时出现了一些内存溢出的现象,如果你了解这个话题,那么可以合理调整JVM参数,从而达到延迟报错或者避免报错的发生.
三种参数类型
XX参数数值类型
当代码在运行了一段时间以后会出现堆内存溢出( java.lang.OutOfMemoryError : Java heap space),可通过调整堆内存大小,延迟内存溢出的时间或者避免内存溢出现象。
-Xmx20m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=e:/a.dump
-Xmx20m 堆内存最大容量
-Xms10m 堆内存最小容量
-XX:+HeapDumpOnOutOfMemoryError 设置如果出现指定的内存溢出现象
-XX:HeapDumpPath=e:/a.dump 设置报错信息的导出的磁盘路径
测试实例:
当在有限的元数据内存区不断的加载新的类时会导致元数据区空间不足从而出现内存溢出现象(java.lang.OutOfMemoryError: Metaspace),例如:
-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
-XX:MetaspaceSize=10M 元数据区最小容量
-XX:MaxMetaspaceSize=10M 元数据区最大容量
大部分新创建的对象会分配在年轻代内存,进行参数调整查看不同内存大小何时GC的次数和耗时
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xmx1G -Xms1G -Xmn100M
-XX:+PrintGCDetails #打印GC的详细信息
-XX:+PrintGCTimeStamps #打印GC的时间戳
-Xmx1G #最大堆内存
-Xms1G #最小堆内存
-Xmn500M #最大年轻代内存(包括伊甸园区和幸存区)
测试实例:
调整参数,调成新生代最大4M时,暂观察GC情况
-XX:+TraceClassLoading 用来观察类加载的顺序过程等
常用的X参数
-Xms: 最小堆内存(初始值),如: -Xms512M
-Xmx: 最大堆内存,如: -Xmx512M
-Xmn: 新生代大小,如: -Xmx256M
-Xss: 指定线程栈大小,如: -Xmx128K
常用的XX参数
-XX:NewSize:设置年轻代最小空间大小,如-XX:NewSize=256M
-XX:MaxNewSize:设置年轻代最大空间大小,如-XX:MaxNewSize=256M
-XX:PermSize:设置永久代最小空间大小
-XX:MaxPermSize:设置永久代最大空间大小
-XX:NewRatio:设置年轻代和老年代的比值。默认值-XX:NewRatio=2,表示年轻代与老年代比值为1:2,年轻代占整个堆大小的1/3
-XX:SurvivorRatio:设置年轻代中Eden区Survivor区的容量比值。默认值-XX:SurvivorRatio=8,表示Eden : Survivor0 : Survivor1 = 8 : 1 : 1
-XX:+HeapDumpOnOutOfMemoryError:表示当JVM发生OOM时,自动生成DUMP文件。
-XX:HeapDumpPath=/usr/local/dump:dump文件路径或者名称。
-XX:+PrintGCDetails 打印GC详情
友情提示: 你可以在命令行窗口中,尝试执行java -version / java -help / java -X 其中,你会发现一些jvm参数的含义…
用于查看有权访问的hotspot虚拟机的进程,当未指定hostid时,默认查看本机jvm进程
jps -l 输出main的类名
jps -v 输出传入JVM的参数
用于打印指定Java进程的对象内存映射或堆内存细节
jmap -heap 1844 进程的堆信息,如果查询时抛出了异常就需要修改运行时jre的版本和jdk的版本一致即可
C:\Users\WuQiong>jmap -heap 1844
Attaching to process ID 1844, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.45-b02
using thread-local object allocation.
Parallel GC with 4 thread(s)
Heap Configuration: MinHeapFreeRatio = 0
#GC后如果发现空闲堆内存小于整个预估堆内存的N%(百分比), 则JVM会增大堆内存,但不能超过-Xmx指定的最大堆内存限制
MaxHeapFreeRatio = 100
#最大空闲堆内存比例,GC后如果发现空闲堆内存大于整个预估堆内存的N%(百分比),JVM则会收缩堆内存,但不能小于最小堆的限制
MaxHeapSize = 2118123520 (2020.0MB) #即-Xmx, 堆内存大小的上限
NewSize = 44564480 (42.5MB) #新生代预估堆内存占用的默认值
MaxNewSize = 705691648 (673.0MB) #新生代占整个堆内存的最大值
OldSize = 89653248 (85.5MB) #老年代的默认大小
NewRatio = 2 #老年代对比新生代的空间大小, 比如2代表老年代空间是新生代的两倍大小
SurvivorRatio = 8 #Eden/Survivor的值. 例如8表示Survivor:Eden=1:8, 因为survivor区有2个, 所以Eden的占比为8/10.
MetaspaceSize = 10485760 (10.0MB)
#类元数据空间的初始大小(Oracle逻辑存储上的初始高水位,the initial high-water-mark ). 此值为估计值. MetaspaceSize设置得过大会延长垃圾回收时间. 垃圾回收过后, 引起下一次垃圾回收的类元数据空间的大小可能会变大
CompressedClassSpaceSize = 1073741824 (1024.0MB) #类指针压缩空间大小, 默认为1G.
MaxMetaspaceSize = 10485760 (10.0MB)#是分配给类元数据空间的最大值, 超过此值就会触发Full GC. 此值仅受限于系统内存的大小, JVM会动态地改变此值
G1HeapRegionSize = 0 (0.0MB) #G1区块的大小, 取值为1M至32M. 其取值是要根据最小Heap大小划分出2048个区块.
Heap Usage: PS Young Generation
Eden Space:
capacity = 439353344 (419.0MB)
used = 114234848 (108.94284057617188MB)
free = 325118496 (310.0571594238281MB)
26.00067794180713% used
From Space:
capacity = 524288 (0.5MB)
used = 0 (0.0MB)
free = 524288 (0.5MB)
0.0% used
To Space:
capacity = 524288 (0.5MB)
used = 0 (0.0MB)
free = 524288 (0.5MB)
0.0% used
PS Old Generation
capacity = 89653248 (85.5MB)
used = 1204736 (1.14892578125MB)
free = 88448512 (84.35107421875MB)
1.3437728435672514% used
3048 interned Strings occupying 251608 bytes.
用于生成java虚拟机当前时刻的线程快照,主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待
jstack -l 22345 查询指定线程的停顿原因,可以检测出死锁问题,定位到具体行号
jconsole命令 :用图形化界面展示线程的内存情况
C:\Users\Administrator>jconsole
#直接输入命令后,选择你想要查看的程序--点击连接--点击不安全连接 即可查看
另外,JVM可视化工具市面上有一些,MAT/VisualVM,他们集成了一些JVM命令行的可视化界面的工具,分析能力强,也可以方便快捷的查看Java程序的相关内存信息.,效果同上.
Class 文件需要加载到Java虚拟机中后才能运行和使用,那么,这些 Class 文件被Java虚拟机加载的过程就是所谓的类加载的过程.
如果你还不知道你的类是何时被加载的,类中的成员何时被加载的初始化的和何时被使用的等等,那么你就该认真了解以下内容,方便以后的系统调优,减少类的加载过程来提高应用的性能.
从我们开始使用类,到类被使用完卸载类的过程,就是类的生命周期.整个周期主要有三步: 加载 -> 连接 -> 初始化,
连接过程又可分为三步: 验证 -> 准备 -> 解析, 所以一共分为5大阶段分别是: 加载 -> 验证 -> 准备 -> 解析 -> 初始化.
通过以上5大阶段就可以把类从磁盘或网络读到JVM内存,然后交给执行引擎执行并使用了.
如果说还有其他过程,那就是,使用阶段和卸载阶段了.
我们知道类的加载过程中可分为加载、验证、准备、解析、初始化 5大阶段,那他们的执行顺序是什么?
JVM规范中是这样说的:
通过类加载的机制,可以把JDK类库里定义的类,第三方类库的类,和你自己定义的类,全都加载进JVM内存中.但是,这些类是用什么方式进行加载的呢? 你可以配置JVM参数 -XX:+TraceClassLoading 来查看.
隐式加载
连接阶段主要分为三部分: 验证 准备 解析.
主要就是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全.主要完成4个检验:
准备阶段是正式为 类变量分配内存并设置类变量默认值的阶段,这些内存都将在方法区中分配.
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,其中:
类加载的最后一个阶段,其他阶段都是JVM来操作主导的,在这个阶段我们可以让自己定义的类加载器参与进来.
所谓类的初始化阶段,主要是 初始化类变量,初始化类的静态资源(静态域)等 ,类变量初始化值有两种方式:
Java程序中,对类的使用方式可以分为两种:
主动使用:会执行加载、连接、并初始化静态域
被动使用:只执行加载、连接,不初始化类静态域
类的主动引用
类的被动引用(不会发生类的初始化)
5. 当访问一个静态域时,只有真正声明这个域的类才会被初始化
6. 当通过子类引用父类的静态变量时,不会导致子类初始化
7. 通过数组定义类引用,不会导致类的初始化
8. 通过static final修饰的常量, 不会导致类的初始化
9. 使用类加载器时,也不会导致类的初始化
思考: 如果 通过子类引用父类的静态字段,此时,自雷会被初始化吗?
答案: 属于子类的被动使用,不会导致子类初始化
public class jvm {
public static void main(String[] args) throws Exception {
//类的主动调用: new/反射/调用类成员或方法
// new Son();//输出啥呢?父类static{}被加载,子类static{}被加载
// Class.forName("cn.tedu.test.Son");//输出啥呢?父类static{}被加载,子类static{}被加载
// System.out.println(Son.s_static_field);//输出啥呢?父类static{}被加载,子类static{}被加载,857
// Son.s_static_method();//输出啥呢?父类static{}被加载,子类static{}被加载
//类的被动调用: 访问静态域(谁声明的才初始化谁)
// System.out.println(Son.f_static_field);//输出啥呢?父类static{}被加载 ,666
// Son.f_static_method();//输出啥呢?父类static{}被加载 ,方法被执行
Son[] sons = new Son[3];//输出啥呢?啥也木有,都是被动加载的
}
}
class Father {
static int f_static_field = 666;
static {
System.out.println("父类static{}被加载");
}
static void f_static_method(){
System.out.println("f_static_method()");
}
}
class Son extends Father {
static {
System.out.println("子类static{}被加载");
}
static int s_static_field = 857;
public static void s_static_method() { }
public static final void s_static_final_method() { }
}
类加载器是指一个对象,它在类运行时把class文件读到Java虚拟机里.其类型为ClassLoader类型,此类型为抽象类型,通常以继承形式出现.
类加载器对象常用方法说明:
Java中,类加载器大致可分为两种: 一类是系统提供的,一类是自己定义的.
系统提供的有四种:
JVM自带的类加载器,只能加载默认classpath下的类,如果我们需要加载应用程序路径之外的类文件 或者 是网络中的一些文件呢?这就要使用自定义加载器了.
因为自定义类加载器可以自己指定类的路径,也更加个性化,所以框架等等一些技术中都有自定义的类加载器.
将来还可以实现动态读取指定目录下的新类,来更新原来类的内容,这个过程就是系统在线升级技术也叫热替换.
准备源文件
MyClassLoader类
核心类,核心功能就是,可以读取用户自己指定的路径下的类文件,并变成Class对象,方便后续反射操作
package cn.tedu.test;
import java.io.*;
//自定义类加载器:
//1. 继承ClassLoader
//2. 重写findClass(),加载自定义路径下的类文件
public class MyClassLoader extends ClassLoader{
//指定要加载的文件的基础路径
private String baseDir;//比如:E:\workspace
public MyClassLoader(String baseDir) {
this.baseDir=baseDir;
}
/**
* 读取指定路径下的类文件,并变成Class对象(你现在只有class文件,只能通过Class反射操作了)
* param: name要加载的类的全路径(包名.类名)
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = baseDir + name.replace('.', File.separatorChar) + ".class";
System.out.println("fileName=" + fileName);//文件的完整磁盘路径
try(
InputStream ins= new FileInputStream(fileName);//读取流
ByteArrayOutputStream baos = new ByteArrayOutputStream();//写出流
) {
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];//缓冲数组
int length = 0;
while ((length = ins.read(buffer)) != -1) {//每次读1024字节
baos.write(buffer, 0, length); //把读到的写出去
}
byte[] datas = baos.toByteArray(); //把数据存入字节数组中
System.out.println("操作成功!");
//使用父类方法,把数组里的数据转成Class对象
return super.defineClass(name, datas, 0, datas.length);
} catch (IOException e) {
System.out.println("操作异常!");
}
return null;
}
}
目的是测测这个类,能不能成功使用自定义类加载器
HelloClassLoader类
package cn.tedu.test;
//TODO: 去你的工作空间里,找到这个类的字节码文件HelloClassLoader.class,拷贝到其他路径去
// 注意:目录新路径的结构要和现在的结构一致(cn.tedu.test)
public class HelloClassLoader {
static {
System.out.println("I'm cn.tedu.test.HelloClassLoader static{}");
}
public HelloClassLoader() {
System.out.println("I'm cn.tedu.test.HelloClassLoader constructor()");
}
}
测试自定义类加载器的使用
TestMyClassLoader类
package cn.tedu.test;
//测试加载指定的类时,使用的是自定义的类加载器还是JVM提供的.
public class TestMyClassLoader {
public static void main(String[] args) throws Exception, InstantiationException {
//指定一个基础路径,注意,这个路径,不能在当前路径下,否则,当前路径下有这个文件时,就会使用JVM默认的类加载器AppClassLoader了..
MyClassLoader my = new MyClassLoader("E:\\testworkspace\\");//自定义的新路径
//拼接类路径(包名.类名)
Class<?> myclass = my.findClass("cn.tedu.test.HelloClassLoader");//要和复制时的路径保持一致
Object instance = myclass.newInstance();
//查看当前实例的class对象HelloClassLoader
System.out.println(instance.getClass());
//查看当前实例用的哪个类加载器?MyClassLoader
System.out.println(instance.getClass().getClassLoader());
// 问题:自定义类加载器成功了,那你还会用默认的类加载器吗?
// new HelloClassLoader().getClass().getClassLoader()
// 你会发现这样写也可以成功加载类并执行,此时使用的类加载器呢?是默认的AppClassLoader
}
}
URLClassLoader已经继承了ClassLoader,可以从指定目录,jar包,网络中加载指定的类资源。
MyClassLoader2
package cn.tedu.test;
import java.net.URL;
import java.net.URLClassLoader;
//自定义类加载器,这种方式很简单,直接提供含参构造就行
public class MyClassLoader2 extends URLClassLoader {
public MyClassLoader2(URL[] urls) {
super(urls);
}
}
TestMyClassLoader2 测试新的类加载器
package cn.tedu.test;
import java.io.File;
import java.net.URI;
import java.net.URL;
public class TestMyClassLoader2 {
public static void main(String[] args) throws Exception {
File file=new File("f:\\testworkspace\\");
//File to URI
URI uri=file.toURI();
URL[] urls={uri.toURL()};
ClassLoader classLoader = new MyClassLoader2(urls);//利用新的类加载器加载指定位置文件
Class<?> cls = classLoader.loadClass("cn.tedu.test.HelloClassLoader");
Object obj = cls.newInstance();
System.out.println(obj);
System.out.println(classLoader);//MyClassLoader2
}
}
基于正在使用的对象进行遍历,对存活的对象进行标记,未标记对象可认为是垃圾对象,然后基于特定算法进行回收,这个过程就叫GC.
GC全称Garbage Collection,所有的GC系统都从如下几个方面进行实现:
1.GC判断策略(例如引用计数,对象可达性分析)
2.GC收集算法(标记-清除,标记-清除-整理,标记-复制-清除)
3.GC收集器(例如Serial,Parallel,CMS,G1)
深入理解GC的工作机制,可以帮你写出更好的Java应用程序,更好的优化应用程序等.
即显式地进行内存分配(allocate)和内存释放(free).
如果忘记释放, 则对应的那块内存不能再次使用.
如果内存一直被占着,却不能使用,这种情况就会产生内存泄漏(memory leak).
比如:c/c++程序中就需要程序员,自己完成对象的回收动作.回收晚了(或者忘了)会导致内存泄漏,回收早了对象还没用完呢,就会报错.
在Java程序中,对于对象的回收问题得到了很好的解决.
Java会自动GC,一般是在JVM系统内存不足时,由JVM系统启动GC对象,自动对内存进行垃圾回收及时释放内存.
JVM会对程序中,已产生的对象进行引用计数, 比如: a对象被引用了2次,b对象被引用了1次,c对象被引用了0次,JVM就会标记这个c对象是需要被清除的对象,并启动GC把c对象清除掉.
注意: JVM包含着多种GC算法收集器.但是无论采用哪种算法,GC一般包含两个步骤: 标记和清除,这也是GC经常使用的标记-清除算法.
标记清除就是对可达对象进行标记,不可达对象即认为是垃圾,然后进行清除.
你整理过操作系统的磁盘碎片么?
那么Java也需要,碎片的整理:
系统GC时,每次执行清除操作,JVM 都必须保证“不可达对象“占用的内存能被回收然后重用.在回收内存这个过程中有可能会产生大量的内存碎片(类似于磁盘碎片), 进而引发两个问题:
我们知道垃圾收集时要停止整个应用程序的运行,那么假如这个收集过程需要的时间很长,就会对应用程序产生很大性能问题.如何解决这个问题呢?通过科学家实验发现,内存中的对象通常可以将其分为两大类:
观察JAVA中,堆内存的内存结构图:
基于此内存架构,对象内存分配过程如下:
垃圾收集事件(Garbage Collection events)通常分为:
现在的GC算法,基本都是要从标记可达对象开始,标记为可达的对象即为存活对象.
同时,我们可以将 查找可达对象时 的起始位置对象 认为是根对象(Garbage Collection Roots).
基于根对象标记可访问或可达对象,对于不可达对象,GC会认为是垃圾对象.
例如: 上图中,绿色云朵为根对象,蓝色圆圈为可达对象,灰色圆圈为垃圾对象
首先,GC遍历内存中整体的对象关系图(object graph)确定根对象,那什么样的对象可作为根对象呢?
GC规范中指出根对象可以是:
移除不可达对象时,会因GC算法的不同而不同,但是大部分的GC操作一般都可大致分为三步:清除(Mark-Sweep),标记清除整理(Mark-Sweep-Compact),标记复制(Mark-Copy).
标记-清除(Mark-Sweep)
对于标记清除算法而言,应用相对简单,但会产生大量的内存碎片,这样再创建大对象时,内存没有足够连续的内存空间可能会出现OutOfMemoryError.
标记-清除-整理(Mark-Sweep-Compact)
标记清除整理算法中,在清除垃圾对象以后,会移动可用对象,对碎片进行压缩,这样会在内存中构建相对比较大的连续空间便于大对象的直接存储,但是会增加GC暂停时间.
标记-复制 (Mark and Copy)
标记复制算法会基于标记清除整理算法,但是会创建新的内存空间用于存储幸存对象,同时复制与标记可以同时并发执行,这样可以较少GC时系统的暂停时间,提高系统性能.
JVM系统在运行时,因新对象的创建可能会触发GC事件.无论哪种GC都可能会暂停应用程序的执行,但如何将暂停时间降到最小,这要看我们使用的GC算法.
现在对于JVM中的GC算法无非两大类: 一类负责收集年轻代,一类负责收集老年代.
假如没有显式的指定垃圾回收算法,一般会采用系统平台默认算法(当然也可以自己指定),例如JDK8中基于特定垃圾回收算法的垃圾收集器应用组合如下:
其中:
Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5 和 JAVA SE6 中客户端虚拟机采用的默认配置.
JVM也提供了其应用的参数配置:java -XX:+UseSerialGC
特点有两个:
CMS的英文名称为 “Mostly Concurrent Mark and Sweep Garbage Collector”,其设计目标是追求更快的响应时间。
特点:
java -XX:+UseConcMarkSweepGC
,默认开启-XX:+UseParNewGC并行收集器,它可利用多个或多核CPU优势实现多线程并行GC操作,其目标是减少停顿时间,以实现更高的吞吐量.
特点:
G1(Garbage-First )收集器是一种工作于服务端模式的垃圾回收器,主要面向多核,大内存的服务器.
G1 在实现高吞吐的同时,也最大限度满足了GC 停顿时间可控的.
在JDK7 update 4 后续的版本中已全面支持G1回收器功能.
特点:
再次强调:G1不是一个实时的收集器,它只是最大可能的来满足设定的停顿时间。G1会基于以往的收集数据,来评估用户指定的停顿时间可以回收多少regions,需要花费的时间,然后确定停顿时间内可以回收多少个regions.
特点:
7. 将java堆均分成大小相同的多个区域(region,1M-32M,最多2000个,最大支持堆内存64G)
8. 内存应用具备极大地弹性(一个或多个不连续的区域共同组成eden、survivor或old区,但大小不再固定)
9. 相对CMS有着更加可控的暂停时间(pause time) 和 更大的吞吐量(throughput)以及更少的碎片(标记整理)
10. 支持并行与并发,可充分利用多CPU,多核优势,降低延迟,提高响应速度
场景应用分析:
java-XX:+UseG1GC 表示启用GC收集器
总之:
G1是HotSpot中最先进的准产品级(production-ready)垃圾收集器.
重要的是, HotSpot 工程师的主要精力都放在不断改进G1上面, 在新的java版本中,都用到了G1收集器.在将来会带来新的功能.