是如何或用什么jvm加载机制到jvm里面的内存中的

51CTO旗下网站
浅析JVM内存结构和6大区域
内存作为系统中重要的资源,对于系统稳定运行和高效运行起到了关键的作用,Java和C之类的语言不同,不需要开发人员来分配内存和回收内存,而是由JVM来管理对象内存的分配以及对象内存的回收(又称为垃圾回收、GC),这对于开发人员来说确实大大降低了编写程序的难度,但带来的一个副作用就是,当系统运行过程中出现JVM抛出的内存异常(例如OutOfMemoryError)的时候,很难知道原因是什么,另外一方面,要编写高性能的程序,通常需要借助内存来提升性能,因此如何才能合理的使用内存以及让JVM合理的进行内存的回收是必须掌握的,本文将主
作者:zhaozheng7758来源:zhaozheng7758的博客| 11:09
其实对于我们一般理解的计算机内存,它算是CPU与计算机打交道最频繁的区域,所有数据都是先经过硬盘至内存,然后由CPU再从内存中获取数据进行处理,又将数据保存到内存,通过分页或分片技术将内存中的数据再flush至硬盘。那JVM的内存结构到底是如何呢?JVM做为一个运行在操作系统上,但又独立于os运行的平台,它的内存至少应该包括象寄存器、堆栈等区域。
JVM在运行时将数据划分为了6个区域来存储,而不仅仅是大家熟知的Heap区域,这6个区域图示如下:
&JVM内存的分配结构示意图
下面将逐一介绍下各个区域所做的工作及其充当的功能。
PC Register(PC寄存器)
PC寄存器是一块很小的内存区域,主要作用是记录当前线程所执行的字节码的行号。字节码解释器工作时就是通过改变当前线程的程序计数器选取下一条字节码指令来工作的。任何分支,循环,方法调用,判断,异常处理,线程等待以及恢复线程,递归等等都是通过这个计数器来完成的。
由于Java多线程是通过交替线程轮流切换并分配处理器时间的方式来实现的,在任何一个确定的时间里,在处理器的一个内核只会执行一条线程中的指令。因此为了线程等待结束需要恢复到正确的位置执行,每条线程都会有一个独立的程序计数器来记录当前指令的行号。计数器之间相互独立互不影响,我们称这块内存为&线程私有&的内存。
如果所调用的方法为native的,则PC寄存器中不存储任何信息。
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址,因此Java中基本类型的变量是值传递,而非基本类型的变量是引用传递,Sun &&&&&&&&& JDK的实现中JVM栈的空间是在物理内存上分配的,而不是从堆上分配。
由于JVM栈是线程私有的,因此其在内存分配上非常高效,并且当线程运行完毕后,这些内存也就被自动回收。
当JVM栈的空间不足时,会抛出StackOverflowError的错误,在Sun JDK中可以通过-Xss来指定栈的大小,例如如下代码:
new&Thread(new&Runnable(){&&&&&&&&&&&&public&void&run()&{&&&&&&&&&&&&&&&loop(0);&&&&&&&&&&&&}&&&&&&&&&&&&&&&&&&&private&void&loop&(int&i){&&&&&&&&&&&&&&&if(i!=1000){&&&&&&&&&&&&&&&&&&&i++;&loop&(i);&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&else{&&&&&&&&&&&&&&&&&&&return;&&&&&&&&&&&&&&&}&&&&&&&&&&&&}&&&&&&&&&&&}).start();&
当JVM参数设置为-Xss1K,运行后会报出类似下面的错误:
Exception in thread &Thread-0&java.lang.StackOverflowError
l& 堆(Heap)
Heap是大家最为熟悉的区域,它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收,Heap在32位的操作系统上最大为2G,在64位的操作系统上则没有限制,其大小通过-Xms和-Xmx来控制,-Xms为JVM启动时申请的最小Heap内存,默认为物理内存的1/64但小于1G,-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4,默认当空余堆内存小于40%时,JVM会增大Heap的大小到-Xmx指定的大小,可通过-XX:MinHeapFreeRatio=来指定这个比例,当空余堆内存大于70%时,JVM会将Heap的大小往-Xms指定的大小调整,可通过-XX:MaxHeapFreeRatio=来指定这个比例,但对于运行系统而言,为了避免频繁的Heap Size的大小,通常都会将-Xms和-Xmx的值设成一样,因此这两个用于调整比例的参数通常是没用的。其实jvm中对于堆内存的分配、使用、管理、收集等有更为精巧的设计,具体可以在JVM堆内存分析中进行详细介绍。
当堆中需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
l& 方法区域(MethodArea)
方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,可见方法区域的重要性。同样,方法区域也是全局共享的,它在虚拟机启动时在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代,默认为64M,可通过-XX:PermSize以及-XX:MaxPermSize来指定其大小。
l& 运行时常量池(RuntimeConstant Pool)
类似C中的符号表,存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。类或接口的常量池在该类的class文件被java虚拟机成功装载时分配。
l& 本地方法堆栈(NativeMethod Stacks)
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
例如有这么一段代码:
public&class&A&{&&&&&&&&&&&&&&&&&&&&public&static&void&main(String[]args){&&&&&&&&&&&&String&a=&a&;&&&&&&&&&&&String&b=&b&;&&&&&&&&&&&&String&ab=&ab&;&&&&&&&&&&&&System.out.println((a+b)==ab);&&&&&&&&&&&&&&&&&&&System.out.println((&a&+&b&)==ab);&&&&&&&&&&&&&&&final&String&afinal=&a&;&&&&&&&&&&&&String&result=afinal+&b&;&&&&&&&&&&&&System.out.println(result==ab);&&&&&&&&&&&&&&&&&&String&plus=a+&b&;&&&&&&&&&&&&System.out.println(plus==ab);&&&&&&&&&&&&&&&&&&&&&&System.out.println(plus.intern()==ab);&&&&&&&}&}&
分析下上面代码执行的结果,可通过javap &verbose A来辅助理解分析。
l& (a+b)==ab
a+b是两个变量相加,需要到运行时才能确定其值,到运行时后JVM会为两者相加后产生一个新的对象,因此a+b==ab的结果为false。
l& (&a&+&b&)==ab
&a&+&b&是常量,在编译时JVM已经将其变为&ab&字符串了,而ab=&ab&也是常量,这两者在常量池即为同一地址,因此(&a&+&b&)==ab为true。
l& result==ab
result=afinal+&b&,afinal是个final的变量, result在编译时也已经被转变为了&ab&,和&ab&在常量池中同样为同一地址,因此result==ab为true。
l& plus=ab
plus和a+b的情况是相同的,因此plus==ab为false。
l& plus.intern()==ab
这里的不同点在于调用了plus.intern()方法,这个方法的作用是获取plus指向的常量池地址,因此plus.intern()==ab为true。
在掌握了JVM对象内存分配的机制后,接下来看看JVM是如何做到自动的对象内存回收的,这里指的的是Heap以及Method Area的回收,其他几个区域的回收都由JVM简单的按生命周期来进行管理
原文链接:http://blog.csdn.net/zhaozheng7758/article/details/8623562
【编辑推荐】
【责任编辑: TEL:(010)】
大家都在看猜你喜欢
关注热点热点头条关注
24H热文一周话题本月最赞
讲师:305251人学习过
讲师:251739人学习过
讲师:131361人学习过
精选博文论坛热帖下载排行
为了满足广大考生的需要,我们组织了参与过多年资格考试命题或辅导的教师,以新的考试大纲为依据,编写了《数据库系统工程师考试全程指导》...
订阅51CTO邮刊3373人阅读
今天用java -jar执行一个jar文件提示内存不够,需要设置虚拟机的堆大小。以下是参考资料:
堆(Heap)和非堆(Non-heap)内存   按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给
自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。 堆内存分配   JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx 指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到- Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC
后调整堆的大小。 非堆内存分配   JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。 JVM内存限制(最大值)   首 先JVM内存限制于实际的最大物理内存(废话!呵呵),假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然 可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统 下为2G-3G),而64bit以上的处理器就不会有限制了。   通常,我们为了避免内存溢出等问题,需要设置环境变量   JAVA_OPTS
-Xms256M -Xmx512M 等,【对于服务器,一般都设置成一样的】   但是有的时候可能这样的设置还会不行(比如,当Server应用程序加载较多类时,即jvm加载类时,永久域中的对象急剧增加,从而使jvm不断调整永久域大小,为了避免调整),你可以使用更多的参数配置,   如: java -Xms512m -Xmx512m -XX:PermSize=64m -XX:MaxPermSize=128m
  其中,使用
