类加载机制
类加载器的种类
- 启动类加载器(Bootstrap ClassLoader) :C语言实现,负责加载
JRE核心类库
,如JRE目标下的rt.jar,charsets.jar等。 - 扩展类加载器(Extension ClassLoader) :负责加载
JRE扩展目录
ext中jar类包。 - 应用类加载器(Application ClassLoader) :负责加载
ClassPath
路径下的类包 。 - 用户自定义加载器(User ClassLoader) :负责加载
用户自定义
路径下的类包。
类加载机制
-
全盘负责委托机制
该类所依赖和引用的类也由当前类的类加载器
载入,除非显示使用另一个类加载器。 -
双亲委派机制
先委托父类加载器
寻找目标类,在找不到的情况下在自己的路径
中查找并载入目标类。
双亲委派模式优点
沙箱安全机制:防止核心库被篡改,自己写的String.class类不会被加载。
避免类重复加载:父类加载器
已经加载了该类时,不需要子类加载器
再加载一次。
类生命周期
1. 加载
将.class文件从磁盘读到内存。
2. 链接
- 验证:验证
字节码文件
的正确性(魔数)。 - 准备:给类的静态变量分配内存,并赋予
默认值
。 - 解析:类装载器加载
所引用的其它所有类
(静态链接)。
静态链接:解析阶段,由符号引用转行为直接引用。
动态链接:运行阶段,由符号引用转行为直接引用。
3. 初始化
为类的静态变量赋予真正的初始值
,执行静态代码块。
JVM运行时数据区域
Java多线程机制使多个任务同时执行处理,所有的线程共享JVM内存区域主存,每个线程有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理(操作码+操作数)。
- 线程私有:程序计数器、虚拟机栈、本地方法栈,线程结束就释放不用垃圾回收。
- 线程共享:堆、元数据区(方法区)。
1、程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,指向方法区中字节码。字节码解释器
通过改变这个计数器的值来选取下一条需要执行字节码指令(分支、循环、跳转、异常处理、线程恢复等)。
- 通过轮流切换并分配处理器执行时间实现多线程,各线程之间的计数器互不影响。
- 如果执行一个Java 方法,计数器记录正在执行的虚拟机字节码指令的地址。
- 如果正在执行Natvie 方法,这个计数器值则为空Undefined。
程序计数器该区域异常状况:
唯一没有任何OutOfMemoryError 情况的区域。
2、Java虚拟机栈(Java Virtual Machine Stacks)
虚拟机栈描述的是Java方法执行的内存模型
,每个方法被执行时会创建一个栈帧
。方法被调用的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。例如:int i = 0,虚拟机栈内存用4个字节来存储;引用型变量存储是一个地址引用,其大小也是固定。
Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以指向常量池的引用作为参数。部分符号引用在类加载阶段(解析)转化为直接引用,这种转化为静态链接
。部分符号引用在运行期间转化为直接引用,这种转化为动态链接
。
Ocean river = new River();
静态类型:编译期间确定的类型(Ocean)
实际类型:运行期间确定的类型(River)
该区域异常状况:
- StackOverflowError:
线程请求栈深度
大于虚拟机所允许深度
时抛出异常;- OutOfMemoryError:如果虚拟机栈可以
动态扩展
,当扩展时无法申请到足够的内存时抛出异常。
// 虚拟机栈操作说明
public int math() {
int a = 1;
int b = 2;
int c = (a+b) * 10;
return c;
}
javap -c -v test.class > test.txt反编译class文件后打开如下所示:
虚拟机栈/**
* -Xss228k,虚拟机栈大小为228k
*/
public class Test {
private static int count = 0;
public static void main(String[] args) {
Test test = new Test();
test.test();
}
/**
* 没有终止条件的递归调用
*/
private void test() {
try {
count++;
test();
} catch (Throwable e) {
//Exception已经捕获不了JVM抛出的StackOverflowError
System.out.println("递归调用次数" + count);
e.printStackTrace();
}
}
}
JVM抛出StackOverflowError表示线程请求的栈深度大于JVM所允许的深度。 对于单线程情况下,无论如何抛出的都是StackOverflowError。如果要抛出OOM异常,导致的原因是不断地在创建线程,直到将内存消耗殆尽。
StackOverflowError-Xss设置每个线程的栈容量,此时如果栈内存越大
,可以创建的线程数量少
,就容易出现OOM;如果栈内存越小
,可以创建的线程数量就多
,就不容易出现OOM。要避免这种情况最好就是减少堆内存+方法区内存,或者适当减少栈内存。对于栈内存的配置,一般采用默认值1M,或者采用64位操作系统以及64位的JVM。
剩余内存=JVM内存 - 堆内存 - 方法区内存。
可以创建的线程数量 = 剩余内存 / 栈内存。
3、本地方法栈(Native Method Stacks)
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
该区域异常状况:
该区域也会抛出StackOverflowError 和OutOfMemoryError异常。
堆(Heap)
堆是Java 虚拟机所管理的内存中最大的一块,被所有线程共享的一块内存区域。在虚拟机启动时创建,主要目的是存放对象实例。堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。既可以实现成固定大小,也可以扩展的,通过-Xmx和-Xms 控制。
随着JIT 编译器
的发展与逃逸分析技术
的逐渐成熟,栈上分配、标量替换优化技术导致一些变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”。Java 堆是垃圾收集器管理的主要区域,如果从内存回收的角度看,现在收集器基本都是采用分代收集算法。
Java 堆可分为
年轻代
和老年代
或者元空间
。
年轻代分为
Eden(伊甸园区)、Survivor(幸存区)。
堆空间内存分配(默认情况下)
年轻代 : 三分之一的堆空间
老年代 : 三分之二的堆空间
eden: 8/10 的年轻代空间
survivorFrom : 1/10 的年轻代空间
survivorTo : 1/10 的年轻代空间
该区域异常状况:
堆中没有内存完成实例分配并且堆无法再扩展,将抛出OutOfMemoryError 异常。
-
新生代(Young Generation)
一个类在这里产生、应用、最后被垃圾回收器收集结束生命。 当Eden区空间用完时,程序又需要创建对象,将在Eden区进行垃圾回收(Minor GC)。将Eden区中不再被其它对象引用的对象进行销毁。将Eden区中剩余的对象移到From Survivor区。若From Survivor区也满了,再对该区进行垃圾回收,然后移动到To Survivor区。From到To使用复制算法,转到15次(VM中对象头markOop中一个byte位0000从0-15,可以通过参数 -XX:MaxTenuringThreshold 设定)移到老年区。
-
老年代(Old Generation)
新生代经过多次GC仍然存在的对象移动到老年区。若老年代也满了,老年区将发生Major GC(也可以叫Full GC)。若Full GC之后依然无法进行对象的保存,就会抛出 OOM(OutOfMemoryError)异常。
-
字符串常量池
JDK1.7把字符串常量池从永久代中剥离出来,存放在堆空间中。
命令行上执行如下命令,查看所有默认的JVM参数。
java -XX:+PrintFlagsFinal -version
输出两个有关键的参数
[Global flags]
uintx InitialSurvivorRatio = 8 {product}
uintx NewRatio = 2 {product}
... ...
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
参数解释
参数 | 作用 |
---|---|
-XX:InitialSurvivorRatio | 新生代Eden/Survivor空间的初始比例 |
-XX:Newratio | Old区 和 Yong区 的内存比例 |
默认参数下,如果仅给出Eden区40M,求堆空间总大小?
根据比例可以推算出,两个survivor区各5M,年轻代50M。老年代是年轻代的两倍,即100M。那么堆总大小就是150M。
/**
测试代码
JVM参数配置
-XX:MaxPermSize=10m
-XX:PermSize=10m
-Xms100m
-Xmx100m
-XX:-UseGCOverheadLimit
*/
public class StringOomMock {
public static void main(String[] args) {
try {
List<String> list = new ArrayList<String>();
for (int i = 0; ; i++) {
System.out.println(i);
list.add(String.valueOf("String" + i++).intern());
}
} catch (java.lang.Exception e) {
e.printStackTrace();
}
}
}
JDK1.6 环境下是永久代OOM
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.jd.im.StringOomMock.main(StringOomMock.java:17)
JDK1.8运行结果同1.7一样,都是堆空间OOM。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at java.io.PrintStream.print(PrintStream.java:597)
at java.io.PrintStream.println(PrintStream.java:736)
at com.jd.im.StringOomMock.main(StringOomMock.java:16)
方法区
与Java 堆一样,各个线程共享
的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,别名叫非堆
Non-Heap或永久代
(Permanent Generation)。垃圾收集行为在这个区域比较少出现,该区内存回收主要针对常量池回收和对类卸载。
该区域异常状况:
方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。
-XX:MaxPermSize=20M 方法区最大大小20M
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
//不断创建线程
}
}
}
-
JDK6字符串常量池存在方法区,会抛出OutOfMemoryError:Permanent Space;
-
JDK7字符串常量池移到了Java堆中,上面的代码不会抛出OOM,若将堆内存改为20M则会抛出OutOfMemoryError:Java heap space;
-
JDK8元空间取代方法区这个概念,配置-XX:MaxPermSize没有任何意义,取代它的是
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
等。 -
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java 虚拟机对Class 文件的每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,运行期间也可能将新的常量放入池中。
该区域异常状况:
当常量池无法再申请到内存时,会抛出OutOfMemoryError 异常。
-
元空间(Meta Space)
在JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。
元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。
移除永久代为什么移除了永久代?
参考官方解释http://openjdk.java.net/jeps/122
移除永久代是为融合HotSpot与JRockit,因为JRockit没有永久代不需要配置。
JVM参数配置
-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=50m
借助cglib框架生成新类。
public class MetaSpaceOomMock {
public static void main(String[] args) {
ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaSpaceOomMock.class);
enhancer.setCallbackTypes(new Class[]{Dispatcher.class,
MethodInterceptor.class});
enhancer.setCallbackFilter(new CallbackFilter() {
@Override
public int accept(Method method) {
return 1;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
});
Class clazz = enhancer.createClass();
System.out.println(clazz.getName());
//显示数量信息(共加载过的类型数目,当前有效的类型数目,已经被卸载的类型数目)
System.out.println("total: " + loadingBean.getTotalLoadedClassCount());
System.out.println("active: " + loadingBean.getLoadedClassCount());
System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());
}
}
}
该区域异常状况:
JDK1.7报OOM的将是PermGen区域。
JDK1.8报异常OutOfMemoryError: Metaspace。
直接内存(Direct Memory)
JDK1.4引入了NIO基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native函数库直接分配堆外内存。直接内存并不是虚拟机运行时数据区的一部分,也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
本机直接内存的分配不会受到Java 堆大小的限制,肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)大小及处理器寻址空间的限制。各个内存区域的总和大于物理内存限制,导致动态扩容时出现OutOfMemoryError异常。
Java 栈、Java 堆、方法区 内存区域之间的关联关系
Object obj = new Object();
Object obj
这部分语义反映到Java 栈的本地变量表中,作为reference类型数据出现。
new Object()
这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值的结构化内存,这块内存的长度是不固定的。
Java堆中包含能查找到此对象类型数据(对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。
reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,不同虚拟机实现的对象访问方式有所不同,主流的访问方式有两种:使用句柄
和直接指针
。
句柄访问方式在Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
句柄访问方式的最大好处就是reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference 本身不需要被修改。
句柄访问方式直接指针访问方式在Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址。HotSpot默认方式。
直接指针访问方式的最大好处就是速度更快,节省指针定位的时间开销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。各种语言和框架使用句柄来访问的情况也十分常见。
直接指针访问方式异常实例
1、Java 堆溢出
设置Java 堆的大小为20MB,不可扩展(将堆的最小值-Xms 参数与最大值-Xmx 参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDump OnOutOfMemoryError 虚拟机在出现内存溢出异常时,Dump 出当前的内存堆转储快照以便事后进行分析。
开发工具设置JVMJava堆溢出测试
-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails
public class HeapOutOfMemory {
public static void main(String[] args) {
List<TestCase> cases = new ArrayList<TestCase>();
while (true) {
cases.add(new TestCase());
}
}
}
2、java栈溢出
Java栈溢出
-Xss128k
public class StackOverFlow {
private int i ;
public void plus() {
i++;
plus();
}
public static void main(String[] args) {
StackOverFlow stackOverFlow = new StackOverFlow();
try {
stackOverFlow.plus();
} catch (Exception e) {
System.out.println("Exception:stack length:"+stackOverFlow.i);
e.printStackTrace();
} catch (Error e) {
System.out.println("Error:stack length:"+stackOverFlow.i);
e.printStackTrace();
}
}
}
3、常量池溢出
常量池内存溢出
-XX:PermSize=10M -XX:MaxPermSize=10M
public class ConstantOutOfMemory {
public static void main(String[] args) throws Exception {
try {
List<String> strings = new ArrayList<String>();
int i = 0;
while (true) {
strings.add(String.valueOf(i++).intern());
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
4、方法去溢出
方法区溢出
-XX:PermSize=10M -XX:MaxPermSize=10M
public class MethodAreaOutOfMemory {
public static void main(String[] args) {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestCase.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2,
MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2);
}
});
enhancer.create();
}
}
}
5、直接内存溢出
直接内存溢出
-Xmx20M -XX:MaxDirectMemorySize=10M
public class DirectoryMemoryOutOfmemory {
private static final int ONE_MB = 1024*1024;
private static int count = 1;
public static void main(String[] args) {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(**null**);
while (true) {
unsafe.allocateMemory(*ONE_MB*);
count ++;
}
} catch (Exception e) {
System.out.println("Exception:instance created "+count);
e.printStackTrace();
} catch (Error e) {
System.out.println("Error:instance created "+*count*);
e.printStackTrace();
}
}
}
逃逸分析:决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。
内存分配担保机制:当在新生代无法分配内存的时候,把新生代的对象转移到老生代,然后把新对象放入腾空的新生代。
JVM 内存参数说明
高并发注意停顿时间,吞吐量是垃圾收集时间和总用户代码执行时间占比。
最低延时考虑FullGC最长停顿时间,平均停顿时间,吞吐量考虑FullGC次数。
-
-Xms
:设置堆的最小值(初始值); -
-Xmx
:设置堆的最大值; -
-Xmn
:设置年轻代的大小,表示NewSize=MaxNewSize=Xmn
,一般建议该参数来设置新生代大小,避免新生代扩容,优先级低于-XX:NewSize
和-XX:MaxNewSize
参数; -
-XX:NewSize
:设置年轻代最小值(初始值); -
-XX:MaxNewSize
:设置年轻代最大值; -
-XX:NewRatio
:设置年轻代和年老代的比值。如:为3,表示年轻代:年老代=1:3
,年轻代占整个年轻代年老代和的1/4; -
-XX:SurvivorRatio
:设置年轻代中Eden区与两个Survivor区的比值。如:3,表示Eden:Survivor=3:2
,一个Survivor区占整个年轻代的1/5; -
-XX:PermSize
: 设置方法区初始大小(JDK1.7及以前); -
-XX:MaxPermSize
: 设置方法区最大大小(JDK1.7及以前); -
-XX:MetaspaceSize
: 设置元数据区初始值(JDK1.8及以后); -
-XX:MaxMetaspaceSize
:设置 元数据区最大值(JDK1.8及以后); -
-XX:MaxDirectMemorySize
:设置直接内存大小,默认与堆内存最大值一样(-Xmx); -
-Xss
:设置一个线程栈的大小,JDK5.0 以后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减 小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成。 -
-XX:MaxTenuringThreshold
:设置垃圾最大年龄。如果设置为 0 的话,则年轻代对象不经 过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象再年轻代 的存活时间,增加在年轻代即被回收的概率。 -
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/heap/dump
:设置当JVM发生OOM时,自动生成DUMP文件,/path/heap/dump
表示文件路径; -
-verbose:gc
:表示输出GC的详细情况;
- 和 -X、 -XX的区别
-
-
:标准参数,所有的JVM实现都必须实现这些参数的功能,而且向后兼容; -
-X
:非标准参数,默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容; -
-XX
:非稳定参数,此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
示例
JDK 1.7: set JAVA_OPTS=-Xms1024m -Xmx1024m -Xss512k -XX:PermSize=128m -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
JDK1.8 : set JAVA_OPTS=-Xms1024m -Xmx1024m -Xss512k -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
查看JVM运行时参数
- -XX:+PrintFlagsInitial 查看初始值
- -XX:+PrintFlagsFinal 查看最终值(初始值可能被修改掉)
- -XX:+UnlockExperimentalVMOptions 解锁实验性参数
- -XX:+UnlockDiagnosticVMOptions 解锁诊断参数
- -XX:+PrintCommandLineFlags 打印命令行参数
- -XX:+PrintGCDetails 查看GC信息
示例
java -XX:+PrintFlagsInitial -version
输出结果:
D:\workspace\etc\etc-credit-card>java -XX:+PrintFlagsInitial -version
[Global flags]
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product}
uintx AdaptiveSizePolicyOutputInterval = 0 {product}
uintx AdaptiveSizePolicyWeight = 10 {product}
uintx AdaptiveSizeThroughPutPolicy = 0 {product}
uintx AdaptiveTimeWeight = 25 {product}
bool AdjustConcurrency = false {product}
bool AggressiveOpts = false {product}
intx AliasLevel = 3 {C2 product}
bool AlignVector = true {C2 product}
bool UseLargePagesIndividualAllocation := false {pd product}
...
将结果输出到文本:
java -XX:+PrintFlagsInitial -version > flag.txt
image.png=表示默认值; :=表示被用户或JVM修改后的值
网友评论