相对于C、C++等语言来讲,Java语言一个很美好的特性就是自动内存管理机制。C语言等在申请堆内存时,须要malloc内存,用完还有手动进行free操做,若程序员忘记回收内存,那这块内存就只能在进程退出时,由操做系统来释放了。而Java程序员(初级)则基本上不须要对内存分配、回收作过多的关注,彻底由Java虚拟机来管理。不过,一旦出现内存泄漏或者溢出,若是不理解JVM管理内存的机制,又如何排除错误、调优系统呢?java
Java程序最终编译成字节码运行在JVM之上,程序计数器能够看作时当前线程执行的字节码的行号指示器。字节码解释器在工做的时候就是经过这个计数器来选择下一条要执行的字节码指令,分支、循环、异常处理等都须要依赖该计数器。程序员
另外,在多线程的场景下,一个CPU(或者一个核)在一个肯定的时刻,只能执行一个线程的一条字节码指令,多线程的实现是由CUP在不一样线程间切换来完成的。而CPU在线程间切换所依赖的也是程序计数器(CPU跳来跳去要肯定调到某个线程的某一行上,从这一点能够看出,程序计数器是线程私有的,线程间互不影响)。算法
注意,在JVM规范中,程序计数器不会发生OOM(就记个数,能用多少内存)。api
线程私有,与线程生命周期相同。数组
栈描述的是Java执行方法的内存模型。线程是进程创造的(例如服务器的每一个请求能够看作是一个线程,举例ThreadLocal),由多个方法间的调用组成,每一个方法在执行时会建立一个栈帧,栈帧内存储的是局部变量表,操做数栈,动态连接,方法出口等信息。每一个方法从调用直到执行完成,就是一个栈帧入栈到出栈的过程。服务器
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。类型:boolean、byte、char、short、int、float、reference(对象起始地址的指针或者句柄)、returnAddress(指向了一条字节码指令的地址)八种。在编译时,每一个方法所需的局部变量表大小就固定下来了。(疑问:若在循环体中定义变量,JVM如何取得的局部变量表的大小? 在内层循环中定义变量到底会不会存在重复分配的问题,这涉及到编译器的优化,不过主流编译器(如vs和gcc)这一块优化都比较好,不会反复分配变量。栈中的空间在编译这个代码的时候大小就肯定下来了,运行这个方法时空间就已经分配好了,不要想固然的觉得声明一次就要分配一次空间,那是c语言,java能够重用这些超出做用域的空间。)多线程
虚拟机栈这块区域规定了两种异常:StackOverflowError,线程请求的栈深度超过必定量(好比递归层级过多,大概几千(与分配给jvm的内存有关)就报错);OutOfMemoryError,没法申请到足够的内存。app
本地方法栈与虚拟机方法栈做用类似,区别为虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用的native方法服务。不少虚拟机在实现时已经将两者合二为一。抛错相同。jvm
Java native 方法:一个Native Method就是一个java调用非java代码的接口。大多数应用场景为java须要与一些底层系统如操做系统、某些硬件交换信息时的状况。ide
全部线程共享,用于存放全部线程产生的对象实例(还有数组)。
堆是垃圾收集器管理的主要区域。为了更好的回收或者分配内存,堆可能会被分为多个区域,例如分代收集算法的垃圾回收器会将堆分为新生代和老年代(固然还能够继续细分:Eden、From Survivor、To Survivor等)。但无论如何划分,每一个区间存储的内容是不变的,都是对象实例。
另外,堆在内存中并非物理连续的,只要逻辑连续便可。当向堆申请内存(实例化对象),而堆中找不到这么大的空间时)会抛出OutOfMemoryError(最新虚拟机均可动态扩展,但扩无可扩时也会抛错)。
线程共享,方法区内存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。一些虚拟机实现上,将方法区做为堆上的“永久代”,意味着垃圾回收器能够向管理堆同样来管理这块内存(但本质上,方法区和永久代是不等价的,会产生一些问题,官方已经不推荐这么使用。例如,String.intern()方法在不一样的虚拟上会由于该机制而表现不一样)。
固然,方法区也确实有一些“永久”的意思,进入到该区域的数据,例如类信息,基本上就不会被卸载了。但其实也会被卸载,只是卸载的条件至关的苛刻,致使不少垃圾回收器在这部分起到的做用并不大
当方法区没法知足内存分配要求时,将抛出OutOfMemoryError异常。
是上边1.5里讲的方法区中的一部分。Class文件在编译期会生成各类字面量和符号引用,这部份内容将在类加载后,放入到方法区的常量池中存放。另外,并不是只有预置入Class文件中的常量池的部分才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,例如String类的intern()方法。
运行时常量池属于方法区的一部分,因此当申请不到内存的时候,会抛出OutOfMemoryError异常。
一些native函数库能够直接分配堆外内存,例如NIO,它能够经过一个存储在Java堆中的DirectByteBuffer对象做为这块内存的引用进行操做。因为直接内存是受机器总内存限制的,当申请不到内存的时候,一样会抛出OutOfMemoryError异常。
建立对象的几种方式:1)使用new 关键字;2)使用反射的newInstance()方法,newInstance方法经过调用无参的构造函数建立对象;3)clone,调用clone时,jvm会建立一个新的对象,将前面对象的内容所有拷贝进去。用clone方法建立对象并不会调用任何构造函数;4)反序列化,jvm会给咱们建立一个单独的对象。在反序列化时,jvm建立对象并不会调用任何构造函数。 咱们在着重谈一谈new时都发生了什么。
当jvm遇到new指令时,第一步要作的是去常量池中找一找,看是否能找到对应类的符号引用,而且检查该符号引用表明的类是否被加载、解析、初始化过(检查类是否被加载)。
类加载检查经过以后,接下来就是分配内存,对象所需内存大小在类加载完成以后就彻底肯定了,因此分配对象的工做其实就是把一块肯定的内存从Java堆中划出来。
堆内存是规整的时候——用过的在一边、没用过的在另外一边,中间用一个指针标记,内存分配就是指针向没用过的方向挪动一下,这种方式叫作指针碰撞。这个时候若多个线程一块儿申请内存,就会冲突。对应的解决方法:1)加同步,采用CAS加失败重试策略;2)为每一个线程预分配一小块内存(Thread Local Allocation Buffer,TLAB),哪一个线程须要内存,就在本身的TLAB上进行分配,而只在建立线程为线程分配TLAB是用同步锁定。
堆内存不是规整的时候——用过和没用过的乱糟糟的放在一块儿,内存分配就须要记住哪些地方被分配了,哪些地方仍是空闲的,这种分配方式叫作分配列表。在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表。
对内存是否规整,是由使用的垃圾回收机制是否带有压缩整理功能决定的。
内存分配完成以后,虚拟机须要设置一下对象的数据:非对象头部分,会被初始化为零值,这个操做保证了对象的实例字段在Java代码中能够不赋初始值就直接使用;对象头部分,进行必要的设置,例如:对象类的元数据信息、哈希码、GC分代信息、锁信息等。
一个新的对象产生了,后续就在java语言层面,按照程序员的想法,执行init函数了。
一个对象在内存中由三部分组成:对象头,实例数据,对齐填充。
对象头由两部分组成:一部分存储运行时数据:哈希码、GC分代、锁状态等等;另外一部分是指向类元数据的指针,说明该对象是由哪一个类实例化来的。
实例数据存放的是对象真正存储的有效信息,也就是程序员本身定义的各类类型字段内容。须要注意的时,为了节省内存,相同类型的字段老是被放在一块儿存放的,并且子类较窄的变量有可能会插入到父类变量的空隙中。
因为对象大小必须是8字节的整数倍,因此对齐填充,就是凑整用的,无关紧要。
两种定位方式:句柄、直接指针。贴两个图,分别说一下他们的优缺点。
句柄访问,堆内划分出一块内存来做为句柄池,对象引用存储的是句柄地址,句柄中包含了对象的真实地址信息。有点:对象被移动时,无需通知引用这个它的对象,只须要更改句柄池就好了;缺点:增长了一层寻址,会慢一些。
直接指针访问:对象引用的就是真实的地址信息。优势:快,节省一次指针定位时间;缺点:对象被移动时,引用它的对象也要跟着修改。
不断递归,超过栈容许的最大深度时,就能够触发StackOverflowError。看一个栈深度超限引起StackOverflowError的示例,代码及错误信息以下:
1 public class Stack_StackOverflowError { 2 private Integer stackLength = 1; 3 4 public void stackLoop() { 5 stackLength++; 6 stackLoop(); 7 } 8 9 public static void main(String[] args) { 10 Stack_StackOverflowError a = new Stack_StackOverflowError(); 11 try { 12 a.stackLoop(); 13 } catch (Throwable e) { 14 System.out.println("stack length: " + a.stackLength); 15 throw e; 16 } 17 } 18 }
Exception in thread "main" stack length: 9651(本人机器64位,12G内存,未对jvm系统作任何参数修改) java.lang.StackOverflowError at java.lang.Number.<init>(Number.java:55) at java.lang.Integer.<init>(Integer.java:849) at java.lang.Integer.valueOf(Integer.java:832) at com.star.ott.scriptsTranslation.api.business.test.Stack_StackOverflowError.stackLoop(Stack_StackOverflowError.java:10) at com.star.ott.scriptsTranslation.api.business.test.Stack_StackOverflowError.stackLoop(Stack_StackOverflowError.java:11) at com.star.ott.scriptsTranslation.api.business.test.Stack_StackOverflowError.stackLoop(Stack_StackOverflowError.java:11)
堆是用来存放对象示例的,只要不断建立对象,而且保证垃圾回收器没法回收这些对象,就能产生堆的OutOfMemoryError异常。看一个不断建立对象引起OutOfMemoryError的示例,代码及错误信息以下:首先将idea中的堆大小限制为20M。
import java.util.ArrayList; import java.util.List; /** * Created by laizy on 2018/7/30. */ public class Heap_OutOfMemoryError { static class OOMTestObject { } public static void main(String[] args) { //保证建立出来的对象不被回收 List<Heap_OutOfMemoryError.OOMTestObject> list = new ArrayList<Heap_OutOfMemoryError.OOMTestObject>(); //不断建立对象 while (true) { list.add(new Heap_OutOfMemoryError.OOMTestObject()); System.out.println(list.size()); } } }
540213 540214 540215 540216 540217(向队列中插入这么多对象以后,崩了) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.star.ott.aaa.Heap_OutOfMemoryError.main(Heap_OutOfMemoryError.java:20) Process finished with exit code 1
另外,在jdk1.8中,String常量池已经从方法区中的运行时常量池分离到堆中了(划重点),也就是说不断的建立String常量,也可以将堆撑爆,代码及错误信息以下:
import java.util.ArrayList;
import java.util.List;
/**
* Created by laizy on 2018/7/31.
*/
// -Xms20m -Xmx20m
public class Heap_StringConstantOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("0");
int i = 1;
try {
while (true) {
list.add(list.get(i - 1) + String.valueOf(i++).intern());
if (list.size() % 100 == 0) {
System.out.println(list.size());
}
}
} catch (Throwable e) {
System.out.print(list.size());
throw e;
}
}
}
1900
2000
2100
2200
2201
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.star.ott.aaa.Heap_StringConstantOOM.main(Heap_StringConstantOOM.java:15)
运行时常量池属于方法区的一部分,首先咱们经过将常量池撑爆的方式,制造方法区溢出。首先仍是限制jvm的参数,设置方法区大小为5m,不限制的话,程序得跑到地老天荒。参照3.2中设置jvm的方式设置方法区大小。在jdk8以前,方法区放到了永久代中,对应参数为:-XX: PermSize=5m -XX:MaxPermSize=5m;在jdk8之后,方法区放到的元数据里,对应参数为:-XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5m。代码及错误信息以下:
好吧,让你失望了,个人环境是jdk8,在jdk8中我作不到(捂脸),但愿你们指点一下,如何在jdk8中实现常量池的溢出。
另外,在以前的jdk中,要实现常量池的溢出是经过不断建立String来实现的,对,就是上边3.2中的用String.intern()撑爆堆的那种作法。
接下来咱们经过CGLib技术,不断建立动态类,将方法区撑爆。代码及异常以下:
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * Created by laizy on 2018/7/31. */ // -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m public class RunTime_ObjectOOM { public static void main(String[] args) { int i = 0; while (true) { i++; System.out.println(i); 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 { } }
328 329 330 331(331次循环以后,方法区崩了) Exception in thread "main" java.lang.OutOfMemoryError: Metaspace at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:386) at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:219) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377) at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285) at com.star.ott.aaa.RunTime_ObjectOOM.main(RunTime_ObjectOOM.java:28)
最后,栈中的OOM、直接内存OOM并未作验证。