-XX:MaxPerSize标志来增加永久域的大小,-XX:PerSize标志设置初始值1332人阅读
【java知识】(4)
JVM内存区域模型
也称永久代” 、“非堆”, 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为,最大值为,可以通过和 参数限制方法区的大小。
运行时常量池:是方法区的一部分,文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
2.虚拟机栈
描述的是方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表包括参数、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。
局部变量表存放了编译器可知的各种基本数据类型、、、、、、、、对象引用引用指针,并非对象本身,其中位长度的和类型的数据会占用个局部变量的空间,其余数据类型只占个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
3.本地方法栈
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的方法服务,而本地方法栈则是为方法服务。
也叫做堆、堆是虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在启动时创建。该内存区域存放了对象实例及数组所有的对象。其大小通过最小值和最大值参数设置,为启动时申请的最小内存,默认为操作系统物理内存的但小于,为可申请的最大内存,默认为物理内存的但小于,默认当空余堆内存小于时,会增大到指定的大小,可通过来指定这个比列;当空余堆内存大于时,会减小的大小到指定的大小,可通过来指定这个比列,对于运行系统,为避免在运行时频繁调整的大小,通常与的值设成一样。
由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代任然存活的对象。
程序新创建的对象都是从新生代分配内存,新生代由和两块相同大小的通常又称和或和构成,可通过参数来指定新生代的大小,也可以通过来调整及的大小。
用于存放经过多次新生代任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:①大对象,可通过启动参数设置单位为字节,默认为来代表超过多大时就不在新生代分配,而是直接在老年代分配。②大的数组对象,切数组中无引用外部对象。
老年代所占的内存大小为对应的值减去对应的值。
5.程序计数器
是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
JVM加载过程
Java语言中,类只有被加载到中才能运行,当运行指定的程序时,会将编译生成的 文件按照一定的规则加载到内存中,并组织成为一个完整的应用程序。类的加载过程是由类加载器完成的(即由和它的子类完成),而类加载器本身也是一个类,其实质是将类文件由硬盘加载到内存中。
类的加载方式有两种:
()显式加载
通过调用方法将所需的类加载到中
()隐式加载
程序在创建新的对象时,隐式地调用类加载器把对应的类加载到中
在语言中,类的加载是动态且灵活的,往往一个大的项目包含很多类,而每一个类或接口都对应一个文件,当程序运行时只需要将需要的类(保证程序运行的基础类,例如基类)加载到中,暂时不需要的类可以先不加载,这样一方面可以提高运行速度,另一方面也可以降低程序运行时对内存的开销。而且,每一个类文件都可以看成是动态的加载单元,当项目需要对某个类进行修改时,修改完毕后只需要重新编译加载被修改的类即可,而不用全部的类都重新进行编译。
类可以分为三种:系统类、扩展类、自定义类,而根据不同的类提供了不同的类加载器
Bootstrap Loader
&==加载系统类(的类)
ExtClassLoader
&==加载扩展类(的类)
AppClassLoader
&==加载应用类(指定的目录或中的类)
具体步骤:
()首先会找到,并且找到位于内部的,这才是真正的虚拟机,然后加载到动态库,激活虚拟机。
()进行初始化操作,结束之后产生启动类加载器
()除了进行一些基本的初始化动作外,最重要的是加载扩展类加载器,并且设定其为,也就代表其父加载器为
()然后再要求加载中的自定义类加载器,并设定其为实体,这两个加载器都是以静态类的形式存在的。
※需要注意的是,其实是谁跟被谁加载的并没有直接关系
我们可以测试一下:
public class classloader {
public static void main(String[] args)
throws Exception{
ClassLoader App = classloader.class.getClassLoader();//class加载器上一层加载器根部加载器
运行结果:
sun.misc.Launcher$AppClassLoader@
sun.misc.Launcher$ExtClassLoader@2490fd20
Bootstrap Loader输出的原因是它是由语言实现的,所以在语言中看不到
程序说明这个类是由加载的
类的加载主要有三步:
()装载:根据查找路径找到相应的文件并导入
()链接:检查文件是否正确给类中的静态变量分配存储空间将符号引用转换成直接引用
()初始化:静态变量和静态代码块的初始化操作
双亲委托机制:
双亲委托模式也就是一个类加载器请求另一个类加载器来加载类型的过程。
除启动类加载器以外的每一个类加载器,都有一个“双亲”类加载器 ,在某个特定的类加载器试图以常用方式加载某个类以前,它会先默认地将这个任务“委派”给它的双亲,请求它的双亲来加载这个类。这个双亲再依次请求它自己的双亲来加载这个类型。这个委派的过程一直向上继续,直到达到启动类加载器,通常启动类加载器是委派链中的最后一个类加载器。如果一个类加载器的双亲类加载器有能力来加载这个类型。则这个类加载器返回这个类型。否则,这个类加载器试图自己来加载这个类。
当一个程序运行时,虚拟机在启动时实例化了两个用户自定义类加载器一个“扩展类加载器”一个“自定义类加载器”这些类装载器和启动类加载器一起联入一个委托链中启动类加载器在最顶端。1512人阅读
java技术(3)
最近看了一下JVM的内存分配,还是比较复杂的。这里做个总结,首先一个common sense就是操作系统会为每个java进程实例化一个jvm实例。jvm然后再来运行java程序,具体的过程就不多说了,简单来说就是核心classloader如bootstrap, extention, System对类的加载(一定是此顺序,jvm对类的加载采取的是代理委托方式,防止核心类被hack),找到对应的main入口来运行。
这里主要是想总结一下,每个java进程对应的jvm对内存的分配,运行时是什么样的。我们都知道jvm内存分为
1. PC计数器
3. 栈(jvm栈,本地方法栈)
4. 方法区(包括class信息,静态变量,class文件常量池,运行时常量池,JIT缓存代码)。
其中,PC计数器,栈是线程私有的,而堆和方法区是线程共享的。操作系统分配给jvm的内存的总大小是有限的,这和操作系统以及是32bit还是64bit,具体的jvm实现都有关系。另外,堆和方法区内存的大小是可以在jvm启动参数中配置的。我们程序员平时经常说的内存操作,OOM都是指的堆内存。
这里顺便说一下JVM的GC机制,JVM的GC不是采用的引用计数算法,而是可达性分析算法。堆根据GC回收的分代算法,又可以分为新生代Eden+2*Survivor, 和老年代。对新生代进行标记-复制算法,进行一次Minor GC,而对老年代进行标记-整理算法,进行一次Major/Full GC,当然肯定有一次Minor GC。程序中绝大部分new的对象放置在Eden区,少数比如大型对象,数组,和长期存活对象直接进入老年代。进入新生代的根据对象年龄计数器可以逐步晋升至老年代中。
那么对于不同的区域,什么时候存放什么东西都是有规范的。
总结如下:
1. 堆: 1) 类的成员变量引用,这条实际上是对2)的说明。 2)new且只有new出来的对象放在堆里。
2. 栈:运行时,成员函数的局部变量引用及其字面量。
3. 方法区:1) class 文件常量池,存放成员变量里的字面量,字符串常量。 2) 静态成员变量。 3)运行时常量池,存放成员函数运行时的常量。
现在举例说明:
int i1 = 1; //i1在堆中A对象里,1在方法区的class文件常量池中
String s1 = "abc"; //s1在堆中A对象里,"abc"在方法区的class文件常量池中
String s2 = new String("abc"); //s2在堆中A对象里,"abc"在方法区的class文件常量池中,只有一份,new出来的String对象在堆里
static int i2 = 2; //i2在方法区,2在方法区的class文件常量池中
//s3在方法区,"xyz"在方法区的class文件常量池中
static String s3 = "xyz";
//s4在方法区,"xyz"在方法区的class文件常量池中,只有一份,new出来的String对象在堆里
static String s4 = new String("xyz");
public void func(){
int i3 = 3; //i3在栈中,字面量3也在栈中
int i4 = 3; //i4在栈中,字面量3已经存在,此时只有一份
String s5 = "china"; //s5在栈中,"china"在方法区的运行时常量池中
String s6 = new String("china"); //s6在栈中,"china"在方法区的运行时常量池中已经有一份相同拷贝,不再存, new出来的对象在堆中
那么,当A被classloader装载并且调用func函数的时候,其所在的jvm内存中不同的地方都有哪些关于A的信息呢?分析如下:
首先jvm第一次碰到A时,比如new A()时,会查看方法区里是否已经存放过关于此类的信息,如果没有,则先调用classloader(还是按照那个Bootstrap, extention…的顺序),最后装载A,此时,方法区里就有关于A类的Class信息了,并且由于在编译期间就能确定成员变量所引用的常量,因此,此时class文件常量池也会有信息,i1所引用的1,s1所引用的”abc”, i2所引用的2,s3所引用的”xyz”。
new A()紧接着会导致在堆中分配关于A的内存,其中包括成员变量i1, s1, s2。其实s2所引用的new String(“abc”)中”abc”也是编译期间就能够确定,因此这里的”abc”也会存在class文件常量池,于是会先去class文件常量池找是否已经有一份相同的,由于之前已经有一份,于是不再存第二份。而new出来的String(“abc”)对象会在堆中有一份。
由于i2, s3, s4都是静态变量,因此它们存在方法区,2和”xyz”存在class文件常量池,注意”xyz”也只有一份,道理同2。另外s4引用的new对象也会在堆中有一份。
当程序运行调用A的func函数时,此时,jvm栈就开始工作了,会有一个关于func函数的栈帧(statck Frame),入栈,里面有i3, i4变量引用和常量或者说字面量3,注意3此时在栈中只有一份!如果后期i4被赋值为4,则栈会开辟新的空间存一个4,i3不变仍然为3。s5,s6也在栈中,”china”由于是在运行时才确定,因此存放在方法区的运行时常量池中,s6所引用的new的String(“china”)中的“china”也只在运行时常量池中保存一份,另外new会在堆中开辟一个新的对象空间存放此对象。
因此,对于equals相等的字符串,在常量池(class文件常量池或者运行时常量池)中永远只有一份,在堆中有多份。因为String类重写/覆盖了Object类的equals方法,只要字符串内容相等即为true,而Object是必须同一个对象的引用地址才为true。但是String并没有重写/覆盖==操作符,所以String对象的==还是只有同一个对象的地址引用才为true。
并且,延伸出来很多面试题的答案,比如:
1) String s = new String(“xyz”); 产生几个对象?
一个或两个。如果常量池中原来没有 ”xyz”, 就是两个。如果原来的常量池中存在“xyz”时,就是一个。
2) String作为一个对象来使用
例子一:对象不同,内容相同,”==”返回false,equals返回true
String s1 = new String(“java”);
String s2 = new String(“java”);
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
例子二:同一对象,”==”和equals结果相同
String s1 = new String(“java”);
String s2 = s1;
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
String作为一个基本类型来使用
如果值不相同,对象就不相同,所以”==” 和equals结果一样
String s1 = “java”;
String s2 = “java”;
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
如果String缓冲池内不存在与其指定值相同的String对象,那么此时虚拟机将为此创建新的String对象,并存放在String缓冲池内。
如果String缓冲池内存在与其指定值相同的String对象,那么此时虚拟机将不为此创建新的String对象,而直接返回已存在的String对象的引用。他的最新文章
他的热门文章
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)

我要回帖

更多关于 jvm类加载机制 的文章

 

随机推荐