强烈建议大家去网上下载《深入理解JAVA虚拟机》一书,基本上网上的JVM、内存等相关的博客都出自该书 !
参考资料:
java内存分配回收机制 : http://www.cnblogs.com/hnrainll/archive/2013/11/06/3410042.html
jvm类加载机制:http://blog.youkuaiyun.com/a19881029/article/details/17068191 ; https://my.oschina.net/xianggao/blog/87735
jvm启动参数设置详解:http://iamzhongyong.iteye.com/blog/1333100
classload类加载:http://www.cnblogs.com/ChrisWang/archive/2009/11/17/Inside-JVM-4-ClassLoader-Knowledge-Sharing.html ;http://www.cnblogs.com/loritin/p/5347979.html
java内存划分 :
1、堆区 : 存储类实例化对象,VM内存第一大块 。 GC回收
2、方法区:通常也规划在堆区,不过是永久代,存储的是类对象、常量池、静态变量等。 GC回收
3、java运行栈:存储方法的局部变量表,方法执行的出入口,便于JVM进行调度。 方法执行完,自动回收内存
4、本地方法栈:存储本地方法的局部变量表,本地方法的出入口,便于JVM调用native方法,方法执行完,自动回收内存
5、程序计数器:记录线程执行到了那个代码行 。线程执行完,自动回收内存
6、直接内存:计算机除去JVM的内存外的其他区域,包括了native方法的本地库 GC回收,且是FULL-GC的时候顺便回收,不在GC回收的范畴
GC回收
1、GC回收分为新生代、老年代、永久代(比例基本1:8:1) ; 而新生代又分为eden、survive0、survive1区(比例基本8:1:1),
2、当新建一个对象的时候,对象的引用放入java运行栈,对象本身会放入到堆中。 初创建的对象都放在堆的eden区中,当eden区满时候,就会eden执行一次垃圾回收,称为minor GC,将其中不可达的对象清除掉,剩余下来的就会存放到survive0区中 。
3、循环执行2中过程,如果某次minor GC,发现survive0区也满了的时候,就会将eden区与survive0区中的存活对象都复制到survive1中,survive0区清空,eden区清空。 后续执行minor GC,会将eden区中的存活对象放到survive1中
4、如果某次minor GC,发现survive1区也满了的时候,就会将eden区与survive1区中的存活对象都复制到survive0中,survive1区清空,eden区清空。 后续执行minor GC,会将eden区中的存活对象再放到survive0中
5、对象如此在为eden、survive0、survive1区中进行切换 。 当survive0、survive1切换多次(Hotspot虚拟机默认是15次)后,会将依旧存活的对象复制到老年代
6、当老年代满了或者达到设置的full GC的时候,就会进行老年代full Gc 。 由于老年代内存空间大,它满了证明内存对象很多,那么Full Gc执行时间会很长。
问题:
1、如何判断哪些对象是可以被回收的。
从GC-Root向上寻找,只要相关联的都记录为存活的,没有关联的就删除 。GC-Root有且仅有:常量池,静态变量,虚拟栈与本地栈中的本地变量表中引用的对象
2、年轻代回收算法
年轻代使用复制回收算法,因为98%的情况下每一次monitor-gc 都会回收89%的对象,只会有少量的对象存活,所以每次都是把存活的对象赋值到空的survivor区,然后清空eden和另外的survivor区。 不存在执行GC的时候还允许其他工作线程执行。 stop the word ! 如果存活的对象大于survior的量,那么会直接丢到老年代;
3、说说人们常说的CMS回收算法
CMS是针对老年代的回收算法,只能有年轻的复制-回收算法共同合作,与以吞吐量为主的年轻代Paraller Scavenge算法不能共存。它是3次标记回收,而且允许回收的同时工作线程也在继续跑。 当然,在进行标记的时候,也会暂停其他所有的工作线程。 由于它在进行回收的时候,其他工作线程会继续创造新的对象,因此一般内存使用达到60%就会开始回收工作了。最开始使用的是标记-删除算法,当执行多次,内存全是碎片,空间分配不出一个大的整体碎片给一个大对象的时候,也会触发GC,这时采用标记-整理-删除算法。
4、如何确定GC-Root
很显然,如果每次GC都会扫描虚拟栈与本地栈,特别是当有大量方法并存执行的时候,GC会特别慢。 于是,GC会有一个对象oopMap记录栈中引用的对象。当然,并不是每次方法执行、完成都会更新oopMap对象,这会造成大量的开销。 而且并不是在任何时候都利于把工作线程停下来,我们称适当的时候为安全点,指标为“线程指令能长时间的运行,方便复原”,一般为循环点,异常点,方法调用点 。 那么如何让所有的线程同时都到达安全点呢? 实际上并不是同时到达,而是系统将要GC的时候会将GC标志位置为1,每次线程执行到安全点的时候就会去轮询GC标志位,如果为1就自己暂停。当所有线程暂停后就开始进行GC了。
JVM 启动与类加载
JVM的启动大致有3个方式,
一个是.class单个文件,其中有main() 方法,使用java xxxx.class ;
加载过程: 找到jre----------->找到jvm.dll---------------->启动JVM并进行初始化---------->产生bootstrp classloader -------->载入extClassLoader ---------->载入appClassLoader------>运行xxx.class的main()方法
一个是可运行jar包,执行 java -tar xxxx.jar 。
加载过程:找到jre------------>找到jvm.dll----------->启动jvm并进行初始化---------->产生bootstrp classloader -------->载入extClassLoader ---------->载入appClassLoader------>运行可运行文件清单中的MANIFEST.MF 文件中的Main-Class中的main()方法 (下为一个清单例子)
Manifest-Version: 1.0
Class-Path: . LottChartStBox_lib/aopalliance-1.0.jar LottChartStBox_li
b/aspectjweaver.jar LottChartStBox_lib/commons-codec-1.10.jar LottCha
rtStBox_lib/commons-dbcp-1.4.jar LottChartStBox_lib/commons-io-1.3.2.
jar LottChartStBox_lib/commons-lang-2.6.jar LottChartStBox_lib/common
s-lang.jar LottChartStBox_lib/commons-lang3-3.1.jar LottChartStBox_li
b/commons-logging-1.1.jar LottChartStBox_lib/commons-pool-1.5.4.jar L
ottChartStBox_lib/jackson-annotations-2.7.0.jar LottChartStBox_lib/ja
ckson-core-2.7.0.jar
Main-Class: cn.com.sinodata.lottChart.Engine
一个是tomcat部署,过程同一,不过对应的启动class是tomcat自己包中的Bootstrap.java中的main()方法
如上图所示,JVM开启,产生C语言底层写的Bootstrp Loader,然后再载入java中的各个loader 。
载入类加载器后,开始执行主类的main()方法 ,其中的类加载过程称为 动态类加载(参考 http://www.ibm.com/developerworks/cn/java/j-dyn0429/)
其中按需加载到某个类的过程可以分为 加载、验证、准备、解析、初始化、使用、卸载 7个步骤
加载 : 获取定义此类的二进制字节流(loader.getResource()),并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,最后在Java方法区中生成一个代表这个类的java.lang.Class对象作为方法区这些数据的访问入口
验证:验证对应的.class文件,是否都是本JVM能够识别并且不会影响JVM运行的(避免他人直接手编写的class文件、其他JVM拷贝文件等等),主要验证过程包括:文件格式验证,元数据验证,字节码验证以及符号引用验证。
准备:为(加载阶段中的)静态对象(static 修饰)分配内存并赋值默认值,如int ,赋值0 ,Object 赋值null。如果被static修饰同时被final修饰,那么直接赋值目标值。
解析:将字符引用解析为直接引用 (解析时虚拟机将常量池中的符号引用替换为直接引用的过程。)
初始化:为准备阶段中的静态对象赋值目标值,并分配目标值所需内存空间 。 静态对象的赋值 和 静态代码块是按照写的顺序执行的!如果静态代码块在赋值前面,那么输出一个静态对象的值的时候,是系统默认值,准备阶段赋值的
例子:运行如下代码的主类 。 可以在run configuration 中的参数配置中写上 : -verbose class ,可以看各个类的加载顺序
public class Engine {
static {
try {
System.setProperty("log4j.configuration", ClassLoader.getSystemResource("config/log4j.xml").toURI().toString());
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
public static Browser browser;
public static int mainStep = 0;
public static Browser getBrowser() {
return browser;
}
/**
* 主线程
* 0、清除本地数据
* 1、加载系统资源文件
* 2、终端电视机显示缓冲loading页面
* 2、同步投注机时间
* 3、加载log4j配置文件
* 4、取投注机站点机号、服务号、版本号
* 5、从中心同步同步站点信息、玩法、开奖号码、兑奖期等信息
* 6、终端电视机显示默认页面
* 7、启动实时获取站点快开游戏数据/补同步中心遗漏数据 线程
* 8、启动实时监控终端文件的下载进度 线程
* @param args
*/
@SuppressWarnings("resource")
public static void main(String args[]) {
System.out.println(1);
browser = new Browser(PublicParams.LOADING_PAGE); //优先加载,防止弹窗时候JS未加载完报错
new ClassPathXmlApplicationContext("config/applicationContext.xml");
}
解析运行步骤
1、加载Engine类,并初始化engine类 ,则会先执行代码块,然后尝试去加载Browser类,但是不会初始化Browser类 (Browser中的依赖类也会顺势加载)
2、运行到main()方法,输出1,然后去初始化Browser,并且在构建一个Browser对象放到堆中 。 由于Browser的生成需要调用PublicParams的静态变量,那么会初始化Browser后,再初始化PublicParams,然后去构建Browser对象
3、执行Spring中的配置文件加载。
JVM的classLoader执行加载过程:摘自资料 http://www.cnblogs.com/loritin/p/5347979.html
Java ClassLoader加载机制
一.体系结构(自上向下)
1.Bootstrap ClassLoader(BootStrapClassLoader) --- 启动类加载器或者叫引导类加载器,加载jdk核心的APIs,这些APIs一般位于jdk_home/lib下;它是一个本地接口,所以不能从java代码中得到它的信息。例如, log(java.lang.String.class.getClassLoader())得到的是null。
2.Extension ClassLoader(Launcher$ExtClassLoader) --- 扩展类加载器, 负责加载jdk_home/lib/ext目录下的API;
3.System ClassLoader(Launcher$AppClassLoader) --- 系统类加载器 主要负责java -classpath所指的目录下的APIs。
4.Custom ClassLoader --- 用户自定义类加载器,是java.lang.ClassLoader的子类),负责在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件。
二.加载机制
Java的ClassLoader是以父子结构展现的。当有请求向加载器请求类时,首先自底向上去寻找是否已经加载了所需的类,如果存在就返回这个类;如果不存在,就委托当前加载器的父加载器去完成加载类的请求,如果父加载器还有上一级,就向上一级的记载器请求去加载所需类,直到顶层,若顶层加载器(BootStrapClassLoader)无法加载类,就回到下一级,如果下一级仍无法加载,再下一级,直到当前的类加载器,如果仍加载不到,就会抛出ClassNotFoundExceptio。 注意:这里的父子关系不是java中的继承关系,可以理解为一种委托关系。
三.类的唯一标识
JVM中,类的唯一标识是(类名,包名,加载器),由不同加载器加载的类,即使具有相同的类名和包名,也不会被认为是同一个实例,不是类型可比型的。例如:ClassLoader A 有两个子加载器ClassLoader B 和 ClassLoader C,类Foo对ClassLoaderB和ClassLoaderC是可见的,对ClassLoaderA是不可见的,当有请求向ClassLoaderB和ClassLoaderC请求Foo的实例时,由于两个加载器会分别加载Foo,这样JVM会产生两个Foo的实例(foo1,foo2),而且它们是不可比的, 语句foo1=(Foo)foo2会抛出ClassCastException;当然,如果Foo对ClassLoaderA是可见的,Foo就会由ClassLoaderA加载,就不会产生ClassCastException了。
JVM参数配置参考 摘自(http://www.cnblogs.com/edwardlauxh/archive/2010/04/25/1918603.html)
典型设置:
- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 - java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。