jvm总结
jvm内存图
方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是 线程私有的内存区域。
1、java对(heap) 是java虚拟机管理内存的最大的一块,是所有线程共享的一块区域,在虚拟机启动时创建,此内存的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配
2、方法区(Method Area),和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码信息
3、程序计数器,是线程私有的,是一块内存较小的空间,作用是当前线程所执行的字节码的行号 指示器。(就相当于拿着一个车牌信息去找车的感觉)
4、JVM栈(JVM Stacks),与程序计数器一样,是线程私有的,生命周期与线程相同,虚拟机栈描述的是Java方法执行的 内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部 变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
5、本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就 是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
堆栈用法
通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存,都使用jvm的栈空间,而是用new关键字和构造器创建的对象则使用堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用的分代收集算法,所以对空间还可以细分为新生代和老年代,再细分一点就可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息,常量,静态变量等数据,程序中的100,"hello"等都被放在常量池,常量池属于方法区的一部分,栈空间操作起来最快,但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小可以通过JVM的启动参数来设置调整,栈空间不足会出现StackOverowError,而堆和常量池空间不足则会引发OutOfMemoryError。
String a = "abc"
String b = new String("abc")
如上图两个代码:
对于第一种,jvm会首先在常量池(String constant pool)中寻找是否已经存在"abc"常量,如果没有则创建,并将创建的引用返回给String a,如果已经存在,则直接返回常量池中的"abc"给String a,这个创建的方法只会在常量池中发生,
对于第二种,jvm会直接在常量池中判断是否有,没有就创建,同时,当jvm遇到new时,会重新创建String对象,并把"abc"的value和hash赋值给他,最后把该对象的引用返回给Sting b
类加载机制
类在从加载再虚拟机内存时开始,到卸载出内存它的为止,它的整个生命周期为:加载,验证,准备,解析,初始化,使用,卸载这七个阶段,
其中类加载的过程包括:加载,验证,准备,解析,初始化五个阶段,在这五个阶段中,加载,验证,准备,初始化这四个阶段的顺序是一定的,而解析阶段则不一定,它在某些情况下可以在初始化之后开始,这里的几个阶段是按顺序开始,而不是按顺序进行或者完成,因为这些阶段都是相互交叉混合进行,通常在一个阶段的执行中调用或**另一个阶段
1、加载: "加载"是"类加载"机制的第一个过程,在加载阶段。虚拟机主要完成三件事:
1)、通过类的全限定名称来获取改类的二进制字节流
2)、将这个二进制字节流所代表的的静态存储结果转化为方法区的运行时数据结构
3)、在堆中生成这个类的Class对象,作文方法区中这些数据的访问入口。相对于类加载机制的其他阶段而言,"加载"阶段是可控性最强的阶段,因为我们程序猿可以通过系统的类加载器加载,还可以使用自己的类加载器加载
2、验证: 主要作用就是验证被加载类的正确性,也是连接阶段的第一步,说白了就是.class文件不能对我们的虚拟机有危害,所以先检测验证一下,它主要是完成了一下四个阶段:
1)、文件格式的验证:验证.class文件是否符合规范,是否能被当前的虚拟机处理
2)、元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言的规范的要求,
3)、字节码验证:这是整个验证过程最复杂的阶段,主要是通过字节流和数据流分析,确定程序语义是合法的,符合逻辑的当然我们 ,如果对自己的代码有十足的自信可以使用== -Xverty:none ==来关闭大部分的验证。
3、准备: 主要是为类,变量分配内存,并设置初始值,这些内存都在方法区分配(这里的初始值是默认的数据类型的初始值,比如:public static int a =1 这里的初始值是0,即int的默认值,在a的值为1时是在初始化阶段才被赋值的)
4、解析: 解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义 的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代 表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到 目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限 定符7类符号引用进行。
5、初始化: 是类加载机制的最后一步,在这个阶段java代码程序才真正开始执行,在初始化阶段,我们程序猿可以根据自己的需求来赋值
类加载器:
1、java语言自带三个类加载器:
Bootstrap ClassLoader :最顶层的加载类,主要加载核心类库,也就是我们环境变量下 面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader 的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路 径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个 目录。
Extention ClassLoader :扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的 jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
Appclass Loader:也称为SystemAppClass。 加载当前应用的classpath的所有类。 我们看到java为我们提供了三个类加载器,应用程序都是由这三种类加载器互相配合进行 加载的,如果有必要,我们还可以加入自定义的类加载器。这三种类加载器的加载顺序是 什么呢? Bootstrap ClassLoader > Extention ClassLoader > Appclass Loader
2、类加载的三种方式 认识了这三种类加载器,接下来我们看看类加载的三种方式。
(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。 (2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是 Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。
(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块
3、委派双亲:
工作流程:当一个类加载器收到类加载任务,会先交给其他父类的类加载器去完成,因此,最终的加载任务都会传递到顶层的启动类加载器,只有当父类的类加载器无法完成加载任务时,才会尝试执行加载任务,这个理解起来就简单了,比如说,另外一个人 给小费,自己不会先去直接拿来塞自己钱包,我们先把钱给领导,领导再给领导,一直到 公司老板,老板不想要了,再一级一级往下分。老板要是要这个钱,下面的领导和自己就 一分钱没有了。(例子不好,理解就好)
采用委派双亲的一个好处就是不管哪个加载器加载这个类,最终都是委托给顶层的启动器类加载,这样就保证了使用不用的类加载器,最终得到同一个Object对象,
双亲委派原则归纳一下就是: 可以避免重复加载,父类已经加载了,子类就不需要再次加载 更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么 用户可以随意定义类加载器来加载核心api,会带来相关隐患
4、自定义类加载器
自定义类加载器有两种方法
(1)遵守双亲委派模型:继承ClassLoader,重写ndClass()方法。
(2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。
通常我们推荐采用第 一种方法自定义类加载器,最大程度上的遵守双亲委派模型。
我们看一下实现步骤
(1)创建一个类继承ClassLoader抽象类
(2)重写ndClass()方法
(3)在ndClass()方法中调用deneClass()
垃圾回收算法
1、标记–清除算法:
顾名思义:分别是标记,清除两个阶段。首先标记处需要回收的对象,在标记完成后统一回收被标记的对象。缺点:标记和清除两个阶段的效率都不高。而且标记清除后会产生大量的不连续的内存空间碎片,空间碎片太多会导致在以后需要分配较大对象的空间时导致找不到连续的空间而不得不提前触发另一次垃圾回收动作,
2、复制算法:
为了解决效率问题,出现了复制算法,具体的就是把内存分为等同的两块,当快满的时候将存活的对象复制到另一块上去,再把原来的空间内存清理一次,这样就是对半个内存整个清理,不存在不连续的空间,但是代价就是把内存缩小为原来的一半,感觉是比较大的代价
3、标记–整理算法:
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的 是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存 中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过 程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所 有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,就感觉像让最后存活的都向一端排一次序,然后清除另一端的内存空间
jvm调优:
jvm的重要参数:
-Xms为jvm启动时分配的内存,比如-Xms512m ,表示分配512M
-Xmx为jvm运行过程中分配的最大内存,比如-Xmx512m ,表示jvm进程最多只能够占用512M内存
-Xss为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M 另 外还有重要参数如下:
-XX:PermSize=256m表示非堆区初始内存分配大小,其缩写为permanent size(持久 化内存)
-XX:MaxPermSize=512m表示对非堆区分配的内存的最大上限
-XX:MaxTenuringThreshold设置的是年龄阈值,默认15(对象被复制的次数)
-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的 时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC);
-XX:+UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的70%),如 果不指定,JVM仅在第一次使用设定值,后续则自动调整.
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1: 3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有 两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小 收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器 垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:lename 并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU 数。并行收集线程数。
jvm调优说白了就是对参数一点一点的调整,
JVM调优原则:
1、多数的Java应用不需要在服务器上进行GC优化;
2、多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合); 4、减少创建对象的数量;
5、减少使用全局变量和大对象;
6、GC优化是到最后不得已才采用的手段;
7、在实际使用中,分析GC情况优化代码比优化GC参数要多得多;