程序计数器:线程私有,pc寄存器的一种,可以认作为当前线程的行号指示器,不会出现OOM
java虚拟机栈:线程私有,栈描述的是Java方法执行的内存模型。
- 每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到执行完成的过程,就对应栈帧从入栈到出栈的过程
- 局部变量表中,存储的就是基本数据类型以及对象引用;
- stackOverflowError:线程请求的栈深度大于虚拟机所允许的深度; OutOfMemoryError:虚拟机栈动态拓展时无法申请到足够的内存
本地方法栈:线程私有,虚拟机使用到的native方法服务,可能底层调用的c或者c++,和虚拟机栈区别就是,虚拟机栈为java方法调用,本地方法栈为native方法调用
java堆(java-heap):线程共享的,堆是java虚拟机管理内存最大的一块内存区域,存放的是对象;可以通过配置(-Xmx和-Xms)来拓展堆空间
老年代:三分之二的堆内存,生命周期比较长,通常采用标记-压缩算法进行垃圾回收
年轻代:三分之一的堆内存,生命周期比较短,用复制算法对新生代进行垃圾回收
- eden区:8/10 的年轻代空间
- survivor0区(from survivor):1/10 的年轻代空间
- suvivor1区(from survivor):1/10 的年轻代空间
方法区(Non-heap):线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量;
jdk1.7以前,方法区被称为永久代(Perm区),1.8以后使用metaspace代替永久代,两者只是方法区不同的实现方式而已;相对于永久代而言,metaspace最大的区别就是放在本地内存中而非jvm中,仅受本地内存大小的限制;
直接内存:jdk1.4引入了NIO,它可以使用Native函数库直接分配堆外内存。
着重关注虚拟机栈,java堆以及方法区三个块区域;
分析下retail-finane的启动脚本;
#!/bin/sh
MEMORY=`free -m | awk '/Mem/{print $2}'`
if [ $MEMORY -gt 15000 ];then
JVM_HEAP="8192"
elif [ $MEMORY -gt 7500 ];then
JVM_HEAP="4096"
elif [ $MEMORY -gt 3500 ];then
JVM_HEAP="2048"
elif [ $MEMORY -gt 1800 ];then
JVM_HEAP="1024"
else
JVM_HEAP=$((MEMORY/7*4))
fi
JVM_EDEN=$((JVM_HEAP/8*3))
JVM_META=$((JVM_HEAP/4))
JVM_MAX_META=$((JVM_HEAP/2))
ACTIVE_PROFILE=qa
JAVA_OPTS="-server -Xms${JVM_HEAP}m -Xmx${JVM_HEAP}m -Xmn${JVM_EDEN}m -XX:MetaspaceSize=${JVM_META}m -XX:MaxMetaspaceSize=${JVM_MAX_META}m -XX:SurvivorRatio=8 -Xss512k -XX:-UseAdaptiveSizePolicy -XX:+PrintPromotionFailure -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/retail-finance/oom.hprof -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:GCLogFileSize=100M -Xloggc:/data/logs/retail-finance/gc.log"
以机器2g内存为例:
首先了解java的数据类型,包括基本数据类型和引用数据类型;
基本类型包括byte、short、char、int、long、float、double、boolean,这些是jvm预先定义好的;引用数据类型根据虚拟机规范就是类型 (class type),数组类型(array type)和接口类型(interface type);
其中数组类是由Java虚拟机直接生成的,而类和接口的使用需要jvm加载对应的字节流来创建;
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程。
主要包括加载、验证、准备、解析、初始化总共五个加载阶段,加上使用以及卸载,就是类的整个生命周期;
加载:
验证:判断class文件的字节流的合法性,是否符合当前虚拟机要求;大致有4个阶段
准备:为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配
解析:虚拟机将常量池内的符号引用替换为直接引用的过程
以上三个过程也统称为连接(Linking)过程;
初始化:执行类构造器
方法的过程,是真正开始执行类中的定义的代码,主要包括执行static变量赋值语句,staic{}语句块
注意和实例构造器
方法区分,
方法主要是执行成员变量和成员对象的赋值,普通语句块的执行,构造函数的执行;
可以看到,以上5个步骤,只有加载和初始化是可以通过用户自定义去参与,其他三个阶段都是虚拟机自动完成的。
以上类加载以及初始化的过程可以看到,所有的操作几乎都是在方法区操作,存放了类的信息,静态变量,静态代码块,以及符号引用等;
类的加载时机没有强制约束,是虚拟机自行控制的。
类的初始化时机,虚拟机规范明确指明有且只有5种情况下会对类做初始化。
以上都被称之为对类的主动引用
,其他所有引用类的方式都被称为被动引用
被动引用的例子:通过子类引用父类的静态字段,不会导致子类初始化;引用常量也不会触发该类的初始化;
类加载器:就是指实现加载类整个动作的代码模块;
类加载器和类本身 确定该类再虚拟机中的唯一性;
java中只有2种加载器类型:
一种是启动类加载器(Bootstrap ClassLoader),由C++实现,是虚拟机自身的一部分;
$JAVA_HOME/lib
中照虚拟机可识别的(按文件名识别)加载到虚拟机中,java无法获取到另一种就是所有其他的类加载器,由java实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader,还可以细分为以下3类:
$JAVA_HOME/lib/ext
目录中的类库,可以在java中获取到Classpath
上所指的的类库,也称之为系统类加载器,可以在java中获取到;上图就是类加载器之间的层次关系,称之为双亲委派模型;
什么是双亲委派模型?
双亲委派模型的工作流程
- 类加载器收到类加载的请求;
- 把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
- 启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,通知子加载器进行加载。如果最初发起类加载请求的子类也无法加载时,就抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载
如下就是双亲委派模型的代码实现,
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型的优点:java类随它的类加载器一起具备了带有优先级的层级关系,保证了java程序的稳定运作;
为什么要破坏双亲委派模型?
举个例子,当要父类加载器需要调用加载子类加载器才能加载的类时,就无法通过双亲委派模型去加载需要的类,最典型的就是JDBC。
前面说的是类的初始化,接下来说下类的实例化。
先进行类的初始化,把类加载到虚拟机中,然后会进行类的实例化进行对象的创建;
实例化,就是由执行类实例创建表达式而引起的对象创建。有以下几种创建对象的方式:
package com.darkman.jvm.createobject;
import com.darkman.jvm.Employee;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author wbhe
* @desc
* @date 2019-07-25
* @ver
**/
public class ObjectTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, CloneNotSupportedException, IOException {
// new 创建对象
Employee employee = new Employee("new");
System.out.println("employee"+"=====" + employee.toString());
// Class类的newInstance()创建的对象, 调用的是无参构造;
Employee employee1 = (Employee)Class.forName("com.darkman.jvm.Employee").newInstance();
System.out.println("employee1" +"====="+ employee1.toString());
// Constructor类的newInstance方法, 可以调用有参构造函数,甚至是私有构造函数
Employee employee2 = Employee.class.getConstructor(String.class).newInstance("有参数的");
System.out.println("employee2" +"====="+ employee2.toString());
// Constructor类的newInstance方法, 私有构造函数
Constructor constructor = Employee.class.getDeclaredConstructor(Long.class);
constructor.setAccessible(true);
Employee employee3 = (Employee) constructor.newInstance(1234L);
System.out.println("employee3" +"====="+ employee3.toString());
// clone创建,不会调用构造函数
Employee employee4 = (Employee) employee.clone();
System.out.println("employee4" +"=====" + employee4.toString());
// 反序列化创建对象,不会调用构造函数
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employ"));
oos.writeObject(employee);
File file = new File("employ");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Employee employee5 = (Employee)ois.readObject();
System.out.println("employee5" +"=====" + employee5.toString());
}
}
为对象分配内存(类加载后就知道需要分配的内存大小);
-XX:+/-UseTLAB
设置是否使用TLAB;使用该方式分配对象内存-XX:PretensureSizeThreshold
参数设置临界值,超过这个值的对象将分配在老年代young gc(minor gc)
仍然存活,年龄设置为 1并进入survivor区,每熬过一次young gc
年龄+1,当年龄大于指定的阈值(默认为15),该对象将会进入老年代;可以通过-XX:MaxTensuringThreshold
设置阈值;将内存空间都初始化为零值(见上文零值图片);
设置对象的基本信息(对象头);对象属于哪个实例,如何找到类的元数据信息,哈希吗,gc分代年龄等;
以上步骤,对象已经产生了,只是还没有进行初始化值的设置;
按顺序进行实例变量的初始化;执行
方法,对实例变量按照编写的代码进行初始化设值(注意和零值的区别);
执行调用构造函数 ;
对象创建的整体过程如下:
对象在内存中存储的布局可以分为三块区域:对象头(header),实例数据(Instance Data)和对齐填充(Padding)。
对象头:存储对象自身的运行时数据,以及类型指针;如果对象是数组,还会记录数组长度;
运行时数据包括,哈希码、gc分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳
类型指针,指的就是指向对象元数据的指针,即该对象指向那个类
实例数据:存储对象真正的有效信息,即对象中各个字段的内容
对齐填充:不是必须存在,也没有特殊含义,仅做占位符作用
有2种方式,一种是句柄访问,一种是直接指针访问;
通过句柄访问对象,栈中的refrence存储的是指向句柄的地址;
通过直接指针访问,栈中的refrence存储的是指向对象的地址;
比较以上2种方式的优劣点;
上面说到,在jvm中,只有程序计数器不会出现OOM,其他几个区域都换产生OOM,简单看下几类内存溢出的情况;
java堆溢出的demo:-Xms
设置最小值,—Xmx
设置最大值;-XX:+HeapDumpOnOutOfMemoryError
设置堆OOM后自动Dump内存快照;
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author wbhe
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
通过设置较少栈容量,达到栈溢出的目的;-Xss
参数设置
/**
* VM Args:-Xss128k
* @author wbhe
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
运行时常量池导致的内存溢出,-XX:PermSize
设置方法区大小,-XX:MaxPermSize
设置方法区的最大容量;
/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
* @author wbhe
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author wbhe
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
本机直接内存溢出demo,-XX:MaxDirectMemorySize
知道直接内存的大小,如果不指定,默认和-Xmx
值一样;
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* @author wbhe
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}