一、JVM
1、JVM是运行在操作系统之上的。Scala语言也是运行在JVM之上。
2、JVM其实是一个标准,规范,符合这个规范的软件都可以称之为java 虚拟机。
Sun HotSpot —> Java 的老东家
Sun买的一家小公司。被Oracle公司收购以后,后来又收购了BEA 公司,于是在某个版本后,将两个虚拟机合二为一。
BEA JRockit —> BEA 有一个服务器非常的有名 WebLogic(tomcat)。
IBM J9 —-> 某某某行业方案解决专家 (IBM小型机)
3、JVM内存模型
1)ClassLoader 类加载器
所有的java文件,都必须编译为class文件 Hello.java —> Hello.class
所有的class文件要想运行,必须放入虚拟机中,通过我们的类加载器,加载进去。
类文件被加载,需要符合我们的双亲委派策略:
一个类的class文件,只能被加载一次。加载的过程中有很多个加载器。而且这些加载器是有顺序的。
第一个加载器:Bootstrap ClassLoader —>
第二个加载器:Extensioin ClassLoader —> 拓展加载器,只加载拓展的jar —> <JAVA_HOME>\lib\ext
第三个加载器:Application ClassLoader —> 如果用户没有自己定义类加载器,默认使用这个。
Student.java —>Student.class 加载开发人员编写的java代码(jar)
第四个加载器:用户自定义加载器(只有用户写了才会有这个加载器)
所有的class文件,在jvm中都是独一份的。
双亲委派策略:
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托
双亲委派策略的好处:
Student.java --> Student.class
String.java --> String.class
比如我们也可以自定编写一个String类,报名和类名都跟java自带的一模一样,但是我们会发现我们自己写的String类压根加载不了,原因是BootstrapClassLoader 已经加载过了。我们自己写的,不会重复加载。
这是java语言设计的一种安全机制。
拓展:双亲委派策略是否可以打破?如何打破?—《java面试速成手册》
2)本地方法接口(native interface)
当我们去java代码中追源码,看到native 修饰的方法,就是底部了,再追就是C++ 了。
我们可以发现Object类中有很多方法是使用native修饰,并且没有方法体,那说明这些方法超出了Java的范围。 所以底层实现需要使用JNI--->Java Native Interface。
native修饰的方法就是告诉java本身,此时需要调用外部的本地类库即C/C++类库来执行这个方法
3) java内存模型中的运行区
分为5部分:栈、本地方法栈、堆、寄存器、方法区
以上图,橘黄色是线程共享的,灰色的是线程私有的。垃圾回收的是线程共享的部分。
1、本地(native)方法栈:其实就是存放java代码中存在的native方法的。
2、方法区(静态方法区): 静态方法、静态变量、class类信息
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间。
存放在方法区的:静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中
ps:只要是被线程私有独享的一律没有回收,只有是线程共享才能有回收。
所谓的垃圾回收回收的是java虚拟机中的方法区和堆。堆占(99%),方法区(1%)
3、程序计数器:
代码运行到了哪一行,需要一个东西记下来,这个东西就是程序计数器。
A线程执行到了23行代码,突然被B线程抢走了,A线程再次执行的时候就从23行以后执行。
栈 管运行,堆 管存储
4、栈(java的栈)
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。
一个方法(Main)调用了A方法,先把Main压栈,A 方法放在Main方法上面,A方法结束以后,A方法出栈,Main出栈。
栈帧中主要保存3 类数据:
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
- 栈操作(Operand Stack):记录出栈、入栈的操作;
- 栈帧数据(Frame Data):包括类文件、方法等等
一个方法被调用了,也就意味着在栈中多一个栈帧。栈帧中有什么信息(这个方法的参数,返回值,方法中的变量,以及这个方法属于哪个类,这个方法是被谁调用的。)
栈是线程私有的,不需要垃圾回收。
方法被调用,进入栈内存,但是一旦调用多了,栈内存满了,内存溢出(StackOverflowError)。
一般出现在递归调用时,没有编写跳出递归的条件,会出现栈内存溢出。
5、堆(Heap)
详见下面
4) 堆(Heap)
堆是管存储。我们new出来的对象,都放在堆中。堆至关重要。
堆内存分为三部分:
- Young Generation Space 新生区 Young/New
- Tenure generation space 养老区 Old/ Tenure
- Permanent Space 永久区 Perm
新生区:存放被new出来的对象的。当新生区中的对象越来越多,就会引发GC(垃圾回收),每一次回收都会有一些幸存者,如果一个对象幸存了15次(默认值),就可以进入养老区养老,当养老区的对象也越来越多,就引发大GC。
一个对象被new出来,放入我们新生区中的伊甸区(Eden),当我们的Eden区使用率达到一定比例之后,进行一次小GC(minor GC), 将我们的Eden和幸存区的From区一起GC 的,幸存的对象,放入我们的幸存区的To区。放入之后,将伊甸区和From区清空。这个时候原来的To区就变为From区。
所谓的From区就是有数据的区域,谁有数据谁是From区。
伊甸区:From: To = 8:1:1
循环往复,一个对象超过15次小GC,仍然存活,就进入老年代(养老区)。
养老区:
1、躲过15次Minor GC的普通对象
2、大对象 String[] arr = new String[50010241024]
养老区满了之后,我们需要进行Full GC (大GC)
如果养老区进行了大GC之后,还是没有空间存储后面的对象,直接报内存溢出(java.lang.OutOfMemoryError: Java heap space)
如果报堆内存溢出,必定进行过Full GC 了。
OutOfMemoryError
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
- Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
- 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
永久区(元空间):
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
100M 的jar 包 —> 加载 (永久区)—> 永久区中的对象是不会被回收的。
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
背会:
- Jdk1.6及之前: 有永久代, 常量池1.6在方法区
- Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆
- Jdk1.8及之后: 无永久代,常量池1.8在元空间
- 我们可以通过 -Xms -Xmx 设置堆内存大小,一般设置的一样。
- 64M —> 600M
- 每一次GC 耗费时间,干脆,直接给到位 600M 600M
运行参数设置:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
二、GC垃圾回收
自己补充:垃圾回收器都有哪些牌子?
Garbage Collection, 简称GC, 是分代垃圾收集算法。 频繁收集Yong区, 较少收集Old区, 基本不动Perm区。
垃圾回收算法:
1、复制算法
将区域中存活的对象,一个个复制到一个新的区域,然后格式化老区域。这种做法就是复制算法。
优点:内存排列整齐,效率比较高
缺点:需要浪费一半的内存空间
我们的新生代就使用了这个算法,这个算法比较适用于存活率不高的场景。
2、标记清除算法
标记和清除是两个动作:
先对我们的内存整体进行标记,标记哪些对象存活者。
优点: 内存使用率比较高,100M 就能用100M
缺点: 需要扫描两次,会产生碎片。
老年代使用的GC算法用到了一部分这个算法。老年代使用的是标记清除和标记整理两种算法的混合使用。
3、标记整理
也是分为两个步骤:标记操作(有清除的成分),整理操作。
优点:内存使用率很高,也没有碎片
缺点:耗时
难道就没有一种最优算法吗? 猜猜看
没有最优的方式,分代整理算法(不同的区域使用不同的算法)
三、堆外内存(了解)
1、概念
堆内存分为堆内和堆外
以上说的是堆内: 新生代+老年代+元空间,由JVM管理
堆外内存:由操作系统管理的那部分区域。
2、好处
1) 堆外的部分不需要垃圾回收
2) 可以直接写磁盘上,提高效率(IO)