二、JVM
1、Java内存区
- **JVM内存区:**程序计数器、虚拟机栈、本地方法栈、堆、方法区(包括常量池)。
- **不属于JVM内存区:**直接内存(Direct Memory),用户I/O操作
2、运行时常量池
-
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
-
**符号引用 :**符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。在解析阶段会有一个步将常量池当中二进制数据当中的符号引用转化为直接引用的过程。符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
Cat cat=new Cat(); Cat cat=null;
3、jvm命令
- jps:查看本机java进程信息。
- jstack:打印线程的栈信息,制作线程dump文件。
- jmap:打印内存映射,制作堆dump文件
- jstat:性能监控工具
- jhat:内存分析工具
- jconsole:简易的可视化控制台
- jvisualvm:功能强大的控制台
4、内存分配
Java把内存分成两种,一种叫做栈内存,一种叫做堆内存。
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。
引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候(比如先前的引用变量x=null时),才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因。
以上段落来自于某一本Java程序设计的书中,实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针。
总结起来就是对象存储在堆内存,引用变量存储在栈内存。栈内存指向堆内存
例1:
下面程序的输出是:(D)
String x="fmn";
x.toUpperCase();
String y=x.replace('f','F');
y=y+"wxy";
System.out.println(y);
- A:FmNwxy
- B:fmnwxy
- C:wxyfmn
- D:Fmnwxy
解析:
String x=“fmn”; “fmn”是在常量池里的不可变对象。
toUpperCase()会对当前对象进行检查 如果不需要转换直接返回当前对象,否则new一个新对象返回;x.toUpperCase(); 在堆中new一个"FMN"对象,但无任何引用指向它。
replace()如果两个参数相同,则直接返回,否则new一个新对象,所以这里y指向"Fmn";String y=x.replace(‘f’,‘F’); 在堆中 new一个"Fmn"对象,y指向它。
y=y+“wxy”; 在堆中 重新new一个"Fmnwxy"对象, 修改y指向,现在y指向它。
5、关于OutOfMemoryError
java.lang.OutOfMemoryError: PermGen space
"永久代"内存大小不足,“永久代”的解释应该为JVM中的方法区,主要用于存储类信息,常量,静态变量,即时编译器编译后代码等。本错误仅限于Hotspot虚拟机,本区进行垃圾回收很少,不够直接加大简单粗暴。 运行时常量池导致的溢出,设置-XX:MaxPermSize可以解决这个问题,
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
直接翻译报错信息:数组过长导致堆内存溢出,加大堆内存或者减少数组长度。属于堆空间不足导致的错误,问题比较少见,解决方式和C相同,
java.lang.OutOfMemoryError: Java heap space
属于java堆内存问题,一般的手段是通过内存映像分析工具,对Dump出来的堆转储存快照进行分析,重点是确认内存中的对象是否是有必要的,也就是要判断是出现了内存泄漏(用资源的时候为他开辟了一段空间,当你用完时忘记释放资源),还是出现了内存溢出,如果是内存列楼,通过工具检查泄露对象打GC Roots的引用链信息,可以准确的确定出泄露代码的位置,不存在泄露,就应该检查虚拟机的堆参数,如果可以继续调大,可以设置-Xmx解决问题 ,堆内存不足,直接增大堆内存。
java.lang.OutOfMemoryError: nativeGetNewTLA
指当虚拟机不能分配新的线程本地空间(Thread Local Area)的时候错误信息,此错误是线程申请一个新的TLA时产生的,这个异常一般只会发生在jRockit虚拟机,只有过于绝对。
6、堆中的新生代与老年代
- 在JAVA中堆被分为两块区域:新生代(young)、老年代(old)。
- 堆大小=新生代+老年代;(新生代占堆空间的1/3、老年代占堆空间2/3)
- 新生代又被分为了eden、from survivor、to survivor(8:1:1);
(1)新生代
新生代这样划分是为了更好的管理堆内存中的对象,方便GC算法—复制算法来进行垃圾回收。
JVM每次只会使用eden和其中一块survivor来为对象服务,所以无论什么时候,都会有一块survivor空间,因此新生代实际可用空间只有90%。
新生代几乎是所有JAVA对象出生的地方,JAVA对象申请的内存和存放都是在这个地方。
新生代GC(minor gc)——当对象在eden(其中包括一个survivor,假如是from),当此对象经过一次minor gc后仍然存活,并且能够被另外一块survivor所容纳(这里survivor则是to了),则使用复制算法将这些仍然存活的对象复制到to survior区域中,然后清理掉eden和from survivor区域,并将这些存活的对象年龄+1,以后对象在survivor中每熬过一次gc则增加1,当年龄达到某个值时(默认15,通过设置参数-xx:maxtenuringThreshold来设置),这些对象就会成为老年代!当一些较大的对象(需要分配连续的内存空间)则直接进入老年代。
(2)老年代
老年代GC(major/full gc)——指发生在老年代的垃圾回收动作,所采用是的标记–整理算法。老年代几乎都是经过survivor熬过来的,它们是不会那么容易“死掉”,因此major gc不会想minor gc那样频繁。
7、永久代的回收机制
hotspot的方法区存放在永久代中,因此方法区被人们称为永久代。永久代的垃圾回收主要包括类型的卸载和废弃常量池的回收。
(1)废弃常量池的回收
当没有对象引用一个常量的时候,该常量即可以被回收。
(2)类型的卸载
必须满足一下三点: a:该类型的所有实例都被回收了, b:该类型的ClassLoader被回收了, c:该类型对应的java.lang.Class没有在任何地方被引用,在任何地方都无法通过反射来实例化一个对象
8、垃圾回收的停顿现象
GC任务是识别和回收垃圾对象,进行内存清理
为了让GC可以高效的执行,在进行GC时,系统会进入一个停顿的状态
停顿目的
终止所有应用线程
只有这样,系统才不会有新的垃圾产生
停顿保证了系统状态,在某一瞬间的一致性,有益于更好的标记垃圾对象
因此,在GC时,都会产生应用程序的停顿
减少GC
可以减少程序的停顿,提高系统的性能
9、同步(Synchronous)和异步(Asynchronous)的概念
web项目中的同步与异步:
同步请求呢往往代表着你必须等待这次请求结束并且刷新整个界面之后,你才能进行下一步操作,而异步请求则可以不刷新界面,它会立即返回,界面也可以继续执行其它的操作。
在Java代码中的同步与异步:
同步的方法一旦执行,调用的地方就必须等待方法调用返回后,才能继续执行后续的功能。
异步的方法更像是传递一个消息,一旦开始,方法就会立即返回,调用的地方就可以继续执行其它的功能。异步的方法通常会在另外一个线程中继续执行,不会阻碍调用者的后续工作。对于调用者来说,异步调用似乎是在一瞬间完成的。如果异步调用需要返回结果,那么当异步调用真实完成时,则会通知调用者。
举个生活中的例子:
比如你在使用支付宝的时候,想给你的女朋友发送一个520元的红包,站在同步的角度来理解就是,你发送之后要等你的女朋友领了红包你才能去做其他的事情,站在异步的角度来理解就是,你发送红包之后马上就可以去做其他的事情,比如看看小电影啥的,等你女朋友随时领了红包之后,支付宝会提示你的红包已被你女朋友领取,你也可以继续做其他的事情。
10、类构造器
初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。(继承中的初始化顺序)
注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触
发定义常量所在的类。 - 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初
始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。 - 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
(ClassLoader类的作用就是根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例.)
11、类加载器
1、启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
2、扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
3、应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。
12、双亲委派模型
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。